omdev 0.0.0.dev221__py3-none-any.whl → 0.0.0.dev223__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. omdev/ci/cache.py +40 -23
  2. omdev/ci/ci.py +49 -109
  3. omdev/ci/cli.py +24 -23
  4. omdev/ci/docker/__init__.py +0 -0
  5. omdev/ci/docker/buildcaching.py +69 -0
  6. omdev/ci/docker/cache.py +57 -0
  7. omdev/ci/{docker.py → docker/cmds.py} +1 -44
  8. omdev/ci/docker/imagepulling.py +64 -0
  9. omdev/ci/docker/inject.py +37 -0
  10. omdev/ci/docker/utils.py +48 -0
  11. omdev/ci/github/cache.py +15 -5
  12. omdev/ci/github/inject.py +30 -0
  13. omdev/ci/inject.py +61 -0
  14. omdev/dataserver/__init__.py +1 -0
  15. omdev/dataserver/handlers.py +198 -0
  16. omdev/dataserver/http.py +69 -0
  17. omdev/dataserver/routes.py +49 -0
  18. omdev/dataserver/server.py +90 -0
  19. omdev/dataserver/targets.py +89 -0
  20. omdev/oci/__init__.py +0 -0
  21. omdev/oci/building.py +221 -0
  22. omdev/oci/compression.py +8 -0
  23. omdev/oci/data.py +151 -0
  24. omdev/oci/datarefs.py +138 -0
  25. omdev/oci/dataserver.py +61 -0
  26. omdev/oci/loading.py +142 -0
  27. omdev/oci/media.py +179 -0
  28. omdev/oci/packing.py +381 -0
  29. omdev/oci/repositories.py +159 -0
  30. omdev/oci/tars.py +144 -0
  31. omdev/pyproject/resources/python.sh +1 -1
  32. omdev/scripts/ci.py +1841 -384
  33. omdev/scripts/interp.py +100 -22
  34. omdev/scripts/pyproject.py +122 -28
  35. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/METADATA +2 -2
  36. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/RECORD +40 -15
  37. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/LICENSE +0 -0
  38. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/WHEEL +0 -0
  39. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/entry_points.txt +0 -0
  40. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+ from omlish.lite.check import check
