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.
- omdev/ci/cache.py +40 -23
- omdev/ci/ci.py +49 -109
- omdev/ci/cli.py +24 -23
- omdev/ci/docker/__init__.py +0 -0
- omdev/ci/docker/buildcaching.py +69 -0
- omdev/ci/docker/cache.py +57 -0
- omdev/ci/{docker.py → docker/cmds.py} +1 -44
- omdev/ci/docker/imagepulling.py +64 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +15 -5
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- omdev/dataserver/__init__.py +1 -0
- omdev/dataserver/handlers.py +198 -0
- omdev/dataserver/http.py +69 -0
- omdev/dataserver/routes.py +49 -0
- omdev/dataserver/server.py +90 -0
- omdev/dataserver/targets.py +89 -0
- omdev/oci/__init__.py +0 -0
- omdev/oci/building.py +221 -0
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +151 -0
- omdev/oci/datarefs.py +138 -0
- omdev/oci/dataserver.py +61 -0
- omdev/oci/loading.py +142 -0
- omdev/oci/media.py +179 -0
- omdev/oci/packing.py +381 -0
- omdev/oci/repositories.py +159 -0
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +1841 -384
- omdev/scripts/interp.py +100 -22
- omdev/scripts/pyproject.py +122 -28
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/RECORD +40 -15
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|
omdev/oci/compression.py
ADDED
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)
|
omdev/oci/dataserver.py
ADDED
@@ -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
|
+
]
|