diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index fb74cd2b..7e98aacf 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -155,3 +155,7 @@ jobs: - name: Run notebooks run: | jupyter nbconvert --to notebook --execute notebooks/*.ipynb + env: + OMMX_BASIC_AUTH_DOMAIN: ghcr.io + OMMX_BASIC_AUTH_USERNAME: ${{ github.actor }} + OMMX_BASIC_AUTH_PASSWORD: ${{ github.token }} diff --git a/Cargo.lock b/Cargo.lock index 9e15f7dd..662cfee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "ocipkg", "ommx", "pyo3", + "pyo3-log", "serde-pyobject", "serde_json", ] @@ -77,9 +78,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -100,6 +101,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "autocfg" version = "1.3.0" @@ -237,7 +244,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -323,7 +330,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -334,7 +341,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -355,7 +362,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -365,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -950,7 +957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1033,7 +1040,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.63", + "syn 2.0.66", "tempfile", ] @@ -1047,7 +1054,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1108,6 +1115,17 @@ dependencies = [ "pyo3-build-config", ] +[[package]] +name = "pyo3-log" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af49834b8d2ecd555177e63b273b708dea75150abc6f5341d0a6e1a9623976c" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + [[package]] name = "pyo3-macros" version = "0.21.2" @@ -1117,7 +1135,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1130,7 +1148,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1388,7 +1406,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1459,9 +1477,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1470,9 +1488,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -1514,7 +1532,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1720,7 +1738,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -1742,7 +1760,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1755,9 +1773,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" dependencies = [ "rustls-pki-types", ] @@ -1921,9 +1939,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index d8b13fcb..d298a5ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ proptest = "1.4.0" prost = "0.12.6" prost-build = "0.12.6" pyo3 = { version = "0.21.2", features = ["anyhow"] } +pyo3-log = "0.10.0" rand = "0.8.5" rand_xoshiro = "0.6.0" serde = { version = "1.0.197", features = ["derive"] } diff --git a/notebooks/artifact.ipynb b/notebooks/artifact.ipynb new file mode 100644 index 00000000..bf7a62a4 --- /dev/null +++ b/notebooks/artifact.ipynb @@ -0,0 +1,155 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ec41c28b-5d3f-4e6c-a76a-bae41a57186f", + "metadata": {}, + "source": [ + "# Create OMMX Artifact" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f8fe903b-8acd-40dc-9263-7c5f9197876d", + "metadata": {}, + "outputs": [], + "source": [ + "import uuid # To generate random tag for testing\n", + "import logging # To see the log of pushing artifact\n", + "logging.basicConfig(level=logging.INFO)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "57b9e174-2c4c-444a-9ea7-d0ab6f112162", + "metadata": {}, + "outputs": [], + "source": [ + "from ommx.artifact import Artifact, ArtifactBuilder\n", + "from ommx.testing import SingleFeasibleLPGenerator, DataType" + ] + }, + { + "cell_type": "markdown", + "id": "a2779a4a-4f76-4c77-a2e2-c64d5a4c2aff", + "metadata": {}, + "source": [ + "## Ready `ommx.v1.Instance` to be packed into artifact" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4be947b0-95c5-477b-8c7e-d2501c03a2d8", + "metadata": {}, + "outputs": [], + "source": [ + "generator = SingleFeasibleLPGenerator(3, DataType.INT)\n", + "instance = generator.get_v1_instance()" + ] + }, + { + "cell_type": "markdown", + "id": "cb3adc3e-483d-4c4a-96fc-23888a43e1f1", + "metadata": {}, + "source": [ + "## Build OMMX artifact" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "00061527-a0ee-4b78-8727-8472c27cac7e", + "metadata": {}, + "outputs": [], + "source": [ + "builder = ArtifactBuilder.for_github(\n", + " \"Jij-Inc\", # GitHub organization\n", + " \"ommx\", # Repository name\n", + " \"single_feasible_lp\", # Name of artifact\n", + " str(uuid.uuid4()) # Tag of artifact\n", + ")\n", + "builder.add_instance(instance)\n", + "artifact = builder.build()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5374f4f4-1d6f-44b4-83f2-8056f349a36c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'ghcr.io/jij-inc/ommx/single_feasible_lp:4c09b065-a55c-4358-8083-9ce3db3b6e6c'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "artifact.image_name" + ] + }, + { + "cell_type": "markdown", + "id": "a04b993d-d13c-4fba-83fc-677da7d6543d", + "metadata": {}, + "source": [ + "## Push artifact to container registry\n", + "\n", + "- This artifact will be pushed to \n", + "- `push` requires authentication using personal access token (PAT) or `GITHUB_TOKEN` on GitHub Actions." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a06ef2f5-595f-485a-9ca9-11923278123b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:ommx.artifact:Pushing: ghcr.io/jij-inc/ommx/single_feasible_lp:4c09b065-a55c-4358-8083-9ce3db3b6e6c\n", + "INFO:ocipkg.distribution.client:POST https://ghcr.io/v2/jij-inc/ommx/single_feasible_lp/blobs/uploads/\n", + "INFO:ocipkg.distribution.client:PUT https://ghcr.io/v2/jij-inc/ommx/single_feasible_lp/blobs/upload/ebb244fb-38e4-4472-b460-932b43569344\n", + "INFO:ocipkg.distribution.client:POST https://ghcr.io/v2/jij-inc/ommx/single_feasible_lp/blobs/uploads/\n", + "INFO:ocipkg.distribution.client:PUT https://ghcr.io/v2/jij-inc/ommx/single_feasible_lp/blobs/upload/09b62410-7376-4cbe-bfc2-aaac66736b43\n", + "INFO:ocipkg.distribution.client:PUT https://ghcr.io/v2/jij-inc/ommx/single_feasible_lp/manifests/4c09b065-a55c-4358-8083-9ce3db3b6e6c\n" + ] + } + ], + "source": [ + "artifact.push()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/python/Cargo.toml b/python/Cargo.toml index f93e50b7..383f523b 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -22,5 +22,6 @@ anyhow.workspace = true derive_more.workspace = true ocipkg.workspace = true pyo3.workspace = true +pyo3-log.workspace = true serde-pyobject.workspace = true serde_json.workspace = true diff --git a/python/ommx/_ommx_rust.pyi b/python/ommx/_ommx_rust.pyi index c8015c66..f4f550e4 100644 --- a/python/ommx/_ommx_rust.pyi +++ b/python/ommx/_ommx_rust.pyi @@ -17,10 +17,13 @@ class ArtifactArchive: @staticmethod def from_oci_archive(path: str) -> ArtifactArchive: ... @property + def image_name(self) -> str | None: ... + @property def annotations(self) -> dict[str, str]: ... @property def layers(self) -> list[Descriptor]: ... def get_blob(self, digest: str) -> bytes: ... + def push(self): ... class ArtifactDir: @staticmethod @@ -28,10 +31,13 @@ class ArtifactDir: @staticmethod def from_image_name(image_name: str) -> ArtifactDir: ... @property + def image_name(self) -> str | None: ... + @property def annotations(self) -> dict[str, str]: ... @property def layers(self) -> list[Descriptor]: ... def get_blob(self, digest: str) -> bytes: ... + def push(self): ... class ArtifactArchiveBuilder: @staticmethod @@ -47,6 +53,8 @@ class ArtifactArchiveBuilder: class ArtifactDirBuilder: @staticmethod def new(image_name: str) -> ArtifactDirBuilder: ... + @staticmethod + def for_github(org: str, repo: str, name: str, tag: str) -> ArtifactDirBuilder: ... def add_layer( self, media_type: str, blob: bytes, annotations: dict[str, str] ) -> Descriptor: ... diff --git a/python/ommx/artifact.py b/python/ommx/artifact.py index 572869c1..7942bafd 100644 --- a/python/ommx/artifact.py +++ b/python/ommx/artifact.py @@ -27,9 +27,11 @@ def load_archive(path: str | Path) -> Artifact: Load an artifact stored as a single file >>> artifact = Artifact.load_archive("data/random_lp_instance.ommx") + >>> print(artifact.image_name) + ghcr.io/jij-inc/ommx/random_lp_instance:... >>> for layer in artifact.layers: ... print(layer.digest) - sha256:93fdc9fcb8e21b34e3517809a348938d9455e9b9e579548bbf018a514c082df2 + sha256:... """ if isinstance(path, str): @@ -52,6 +54,8 @@ def load(image_name: str) -> Artifact: If the image is not found in local registry, it will try to pull from remote registry. >>> artifact = Artifact.load("ghcr.io/jij-inc/ommx/random_lp_instance:4303c7f") + >>> print(artifact.image_name) + ghcr.io/jij-inc/ommx/random_lp_instance:4303c7f >>> for layer in artifact.layers: ... print(layer.digest) sha256:93fdc9fcb8e21b34e3517809a348938d9455e9b9e579548bbf018a514c082df2 @@ -60,8 +64,28 @@ def load(image_name: str) -> Artifact: base = ArtifactDir.from_image_name(image_name) return Artifact(base) + def push(self): + """ + Push the artifact to remote registry + """ + self._base.push() + + @property + def image_name(self) -> str | None: + return self._base.image_name + @property def annotations(self) -> dict[str, str]: + """ + Annotations in the artifact manifest + + >>> artifact = Artifact.load("ghcr.io/jij-inc/ommx/random_lp_instance:4303c7f") + >>> print(artifact.annotations['org.opencontainers.image.source']) + https://github.com/Jij-Inc/ommx + >>> print(artifact.annotations['org.opencontainers.image.description']) + Test artifact created by examples/artifact_archive.rs + + """ return self._base.annotations @property @@ -92,7 +116,7 @@ def get_layer(self, descriptor: Descriptor) -> Instance | Solution: """ Get the layer object corresponding to the descriptor - This is dynamically dispatched based on the :attr:`Descriptor.media_type`. + This is dynamically dispatched based on the :py:attr:`Descriptor.media_type`. """ if descriptor.media_type == "application/org.ommx.v1.instance": return self.get_instance(descriptor) @@ -133,18 +157,30 @@ def new_archive_unnamed(path: str | Path) -> ArtifactBuilder: """ Create a new artifact archive with an unnamed image name. This cannot be loaded into local registry nor pushed to remote registry. + Example + ======== + + Ready instance to be added to the artifact + >>> from ommx.testing import SingleFeasibleLPGenerator, DataType >>> generator = SingleFeasibleLPGenerator(3, DataType.INT) >>> instance = generator.get_v1_instance() + File name for the artifact + >>> import uuid # To generate a unique name for testing - >>> builder = ArtifactBuilder.new_archive_unnamed( - ... f"data/single_feasible_lp.ommx.{uuid.uuid4()}" - ... ) - >>> builder.add_instance(instance) - - >>> builder.build() - Artifact(_base=) + >>> filename = f"data/single_feasible_lp.ommx.{uuid.uuid4()}" + + Build the artifact + + >>> builder = ArtifactBuilder.new_archive_unnamed(filename) + >>> _desc = builder.add_instance(instance) + >>> artifact = builder.build() + + In this case, the :py:attr:`Artifact.image_name` is `None`. + + >>> print(artifact.image_name) + None """ if isinstance(path, str): @@ -154,22 +190,32 @@ def new_archive_unnamed(path: str | Path) -> ArtifactBuilder: @staticmethod def new_archive(path: str | Path, image_name: str) -> ArtifactBuilder: """ - Create a new artifact archive with a named image name like `ghcr.io/jij-inc/ommx/random_lp_instance:4303c7f`. + Create a new artifact archive with a named image name + + Example + ======== + + Ready instance to be added to the artifact >>> from ommx.testing import SingleFeasibleLPGenerator, DataType >>> generator = SingleFeasibleLPGenerator(3, DataType.INT) >>> instance = generator.get_v1_instance() + File name and image name for the artifact. + >>> import uuid # To generate a unique name for testing >>> tag = uuid.uuid4() - >>> builder = ArtifactBuilder.new_archive( - ... f"data/single_feasible_lp.ommx.{tag}", - ... f"ghcr.io/jij-inc/ommx/single_feasible_lp:{tag}" - ... ) - >>> builder.add_instance(instance) - - >>> builder.build() - Artifact(_base=) + >>> filename = f"data/single_feasible_lp.ommx.{tag}" + >>> image_name = f"ghcr.io/jij-inc/ommx/single_feasible_lp:{tag}" + + Build the artifact + + >>> builder = ArtifactBuilder.new_archive(filename, image_name) + >>> _desc = builder.add_instance(instance) + >>> artifact = builder.build() + + >>> print(artifact.image_name) + ghcr.io/jij-inc/ommx/single_feasible_lp:... """ if isinstance(path, str): @@ -179,24 +225,65 @@ def new_archive(path: str | Path, image_name: str) -> ArtifactBuilder: @staticmethod def new(image_name: str) -> ArtifactBuilder: """ - Create a new artifact in local registry with a named image name like `ghcr.io/jij-inc/ommx/random_lp_instance:4303c7f`. + Create a new artifact in local registry with a named image name + + Example + ======== + + Ready instance to be added to the artifact >>> from ommx.testing import SingleFeasibleLPGenerator, DataType >>> generator = SingleFeasibleLPGenerator(3, DataType.INT) >>> instance = generator.get_v1_instance() + Image name for the artifact + >>> import uuid # To generate a unique name for testing - >>> builder = ArtifactBuilder.new( - ... f"ghcr.io/jij-inc/ommx/single_feasible_lp:{uuid.uuid4()}" - ... ) - >>> builder.add_instance(instance) - - >>> builder.build() - Artifact(_base=) + >>> image_name = f"ghcr.io/jij-inc/ommx/single_feasible_lp:{uuid.uuid4()}" + + Build the artifact + + >>> builder = ArtifactBuilder.new(image_name) + >>> _desc = builder.add_instance(instance) + >>> artifact = builder.build() + + >>> print(artifact.image_name) + ghcr.io/jij-inc/ommx/single_feasible_lp:... """ return ArtifactBuilder(ArtifactDirBuilder.new(image_name)) + @staticmethod + def for_github(org: str, repo: str, name: str, tag: str) -> ArtifactBuilder: + """ + An alias for :py:meth:`new` to create a new artifact in local registry with GitHub Container Registry image name + + This also set the `org.opencontainers.image.source` annotation to the GitHub repository URL. + + Example + ======== + + Ready instance to be added to the artifact + + >>> from ommx.testing import SingleFeasibleLPGenerator, DataType + >>> generator = SingleFeasibleLPGenerator(3, DataType.INT) + >>> instance = generator.get_v1_instance() + + Build the artifact + + >>> import uuid # To generate a unique name for testing + >>> builder = ArtifactBuilder.for_github( + ... "Jij-Inc", "ommx", "single_feasible_lp", str(uuid.uuid4()) + ... ) + >>> _desc = builder.add_instance(instance) + >>> artifact = builder.build() + + >>> print(artifact.image_name) + ghcr.io/jij-inc/ommx/single_feasible_lp:... + + """ + return ArtifactBuilder(ArtifactDirBuilder.for_github(org, repo, name, tag)) + def add_instance( self, instance: Instance, annotations: dict[str, str] = {} ) -> Descriptor: diff --git a/python/src/artifact.rs b/python/src/artifact.rs index 93a5bfad..77feeb1e 100644 --- a/python/src/artifact.rs +++ b/python/src/artifact.rs @@ -22,6 +22,11 @@ impl ArtifactArchive { Ok(Self(artifact)) } + #[getter] + pub fn image_name(&mut self) -> Option { + self.0.get_name().map(|name| name.to_string()).ok() + } + #[getter] pub fn annotations(&mut self) -> Result> { let manifest = self.0.get_manifest()?; @@ -44,6 +49,13 @@ impl ArtifactArchive { let blob = self.0.get_blob(&digest)?; Ok(PyBytes::new_bound(py, blob.as_ref())) } + + pub fn push(&mut self) -> Result<()> { + // Do not expose Artifact to Python API for simplicity. + // In Python API, the `Artifact` class always refers to the local artifact, which may be either an OCI archive or an OCI directory. + let _remote = self.0.push()?; + Ok(()) + } } #[pyclass] @@ -70,6 +82,11 @@ impl ArtifactDir { Ok(Self(artifact)) } + #[getter] + pub fn image_name(&mut self) -> Option { + self.0.get_name().map(|name| name.to_string()).ok() + } + #[getter] pub fn annotations(&mut self) -> Result> { let manifest = self.0.get_manifest()?; @@ -92,4 +109,11 @@ impl ArtifactDir { let blob = self.0.get_blob(&digest)?; Ok(PyBytes::new_bound(py, blob.as_ref())) } + + pub fn push(&mut self) -> Result<()> { + // Do not expose Artifact to Python API for simplicity. + // In Python API, the `Artifact` class always refers to the local artifact, which may be either an OCI archive or an OCI directory. + let _remote = self.0.push()?; + Ok(()) + } } diff --git a/python/src/builder.rs b/python/src/builder.rs index cd362a4d..0070d49d 100644 --- a/python/src/builder.rs +++ b/python/src/builder.rs @@ -74,6 +74,12 @@ impl ArtifactDirBuilder { Ok(Self(Some(builder))) } + #[staticmethod] + pub fn for_github(org: &str, repo: &str, name: &str, tag: &str) -> Result { + let builder = Builder::for_github(org, repo, name, tag)?; + Ok(Self(Some(builder))) + } + pub fn add_layer( &mut self, media_type: &str, diff --git a/python/src/lib.rs b/python/src/lib.rs index b4b23d4e..51653298 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -12,6 +12,8 @@ use pyo3::prelude::*; #[pymodule] fn _ommx_rust(_py: Python, m: &Bound) -> PyResult<()> { + pyo3_log::init(); + m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/rust/ommx/src/artifact/builder.rs b/rust/ommx/src/artifact/builder.rs index 797837dc..57a6ee8a 100644 --- a/rust/ommx/src/artifact/builder.rs +++ b/rust/ommx/src/artifact/builder.rs @@ -13,6 +13,7 @@ use std::{ ops::{Deref, DerefMut}, path::PathBuf, }; +use url::Url; /// Build [Artifact] pub struct Builder(OciArtifactBuilder); @@ -57,6 +58,23 @@ impl Builder { media_types::v1_artifact(), )?)) } + + /// Create a new artifact builder for a GitHub container registry image + pub fn for_github(org: &str, repo: &str, name: &str, tag: &str) -> Result { + let image_name = ImageName::parse(&format!( + "ghcr.io/{}/{}/{}:{}", + org.to_lowercase(), + repo.to_lowercase(), + name.to_lowercase(), + tag + ))?; + let source = Url::parse(&format!("https://github.com/{org}/{repo}"))?; + + let mut builder = Self::new(image_name)?; + builder.add_source(&source); + + Ok(builder) + } } impl Builder {