omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__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 +148 -23
- omdev/ci/ci.py +50 -110
- 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/cacheserved.py +262 -0
- omdev/ci/{docker.py → docker/cmds.py} +1 -44
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +65 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +35 -6
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- omdev/ci/utils.py +0 -49
- 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 +121 -0
- omdev/oci/building.py +107 -9
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +43 -0
- omdev/oci/datarefs.py +90 -50
- omdev/oci/dataserver.py +64 -0
- omdev/oci/loading.py +20 -0
- omdev/oci/media.py +20 -0
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/pack/unpacking.py +204 -0
- omdev/oci/repositories.py +84 -2
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +2137 -512
- omdev/scripts/interp.py +119 -22
- omdev/scripts/pyproject.py +141 -28
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- generate to nginx config
|
5
|
+
"""
|
6
|
+
import dataclasses as dc
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from omlish.lite.check import check
|
10
|
+
|
11
|
+
from .targets import DataServerTarget
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
@dc.dataclass(frozen=True)
|
18
|
+
class DataServerRoute:
|
19
|
+
paths: ta.Sequence[str]
|
20
|
+
target: DataServerTarget
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def of(cls, obj: ta.Union[
|
24
|
+
'DataServerRoute',
|
25
|
+
ta.Tuple[
|
26
|
+
ta.Union[str, ta.Iterable[str]],
|
27
|
+
DataServerTarget,
|
28
|
+
],
|
29
|
+
]) -> 'DataServerRoute':
|
30
|
+
if isinstance(obj, cls):
|
31
|
+
return obj
|
32
|
+
|
33
|
+
elif isinstance(obj, tuple):
|
34
|
+
p, t = obj
|
35
|
+
|
36
|
+
if isinstance(p, str):
|
37
|
+
p = [p]
|
38
|
+
|
39
|
+
return cls(
|
40
|
+
paths=tuple(p),
|
41
|
+
target=check.isinstance(t, DataServerTarget),
|
42
|
+
)
|
43
|
+
|
44
|
+
else:
|
45
|
+
raise TypeError(obj)
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def of_(cls, *objs: ta.Any) -> ta.List['DataServerRoute']:
|
49
|
+
return [cls.of(obj) for obj in objs]
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
3
|
+
import http.client
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from omlish.lite.check import check
|
7
|
+
|
8
|
+
from .handlers import DataServerHandler
|
9
|
+
from .handlers import DataServerRequest
|
10
|
+
from .handlers import DataServerResponse
|
11
|
+
from .handlers import DataServerTargetHandler
|
12
|
+
from .routes import DataServerRoute
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
class DataServer:
|
19
|
+
@dc.dataclass(frozen=True)
|
20
|
+
class HandlerRoute:
|
21
|
+
paths: ta.Sequence[str]
|
22
|
+
handler: DataServerHandler
|
23
|
+
|
24
|
+
def __post_init__(self) -> None:
|
25
|
+
check.not_isinstance(self.paths, str)
|
26
|
+
for p in self.paths:
|
27
|
+
check.non_empty_str(p)
|
28
|
+
check.isinstance(self.handler, DataServerHandler)
|
29
|
+
|
30
|
+
@classmethod
|
31
|
+
def of(cls, obj: ta.Union[
|
32
|
+
'DataServer.HandlerRoute',
|
33
|
+
DataServerRoute,
|
34
|
+
]) -> 'DataServer.HandlerRoute':
|
35
|
+
if isinstance(obj, cls):
|
36
|
+
return obj
|
37
|
+
|
38
|
+
elif isinstance(obj, DataServerRoute):
|
39
|
+
return cls(
|
40
|
+
paths=obj.paths,
|
41
|
+
handler=DataServerTargetHandler.for_target(obj.target),
|
42
|
+
)
|
43
|
+
|
44
|
+
else:
|
45
|
+
raise TypeError(obj)
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def of_(cls, *objs: ta.Any) -> ta.List['DataServer.HandlerRoute']:
|
49
|
+
return [cls.of(obj) for obj in objs]
|
50
|
+
|
51
|
+
#
|
52
|
+
|
53
|
+
@dc.dataclass(frozen=True)
|
54
|
+
class Config:
|
55
|
+
pass
|
56
|
+
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
routes: ta.Optional[ta.Iterable[HandlerRoute]] = None,
|
60
|
+
config: Config = Config(),
|
61
|
+
) -> None:
|
62
|
+
super().__init__()
|
63
|
+
|
64
|
+
self._config = config
|
65
|
+
|
66
|
+
self.set_routes(routes)
|
67
|
+
|
68
|
+
#
|
69
|
+
|
70
|
+
_routes_by_path: ta.Dict[str, HandlerRoute]
|
71
|
+
|
72
|
+
def set_routes(self, routes: ta.Optional[ta.Iterable[HandlerRoute]]) -> None:
|
73
|
+
routes_by_path: ta.Dict[str, DataServer.HandlerRoute] = {}
|
74
|
+
|
75
|
+
for r in routes or []:
|
76
|
+
for p in r.paths:
|
77
|
+
check.not_in(p, routes_by_path)
|
78
|
+
routes_by_path[p] = r
|
79
|
+
|
80
|
+
self._routes_by_path = routes_by_path
|
81
|
+
|
82
|
+
#
|
83
|
+
|
84
|
+
def handle(self, req: DataServerRequest) -> DataServerResponse:
|
85
|
+
try:
|
86
|
+
rt = self._routes_by_path[req.path]
|
87
|
+
except KeyError:
|
88
|
+
return DataServerResponse(http.HTTPStatus.NOT_FOUND)
|
89
|
+
|
90
|
+
return rt.handler.handle(req)
|
@@ -0,0 +1,121 @@
|
|
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
|
+
#
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def of(
|
23
|
+
cls,
|
24
|
+
obj: ta.Union[
|
25
|
+
'DataServerTarget',
|
26
|
+
bytes,
|
27
|
+
None,
|
28
|
+
] = None,
|
29
|
+
*,
|
30
|
+
|
31
|
+
file_path: ta.Optional[str] = None,
|
32
|
+
url: ta.Optional[str] = None,
|
33
|
+
|
34
|
+
**kwargs: ta.Any,
|
35
|
+
) -> 'DataServerTarget':
|
36
|
+
if isinstance(obj, DataServerTarget):
|
37
|
+
check.none(file_path)
|
38
|
+
check.none(url)
|
39
|
+
check.empty(kwargs)
|
40
|
+
return obj
|
41
|
+
|
42
|
+
elif isinstance(obj, bytes):
|
43
|
+
return BytesDataServerTarget(
|
44
|
+
data=obj,
|
45
|
+
**kwargs,
|
46
|
+
)
|
47
|
+
|
48
|
+
elif file_path is not None:
|
49
|
+
check.none(obj)
|
50
|
+
check.none(url)
|
51
|
+
return FileDataServerTarget(
|
52
|
+
file_path=file_path,
|
53
|
+
**kwargs,
|
54
|
+
)
|
55
|
+
|
56
|
+
elif url is not None:
|
57
|
+
check.none(obj)
|
58
|
+
check.none(file_path)
|
59
|
+
return UrlDataServerTarget(
|
60
|
+
url=url,
|
61
|
+
**kwargs,
|
62
|
+
)
|
63
|
+
|
64
|
+
else:
|
65
|
+
raise TypeError('No target type provided')
|
66
|
+
|
67
|
+
#
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def of_bytes(cls, data: bytes) -> 'BytesDataServerTarget':
|
71
|
+
return BytesDataServerTarget(
|
72
|
+
data=data,
|
73
|
+
content_type='application/octet-stream',
|
74
|
+
)
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def of_text(cls, data: str) -> 'BytesDataServerTarget':
|
78
|
+
return BytesDataServerTarget(
|
79
|
+
data=data.encode('utf-8'),
|
80
|
+
content_type='text/plain; charset=utf-8',
|
81
|
+
)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def of_json(cls, data: str) -> 'BytesDataServerTarget':
|
85
|
+
return BytesDataServerTarget(
|
86
|
+
data=data.encode('utf-8'),
|
87
|
+
content_type='application/json; charset=utf-8',
|
88
|
+
)
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def of_html(cls, data: str) -> 'BytesDataServerTarget':
|
92
|
+
return BytesDataServerTarget(
|
93
|
+
data=data.encode('utf-8'),
|
94
|
+
content_type='text/html; charset=utf-8',
|
95
|
+
)
|
96
|
+
|
97
|
+
|
98
|
+
@dc.dataclass(frozen=True)
|
99
|
+
class BytesDataServerTarget(DataServerTarget):
|
100
|
+
data: ta.Optional[bytes] = None # required
|
101
|
+
|
102
|
+
|
103
|
+
@dc.dataclass(frozen=True)
|
104
|
+
class FileDataServerTarget(DataServerTarget):
|
105
|
+
file_path: ta.Optional[str] = None # required
|
106
|
+
|
107
|
+
def __post_init__(self) -> None:
|
108
|
+
dataclass_maybe_post_init(super())
|
109
|
+
check.non_empty_str(self.file_path)
|
110
|
+
|
111
|
+
|
112
|
+
@dc.dataclass(frozen=True)
|
113
|
+
class UrlDataServerTarget(DataServerTarget):
|
114
|
+
url: ta.Optional[str] = None # required
|
115
|
+
methods: ta.Optional[ta.Sequence[str]] = None # required
|
116
|
+
|
117
|
+
def __post_init__(self) -> None:
|
118
|
+
dataclass_maybe_post_init(super())
|
119
|
+
check.non_empty_str(self.url)
|
120
|
+
check.not_none(self.methods)
|
121
|
+
check.not_isinstance(self.methods, str)
|
omdev/oci/building.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
# @omlish-lite
|
3
3
|
import dataclasses as dc
|
4
|
+
import json
|
4
5
|
import typing as ta
|
5
6
|
|
6
7
|
from omlish.lite.check import check
|
@@ -15,36 +16,90 @@ from .data import OciImageManifest
|
|
15
16
|
from .datarefs import BytesOciDataRef
|
16
17
|
from .datarefs import OciDataRef
|
17
18
|
from .datarefs import OciDataRefInfo
|
19
|
+
from .datarefs import open_oci_data_ref
|
18
20
|
from .media import OCI_IMAGE_LAYER_KIND_MEDIA_TYPES
|
19
21
|
from .media import OciMediaDataclass
|
20
22
|
from .media import OciMediaDescriptor
|
21
23
|
from .media import OciMediaImageConfig
|
22
24
|
from .media import OciMediaImageIndex
|
23
25
|
from .media import OciMediaImageManifest
|
26
|
+
from .media import unmarshal_oci_media_dataclass
|
27
|
+
|
28
|
+
|
29
|
+
OciMediaDataclassT = ta.TypeVar('OciMediaDataclassT', bound='OciMediaDataclass')
|
24
30
|
|
25
31
|
|
26
32
|
##
|
27
33
|
|
28
34
|
|
29
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
|
+
|
30
66
|
def __init__(self) -> None:
|
31
67
|
super().__init__()
|
32
68
|
|
33
|
-
self._blobs: ta.Dict[str,
|
69
|
+
self._blobs: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
|
34
70
|
|
35
|
-
|
71
|
+
#
|
72
|
+
|
73
|
+
def get_blobs(self) -> ta.Dict[str, Blob]:
|
36
74
|
return dict(self._blobs)
|
37
75
|
|
38
76
|
def add_blob(
|
39
77
|
self,
|
40
78
|
r: OciDataRef,
|
41
79
|
ri: ta.Optional[OciDataRefInfo] = None,
|
42
|
-
|
80
|
+
*,
|
81
|
+
media_type: ta.Optional[str] = None,
|
82
|
+
) -> Blob:
|
43
83
|
if ri is None:
|
44
84
|
ri = OciDataRefInfo(r)
|
45
|
-
|
85
|
+
|
86
|
+
if (dg := ri.digest()) in self._blobs:
|
46
87
|
raise KeyError(ri.digest())
|
47
|
-
|
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
|
+
#
|
48
103
|
|
49
104
|
def marshal_media(self, obj: OciMediaDataclass) -> bytes:
|
50
105
|
check.isinstance(obj, OciMediaDataclass)
|
@@ -58,14 +113,20 @@ class OciRepositoryBuilder:
|
|
58
113
|
|
59
114
|
r = BytesOciDataRef(b)
|
60
115
|
ri = OciDataRefInfo(r)
|
61
|
-
self.add_blob(
|
116
|
+
self.add_blob(
|
117
|
+
r,
|
118
|
+
ri,
|
119
|
+
media_type=obj.media_type,
|
120
|
+
)
|
62
121
|
|
63
122
|
return OciMediaDescriptor(
|
64
|
-
media_type=
|
123
|
+
media_type=obj.media_type,
|
65
124
|
digest=ri.digest(),
|
66
125
|
size=ri.size(),
|
67
126
|
)
|
68
127
|
|
128
|
+
#
|
129
|
+
|
69
130
|
def to_media(self, obj: OciDataclass) -> ta.Union[OciMediaDataclass, OciMediaDescriptor]:
|
70
131
|
def make_kw(*exclude):
|
71
132
|
return {
|
@@ -97,9 +158,14 @@ class OciRepositoryBuilder:
|
|
97
158
|
|
98
159
|
elif isinstance(obj, OciImageLayer):
|
99
160
|
ri = OciDataRefInfo(obj.data)
|
100
|
-
|
161
|
+
mt = OCI_IMAGE_LAYER_KIND_MEDIA_TYPES[obj.kind]
|
162
|
+
self.add_blob(
|
163
|
+
obj.data,
|
164
|
+
ri,
|
165
|
+
media_type=mt,
|
166
|
+
)
|
101
167
|
return OciMediaDescriptor(
|
102
|
-
media_type=
|
168
|
+
media_type=mt,
|
103
169
|
digest=ri.digest(),
|
104
170
|
size=ri.size(),
|
105
171
|
)
|
@@ -121,3 +187,35 @@ class OciRepositoryBuilder:
|
|
121
187
|
|
122
188
|
else:
|
123
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
CHANGED
@@ -5,9 +5,11 @@ import dataclasses as dc
|
|
5
5
|
import enum
|
6
6
|
import typing as ta
|
7
7
|
|
8
|
+
from omlish.lite.check import check
|
8
9
|
from omlish.lite.marshal import OBJ_MARSHALER_FIELD_KEY
|
9
10
|
from omlish.lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
|
10
11
|
|
12
|
+
from .compression import OciCompression
|
11
13
|
from .datarefs import OciDataRef
|
12
14
|
|
13
15
|
|
@@ -40,6 +42,7 @@ class OciImageManifest(OciDataclass):
|
|
40
42
|
|
41
43
|
annotations: ta.Optional[ta.Dict[str, str]] = None
|
42
44
|
|
45
|
+
|
43
46
|
#
|
44
47
|
|
45
48
|
|
@@ -50,6 +53,28 @@ class OciImageLayer(OciDataclass):
|
|
50
53
|
TAR_GZIP = enum.auto()
|
51
54
|
TAR_ZSTD = enum.auto()
|
52
55
|
|
56
|
+
@property
|
57
|
+
def compression(self) -> ta.Optional[OciCompression]:
|
58
|
+
if self is self.TAR:
|
59
|
+
return None
|
60
|
+
elif self is self.TAR_GZIP:
|
61
|
+
return OciCompression.GZIP
|
62
|
+
elif self is self.TAR_ZSTD:
|
63
|
+
return OciCompression.ZSTD
|
64
|
+
else:
|
65
|
+
raise ValueError(self)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def from_compression(cls, compression: ta.Optional[OciCompression]) -> 'OciImageLayer.Kind':
|
69
|
+
if compression is None:
|
70
|
+
return cls.TAR
|
71
|
+
elif compression == OciCompression.GZIP:
|
72
|
+
return cls.TAR_GZIP
|
73
|
+
elif compression == OciCompression.ZSTD:
|
74
|
+
return cls.TAR_ZSTD
|
75
|
+
else:
|
76
|
+
raise ValueError(compression)
|
77
|
+
|
53
78
|
kind: Kind
|
54
79
|
|
55
80
|
data: OciDataRef
|
@@ -125,3 +150,21 @@ def is_empty_oci_dataclass(obj: OciDataclass) -> bool:
|
|
125
150
|
|
126
151
|
else:
|
127
152
|
return False
|
153
|
+
|
154
|
+
|
155
|
+
##
|
156
|
+
|
157
|
+
|
158
|
+
def get_single_leaf_oci_image_index(image_index: OciImageIndex) -> OciImageIndex:
|
159
|
+
while True:
|
160
|
+
child_manifest = check.single(image_index.manifests)
|
161
|
+
if isinstance(child_manifest, OciImageManifest):
|
162
|
+
break
|
163
|
+
image_index = check.isinstance(child_manifest, OciImageIndex)
|
164
|
+
|
165
|
+
return image_index
|
166
|
+
|
167
|
+
|
168
|
+
def get_single_oci_image_manifest(image_index: OciImageIndex) -> OciImageManifest:
|
169
|
+
child_index = check.single(image_index.manifests)
|
170
|
+
return check.isinstance(child_index, OciImageManifest)
|
omdev/oci/datarefs.py
CHANGED
@@ -2,13 +2,16 @@
|
|
2
2
|
# @omlish-lite
|
3
3
|
import abc
|
4
4
|
import dataclasses as dc
|
5
|
+
import functools
|
5
6
|
import hashlib
|
6
7
|
import io
|
7
8
|
import os.path
|
8
9
|
import shutil
|
10
|
+
import tarfile
|
9
11
|
import typing as ta
|
10
12
|
|
11
13
|
from omlish.lite.cached import cached_nullary
|
14
|
+
from omlish.lite.check import check
|
12
15
|
|
13
16
|
|
14
17
|
##
|
@@ -29,70 +32,107 @@ class FileOciDataRef(OciDataRef):
|
|
29
32
|
path: str
|
30
33
|
|
31
34
|
|
35
|
+
@dc.dataclass(frozen=True)
|
36
|
+
class TarFileOciDataRef(OciDataRef):
|
37
|
+
tar_file: tarfile.TarFile
|
38
|
+
tar_info: tarfile.TarInfo
|
39
|
+
|
40
|
+
|
32
41
|
##
|
33
42
|
|
34
43
|
|
35
|
-
@
|
36
|
-
|
37
|
-
|
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
|
38
55
|
|
39
|
-
@cached_nullary
|
40
|
-
def sha256(self) -> str:
|
41
|
-
if isinstance(self.data, FileOciDataRef):
|
42
|
-
with open(self.data.path, 'rb') as f:
|
43
|
-
return hashlib.file_digest(f, 'sha256').hexdigest() # noqa
|
44
56
|
|
45
|
-
|
46
|
-
|
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)
|
47
72
|
|
48
|
-
else:
|
49
|
-
raise TypeError(self.data)
|
50
73
|
|
51
|
-
|
52
|
-
def digest(self) -> str:
|
53
|
-
return f'sha256:{self.sha256()}'
|
74
|
+
#
|
54
75
|
|
55
|
-
@cached_nullary
|
56
|
-
def size(self) -> int:
|
57
|
-
if isinstance(self.data, FileOciDataRef):
|
58
|
-
return os.path.getsize(self.data.path)
|
59
76
|
|
60
|
-
|
61
|
-
|
77
|
+
@functools.singledispatch
|
78
|
+
def open_oci_data_ref(data: OciDataRef) -> ta.BinaryIO:
|
79
|
+
raise TypeError(data)
|
62
80
|
|
63
|
-
else:
|
64
|
-
raise TypeError(self.data)
|
65
81
|
|
82
|
+
@open_oci_data_ref.register
|
83
|
+
def _(data: FileOciDataRef) -> ta.BinaryIO:
|
84
|
+
return open(data.path, 'rb')
|
66
85
|
|
67
|
-
def write_oci_data_ref_to_file(
|
68
|
-
data: OciDataRef,
|
69
|
-
dst: str,
|
70
|
-
*,
|
71
|
-
symlink: bool = False,
|
72
|
-
) -> None:
|
73
|
-
if isinstance(data, FileOciDataRef):
|
74
|
-
if symlink:
|
75
|
-
os.symlink(
|
76
|
-
os.path.relpath(data.path, os.path.dirname(dst)),
|
77
|
-
dst,
|
78
|
-
)
|
79
|
-
else:
|
80
|
-
shutil.copyfile(data.path, dst)
|
81
|
-
|
82
|
-
elif isinstance(data, BytesOciDataRef):
|
83
|
-
with open(dst, 'wb') as f:
|
84
|
-
f.write(data.data)
|
85
86
|
|
86
|
-
|
87
|
-
|
87
|
+
@open_oci_data_ref.register
|
88
|
+
def _(data: BytesOciDataRef) -> ta.BinaryIO:
|
89
|
+
return io.BytesIO(data.data)
|
88
90
|
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
-
|
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]
|
93
95
|
|
94
|
-
elif isinstance(data, BytesOciDataRef):
|
95
|
-
return io.BytesIO(data.data)
|
96
96
|
|
97
|
-
|
98
|
-
|
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)
|