7
+ from omlish.lite.dataclasses import dataclass_maybe_post_init
8
+ from omlish.lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
9
+
10
+
11
+ ##
12
+
13
+
14
+ @dc.dataclass(frozen=True)
15
+ class DataServerTarget(abc.ABC): # noqa
16
+ content_type: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
17
+ content_length: ta.Optional[int] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
18
+
19
+ @classmethod
20
+ def of(
21
+ cls,
22
+ obj: ta.Union[
23
+ 'DataServerTarget',
24
+ bytes,
25
+ None,
26
+ ] = None,
27
+ *,
28
+
29
+ file_path: ta.Optional[str] = None,
30
+ url: ta.Optional[str] = None,
31
+
32
+ **kwargs: ta.Any,
33
+ ) -> 'DataServerTarget':
34
+ if isinstance(obj, DataServerTarget):
35
+ check.none(file_path)
36
+ check.none(url)
37
+ check.empty(kwargs)
38
+ return obj
39
+
40
+ elif isinstance(obj, bytes):
41
+ return BytesDataServerTarget(
42
+ data=obj,
43
+ **kwargs,
44
+ )
45
+
46
+ elif file_path is not None:
47
+ check.none(obj)
48
+ check.none(url)
49
+ return FileDataServerTarget(
50
+ file_path=file_path,
51
+ **kwargs,
52
+ )
53
+
54
+ elif url is not None:
55
+ check.none(obj)
56
+ check.none(file_path)
57
+ return UrlDataServerTarget(
58
+ url=url,
59
+ **kwargs,
60
+ )
61
+
62
+ else:
63
+ raise TypeError('No target type provided')
64
+
65
+
66
+ @dc.dataclass(frozen=True)
67
+ class BytesDataServerTarget(DataServerTarget):
68
+ data: ta.Optional[bytes] = None # required
69
+
70
+
71
+ @dc.dataclass(frozen=True)
72
+ class FileDataServerTarget(DataServerTarget):
73
+ file_path: ta.Optional[str] = None # required
74
+
75
+ def __post_init__(self) -> None:
76
+ dataclass_maybe_post_init(super())
77
+ check.non_empty_str(self.file_path)
78
+
79
+
80
+ @dc.dataclass(frozen=True)
81
+ class UrlDataServerTarget(DataServerTarget):
82
+ url: ta.Optional[str] = None # required
83
+ methods: ta.Optional[ta.Sequence[str]] = None # required
84
+
85
+ def __post_init__(self) -> None:
86
+ dataclass_maybe_post_init(super())
87
+ check.non_empty_str(self.url)
88
+ check.not_none(self.methods)
89
+ check.not_isinstance(self.methods, str)
omdev/oci/__init__.py ADDED
File without changes
omdev/oci/building.py ADDED
@@ -0,0 +1,221 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import json
5
+ import typing as ta
6
+
7
+ from omlish.lite.check import check
8
+ from omlish.lite.json import json_dumps_compact
9
+ from omlish.lite.marshal import marshal_obj
10
+
11
+ from .data import OciDataclass
12
+ from .data import OciImageConfig
13
+ from .data import OciImageIndex
14
+ from .data import OciImageLayer
15
+ from .data import OciImageManifest
16
+ from .datarefs import BytesOciDataRef
17
+ from .datarefs import OciDataRef
18
+ from .datarefs import OciDataRefInfo
19
+ from .datarefs import open_oci_data_ref
20
+ from .media import OCI_IMAGE_LAYER_KIND_MEDIA_TYPES
21
+ from .media import OciMediaDataclass
22
+ from .media import OciMediaDescriptor
23
+ from .media import OciMediaImageConfig
24
+ from .media import OciMediaImageIndex
25
+ from .media import OciMediaImageManifest
26
+ from .media import unmarshal_oci_media_dataclass
27
+
28
+
29
+ OciMediaDataclassT = ta.TypeVar('OciMediaDataclassT', bound='OciMediaDataclass')
30
+
31
+
32
+ ##
33
+
34
+
35
+ class OciRepositoryBuilder:
36
+ @dc.dataclass(frozen=True)
37
+ class Blob:
38
+ digest: str
39
+
40
+ data: OciDataRef
41
+ info: OciDataRefInfo
42
+
43
+ media_type: ta.Optional[str] = None
44
+
45
+ #
46
+
47
+ def read(self) -> bytes:
48
+ with open_oci_data_ref(self.data) as f:
49
+ return f.read()
50
+
51
+ def read_json(self) -> ta.Any:
52
+ return json.loads(self.read().decode('utf-8'))
53
+
54
+ def read_media(
55
+ self,
56
+ cls: ta.Type[OciMediaDataclassT] = OciMediaDataclass, # type: ignore[assignment]
57
+ ) -> OciMediaDataclassT:
58
+ mt = check.non_empty_str(self.media_type)
59
+ dct = self.read_json()
60
+ obj = unmarshal_oci_media_dataclass(
61
+ dct,
62
+ media_type=mt,
63
+ )
64
+ return check.isinstance(obj, cls)
65
+
66
+ def __init__(self) -> None:
67
+ super().__init__()
68
+
69
+ self._blobs: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
70
+
71
+ #
72
+
73
+ def get_blobs(self) -> ta.Dict[str, Blob]:
74
+ return dict(self._blobs)
75
+
76
+ def add_blob(
77
+ self,
78
+ r: OciDataRef,
79
+ ri: ta.Optional[OciDataRefInfo] = None,
80
+ *,
81
+ media_type: ta.Optional[str] = None,
82
+ ) -> Blob:
83
+ if ri is None:
84
+ ri = OciDataRefInfo(r)
85
+
86
+ if (dg := ri.digest()) in self._blobs:
87
+ raise KeyError(ri.digest())
88
+
89
+ blob = self.Blob(
90
+ digest=dg,
91
+
92
+ data=r,
93
+ info=ri,
94
+
95
+ media_type=media_type,
96
+ )
97
+
98
+ self._blobs[dg] = blob
99
+
100
+ return blob
101
+
102
+ #
103
+
104
+ def marshal_media(self, obj: OciMediaDataclass) -> bytes:
105
+ check.isinstance(obj, OciMediaDataclass)
106
+ m = marshal_obj(obj)
107
+ j = json_dumps_compact(m)
108
+ b = j.encode('utf-8')
109
+ return b
110
+
111
+ def add_media(self, obj: OciMediaDataclass) -> OciMediaDescriptor:
112
+ b = self.marshal_media(obj)
113
+
114
+ r = BytesOciDataRef(b)
115
+ ri = OciDataRefInfo(r)
116
+ self.add_blob(
117
+ r,
118
+ ri,
119
+ media_type=obj.media_type,
120
+ )
121
+
122
+ return OciMediaDescriptor(
123
+ media_type=obj.media_type,
124
+ digest=ri.digest(),
125
+ size=ri.size(),
126
+ )
127
+
128
+ #
129
+
130
+ def to_media(self, obj: OciDataclass) -> ta.Union[OciMediaDataclass, OciMediaDescriptor]:
131
+ def make_kw(*exclude):
132
+ return {
133
+ a: v
134
+ for f in dc.fields(obj)
135
+ if (a := f.name) not in exclude
136
+ for v in [getattr(obj, a)]
137
+ if v is not None
138
+ }
139
+
140
+ if isinstance(obj, OciImageIndex):
141
+ return OciMediaImageIndex(
142
+ **make_kw('manifests'),
143
+ manifests=[
144
+ self.add_data(m)
145
+ for m in obj.manifests
146
+ ],
147
+ )
148
+
149
+ elif isinstance(obj, OciImageManifest):
150
+ return OciMediaImageManifest(
151
+ **make_kw('config', 'layers'),
152
+ config=self.add_data(obj.config),
153
+ layers=[
154
+ self.add_data(l)
155
+ for l in obj.layers
156
+ ],
157
+ )
158
+
159
+ elif isinstance(obj, OciImageLayer):
160
+ ri = OciDataRefInfo(obj.data)
161
+ mt = OCI_IMAGE_LAYER_KIND_MEDIA_TYPES[obj.kind]
162
+ self.add_blob(
163
+ obj.data,
164
+ ri,
165
+ media_type=mt,
166
+ )
167
+ return OciMediaDescriptor(
168
+ media_type=mt,
169
+ digest=ri.digest(),
170
+ size=ri.size(),
171
+ )
172
+
173
+ elif isinstance(obj, OciImageConfig):
174
+ return OciMediaImageConfig(**make_kw())
175
+
176
+ else:
177
+ raise TypeError(obj)
178
+
179
+ def add_data(self, obj: OciDataclass) -> OciMediaDescriptor:
180
+ ret = self.to_media(obj)
181
+
182
+ if isinstance(ret, OciMediaDataclass):
183
+ return self.add_media(ret)
184
+
185
+ elif isinstance(ret, OciMediaDescriptor):
186
+ return ret
187
+
188
+ else:
189
+ raise TypeError(ret)
190
+
191
+
192
+ ##
193
+
194
+
195
+ @dc.dataclass(frozen=True)
196
+ class BuiltOciImageIndexRepository:
197
+ index: OciImageIndex
198
+
199
+ media_index_descriptor: OciMediaDescriptor
200
+ media_index: OciMediaImageIndex
201
+
202
+ blobs: ta.Mapping[str, OciRepositoryBuilder.Blob]
203
+
204
+
205
+ def build_oci_index_repository(index: OciImageIndex) -> BuiltOciImageIndexRepository:
206
+ builder = OciRepositoryBuilder()
207
+
208
+ media_index_descriptor = builder.add_data(index)
209
+
210
+ blobs = builder.get_blobs()
211
+
212
+ media_index = blobs[media_index_descriptor.digest].read_media(OciMediaImageIndex)
213
+
214
+ return BuiltOciImageIndexRepository(
215
+ index=index,
216
+
217
+ media_index_descriptor=media_index_descriptor,
218
+ media_index=media_index,
219
+
220
+ blobs=blobs,
221
+ )
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import enum
4
+
5
+
6
+ class OciCompression(enum.Enum):
7
+ GZIP = enum.auto()
8
+ ZSTD = enum.auto()
omdev/oci/data.py ADDED
@@ -0,0 +1,151 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import enum
6
+ import typing as ta
7
+
8
+ from omlish.lite.marshal import OBJ_MARSHALER_FIELD_KEY
9
+ from omlish.lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
10
+
11
+ from .compression import OciCompression
12
+ from .datarefs import OciDataRef
13
+
14
+
15
+ ##
16
+
17
+
18
+ @dc.dataclass()
19
+ class OciDataclass(abc.ABC): # noqa
20
+ pass
21
+
22
+
23
+ ##
24
+
25
+
26
+ @dc.dataclass()
27
+ class OciImageIndex(OciDataclass):
28
+ manifests: ta.List[ta.Union['OciImageIndex', 'OciImageManifest']]
29
+
30
+ annotations: ta.Optional[ta.Dict[str, str]] = None
31
+
32
+
33
+ #
34
+
35
+
36
+ @dc.dataclass()
37
+ class OciImageManifest(OciDataclass):
38
+ config: 'OciImageConfig'
39
+
40
+ layers: ta.List['OciImageLayer']
41
+
42
+ annotations: ta.Optional[ta.Dict[str, str]] = None
43
+
44
+
45
+ #
46
+
47
+
48
+ @dc.dataclass()
49
+ class OciImageLayer(OciDataclass):
50
+ class Kind(enum.Enum):
51
+ TAR = enum.auto()
52
+ TAR_GZIP = enum.auto()
53
+ TAR_ZSTD = enum.auto()
54
+
55
+ @property
56
+ def compression(self) -> ta.Optional[OciCompression]:
57
+ if self is self.TAR:
58
+ return None
59
+ elif self is self.TAR_GZIP:
60
+ return OciCompression.GZIP
61
+ elif self is self.TAR_ZSTD:
62
+ return OciCompression.ZSTD
63
+ else:
64
+ raise ValueError(self)
65
+
66
+ @classmethod
67
+ def from_compression(cls, compression: ta.Optional[OciCompression]) -> 'OciImageLayer.Kind':
68
+ if compression is None:
69
+ return cls.TAR
70
+ elif compression == OciCompression.GZIP:
71
+ return cls.TAR_GZIP
72
+ elif compression == OciCompression.ZSTD:
73
+ return cls.TAR_ZSTD
74
+ else:
75
+ raise ValueError(compression)
76
+
77
+ kind: Kind
78
+
79
+ data: OciDataRef
80
+
81
+
82
+ #
83
+
84
+
85
+ @dc.dataclass()
86
+ class OciImageConfig(OciDataclass):
87
+ """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/config.md"""
88
+
89
+ architecture: str
90
+ os: str
91
+
92
+ @dc.dataclass()
93
+ class RootFs:
94
+ type: str
95
+ diff_ids: ta.List[str]
96
+
97
+ rootfs: RootFs
98
+
99
+ #
100
+
101
+ created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
102
+ author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
103
+ os_version: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.version', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
104
+ os_features: ta.Optional[ta.List[str]] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.features', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
105
+ variant: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
106
+
107
+ """
108
+ config object, OPTIONAL
109
+ User string, OPTIONAL
110
+ ExposedPorts object, OPTIONAL
111
+ Env array of strings, OPTIONAL
112
+ Entrypoint array of strings, OPTIONAL
113
+ Cmd array of strings, OPTIONAL
114
+ Volumes object, OPTIONAL
115
+ WorkingDir string, OPTIONAL
116
+ Labels object, OPTIONAL
117
+ StopSignal string, OPTIONAL
118
+ ArgsEscaped boolean, OPTIONAL
119
+ Memory integer, OPTIONAL
120
+ MemorySwap integer, OPTIONAL
121
+ CpuShares integer, OPTIONAL
122
+ Healthcheck object, OPTIONAL
123
+ """
124
+ config: ta.Optional[ta.Dict[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
125
+
126
+ @dc.dataclass()
127
+ class History:
128
+ created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
129
+ author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
130
+ created_by: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
131
+ comment: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
132
+ empty_layer: ta.Optional[bool] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
133
+
134
+ history: ta.Optional[ta.List[History]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
135
+
136
+
137
+ ##
138
+
139
+
140
+ def is_empty_oci_dataclass(obj: OciDataclass) -> bool:
141
+ if not isinstance(obj, OciDataclass):
142
+ raise TypeError(obj)
143
+
144
+ elif isinstance(obj, OciImageIndex):
145
+ return not obj.manifests
146
+
147
+ elif isinstance(obj, OciImageManifest):
148
+ return not obj.layers
149
+
150
+ else:
151
+ return False
omdev/oci/datarefs.py ADDED
@@ -0,0 +1,138 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import functools
6
+ import hashlib
7
+ import io
8
+ import os.path
9
+ import shutil
10
+ import tarfile
11
+ import typing as ta
12
+
13
+ from omlish.lite.cached import cached_nullary
14
+ from omlish.lite.check import check
15
+
16
+
17
+ ##
18
+
19
+
20
+ @dc.dataclass(frozen=True)
21
+ class OciDataRef(abc.ABC): # noqa
22
+ pass
23
+
24
+
25
+ @dc.dataclass(frozen=True)
26
+ class BytesOciDataRef(OciDataRef):
27
+ data: bytes
28
+
29
+
30
+ @dc.dataclass(frozen=True)
31
+ class FileOciDataRef(OciDataRef):
32
+ path: str
33
+
34
+
35
+ @dc.dataclass(frozen=True)
36
+ class TarFileOciDataRef(OciDataRef):
37
+ tar_file: tarfile.TarFile
38
+ tar_info: tarfile.TarInfo
39
+
40
+
41
+ ##
42
+
43
+
44
+ @functools.singledispatch
45
+ def write_oci_data_ref_to_file(
46
+ src_data: OciDataRef,
47
+ dst_file: str,
48
+ *,
49
+ symlink: bool = False, # noqa
50
+ chunk_size: int = 1024 * 1024,
51
+ ) -> None:
52
+ with open_oci_data_ref(src_data) as f_src:
53
+ with open(dst_file, 'wb') as f_dst:
54
+ shutil.copyfileobj(f_src, f_dst, length=chunk_size) # noqa
55
+
56
+
57
+ @write_oci_data_ref_to_file.register
58
+ def _(
59
+ src_data: FileOciDataRef,
60
+ dst_file: str,
61
+ *,
62
+ symlink: bool = False,
63
+ **kwargs: ta.Any,
64
+ ) -> None:
65
+ if symlink:
66
+ os.symlink(
67
+ os.path.relpath(src_data.path, os.path.dirname(dst_file)),
68
+ dst_file,
69
+ )
70
+ else:
71
+ shutil.copyfile(src_data.path, dst_file)
72
+
73
+
74
+ #
75
+
76
+
77
+ @functools.singledispatch
78
+ def open_oci_data_ref(data: OciDataRef) -> ta.BinaryIO:
79
+ raise TypeError(data)
80
+
81
+
82
+ @open_oci_data_ref.register
83
+ def _(data: FileOciDataRef) -> ta.BinaryIO:
84
+ return open(data.path, 'rb')
85
+
86
+
87
+ @open_oci_data_ref.register
88
+ def _(data: BytesOciDataRef) -> ta.BinaryIO:
89
+ return io.BytesIO(data.data)
90
+
91
+
92
+ @open_oci_data_ref.register
93
+ def _(data: TarFileOciDataRef) -> ta.BinaryIO:
94
+ return check.not_none(data.tar_file.extractfile(data.tar_info)) # type: ignore[return-value]
95
+
96
+
97
+ #
98
+
99
+
100
+ @functools.singledispatch
101
+ def get_oci_data_ref_size(data: OciDataRef) -> int:
102
+ raise TypeError(data)
103
+
104
+
105
+ @get_oci_data_ref_size.register
106
+ def _(data: FileOciDataRef) -> int:
107
+ return os.path.getsize(data.path)
108
+
109
+
110
+ @get_oci_data_ref_size.register
111
+ def _(data: BytesOciDataRef) -> int:
112
+ return len(data.data)
113
+
114
+
115
+ @get_oci_data_ref_size.register
116
+ def _(data: TarFileOciDataRef) -> int:
117
+ return data.tar_info.size
118
+
119
+
120
+ ##
121
+
122
+
123
+ @dc.dataclass(frozen=True)
124
+ class OciDataRefInfo:
125
+ data: OciDataRef
126
+
127
+ @cached_nullary
128
+ def sha256(self) -> str:
129
+ with open_oci_data_ref(self.data) as f:
130
+ return hashlib.file_digest(f, 'sha256').hexdigest() # type: ignore[arg-type]
131
+
132
+ @cached_nullary
133
+ def digest(self) -> str:
134
+ return f'sha256:{self.sha256()}'
135
+
136
+ @cached_nullary
137
+ def size(self) -> int:
138
+ return get_oci_data_ref_size(self.data)
@@ -0,0 +1,61 @@
1
+ # ruff: noqa: PT009 UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.check import check
5
+
6
+ from ..dataserver.routes import DataServerRoute
7
+ from ..dataserver.targets import DataServerTarget
8
+ from .building import BuiltOciImageIndexRepository
9
+ from .building import OciRepositoryBuilder
10
+ from .datarefs import BytesOciDataRef
11
+ from .datarefs import FileOciDataRef
12
+ from .datarefs import open_oci_data_ref
13
+ from .media import OCI_MANIFEST_MEDIA_TYPES
14
+
15
+
16
+ def build_oci_repository_data_server_routes(
17
+ repo_name: str,
18
+ built_repo: BuiltOciImageIndexRepository,
19
+ ) -> ta.List[DataServerRoute]:
20
+ base_url_path = f'/v2/{repo_name}'
21
+
22
+ repo_contents: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
23
+
24
+ repo_contents[f'{base_url_path}/manifests/latest'] = built_repo.blobs[built_repo.media_index_descriptor.digest]
25
+
26
+ for blob in built_repo.blobs.values():
27
+ repo_contents['/'.join([
28
+ base_url_path,
29
+ 'manifests' if blob.media_type in OCI_MANIFEST_MEDIA_TYPES else 'blobs',
30
+ blob.digest,
31
+ ])] = blob
32
+
33
+ #
34
+
35
+ def build_blob_target(blob: OciRepositoryBuilder.Blob) -> ta.Optional[DataServerTarget]: # noqa
36
+ kw: dict = dict(
37
+ content_type=check.non_empty_str(blob.media_type),
38
+ )
39
+
40
+ if isinstance(blob.data, BytesOciDataRef):
41
+ return DataServerTarget.of(blob.data.data, **kw)
42
+
43
+ elif isinstance(blob.data, FileOciDataRef):
44
+ return DataServerTarget.of(file_path=blob.data.path, **kw)
45
+
46
+ else:
47
+ with open_oci_data_ref(blob.data) as f:
48
+ data = f.read()
49
+
50
+ return DataServerTarget.of(data, **kw)
51
+
52
+ #
53
+
54
+ return [
55
+ DataServerRoute(
56
+ paths=[path],
57
+ target=target,
58
+ )
59
+ for path, blob in repo_contents.items()
60
+ if (target := build_blob_target(blob)) is not None
61
+ ]