Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: simplify how subjects are created from existing manifest #131

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
The versions coincide with releases on pip. Only major versions will be released as tags on Github.

## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x)
- Allow generating a Subject from a pre-existing Manifest (0.1.30)
- add option to not refresh headers during the pushing flow, useful for push with basic auth (0.1.29)
- enable additionalProperties in schema validation (0.1.28)
- Introduce the option to not refresh headers when fetching manifests when pulling artifacts (0.1.27)
Expand Down
22 changes: 22 additions & 0 deletions docs/getting_started/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,28 @@ def push(uri, root):

</details>

<details>

<summary>Example of basic artifact attachment</summary>

We are assuming an `derived-artifact.txt` in the present working directory and that there's already a `localhost:5000/dinosaur/artifact:v1` artifact present in the registry. Here is an example of how to [attach](https://oras.land/docs/concepts/reftypes/) a derived artifact to the existing artifact.

```python
import oras.client
import oras.oci

client = oras.client.OrasClient(insecure=True)

manifest = client.remote.get_manifest("localhost:5000/dinosaur/artifact:v1")
subject = oras.oci.Subject.from_manifest(manifest)

client.push(files=["derived-artifact.txt"], target="localhost:5000/dinosaur/artifact:v1-derived", subject=subject)
Successfully pushed localhost:5000/dinosaur/artifact:v1-derived
Out[4]: <Response [201]>
```

</details>

The above examples are just a start! See our [examples](https://github.com/oras-project/oras-py/tree/main/examples)
folder alongside the repository for more code examples and clients. If you would like help
for an example, or to contribute an example, [you know what to do](https://github.com/oras-project/oras-py/issues)!
Expand Down
27 changes: 27 additions & 0 deletions oras/oci.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
__license__ = "Apache-2.0"

import copy
import hashlib
import json
import os
from dataclasses import dataclass
from typing import Dict, Optional, Tuple

import jsonschema
Expand Down Expand Up @@ -151,3 +154,27 @@ def NewManifest() -> dict:
Get an empty manifest config.
"""
return copy.deepcopy(EmptyManifest)


@dataclass
class Subject:
mediaType: str
digest: str
size: int

@classmethod
def from_manifest(cls, manifest: dict) -> "Subject":
"""
Create a new Subject from a Manifest

:param manifest: manifest to convert to subject
"""
manifest_string = json.dumps(manifest).encode("utf-8")
digest = "sha256:" + hashlib.sha256(manifest_string).hexdigest()
size = len(manifest_string)

return cls(
manifest["mediaType"] or oras.defaults.default_manifest_media_type,
digest,
size,
)
11 changes: 2 additions & 9 deletions oras/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import urllib
from contextlib import contextmanager, nullcontext
from dataclasses import asdict, dataclass
from dataclasses import asdict
from http.cookiejar import DefaultCookiePolicy
from tempfile import TemporaryDirectory
from typing import Callable, Generator, List, Optional, Tuple, Union
Expand Down Expand Up @@ -35,13 +35,6 @@ def temporary_empty_config() -> Generator[str, None, None]:
yield config_file


@dataclass
class Subject:
mediaType: str
digest: str
size: int


class Registry:
"""
Direct interactions with an OCI registry.
Expand Down Expand Up @@ -697,7 +690,7 @@ def push(self, *args, **kwargs) -> requests.Response:
:param refresh_headers: if true or None, headers are refreshed
:type refresh_headers: bool
:param subject: optional subject reference
:type subject: Subject
:type subject: oras.oci.Subject
"""
container = self.get_container(kwargs["target"])
self.load_configs(container, configs=kwargs.get("config_path"))
Expand Down
20 changes: 20 additions & 0 deletions oras/tests/test_oci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest

import oras.defaults
import oras.oci


@pytest.mark.with_auth(False)
def test_create_subject_from_manifest():
"""
Basic tests for oras Subject creation from empty manifest
"""
manifest = oras.oci.NewManifest()
subject = oras.oci.Subject.from_manifest(manifest)

assert subject.mediaType == oras.defaults.default_manifest_media_type
assert (
subject.digest
== "sha256:7a6f84d8c73a71bf9417c13f721ed102f74afac9e481f89e5a72d28954e7d0c5"
)
assert subject.size == 126
1 change: 1 addition & 0 deletions oras/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import oras.client
import oras.defaults
import oras.oci
import oras.provider
import oras.utils

Expand Down
2 changes: 1 addition & 1 deletion oras/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
__copyright__ = "Copyright The ORAS Authors."
__license__ = "Apache-2.0"

__version__ = "0.1.29"
__version__ = "0.1.30"
AUTHOR = "Vanessa Sochat"
EMAIL = "[email protected]"
NAME = "oras"
Expand Down
Loading