omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. omdev/ci/cache.py +148 -23
  2. omdev/ci/ci.py +50 -110
  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/cacheserved.py +262 -0
  8. omdev/ci/{docker.py → docker/cmds.py} +1 -44
  9. omdev/ci/docker/dataserver.py +204 -0
  10. omdev/ci/docker/imagepulling.py +65 -0
  11. omdev/ci/docker/inject.py +37 -0
  12. omdev/ci/docker/packing.py +72 -0
  13. omdev/ci/docker/repositories.py +40 -0
  14. omdev/ci/docker/utils.py +48 -0
  15. omdev/ci/github/cache.py +35 -6
  16. omdev/ci/github/client.py +9 -2
  17. omdev/ci/github/inject.py +30 -0
  18. omdev/ci/inject.py +61 -0
  19. omdev/ci/utils.py +0 -49
  20. omdev/dataserver/__init__.py +1 -0
  21. omdev/dataserver/handlers.py +198 -0
  22. omdev/dataserver/http.py +69 -0
  23. omdev/dataserver/routes.py +49 -0
  24. omdev/dataserver/server.py +90 -0
  25. omdev/dataserver/targets.py +121 -0
  26. omdev/oci/building.py +107 -9
  27. omdev/oci/compression.py +8 -0
  28. omdev/oci/data.py +43 -0
  29. omdev/oci/datarefs.py +90 -50
  30. omdev/oci/dataserver.py +64 -0
  31. omdev/oci/loading.py +20 -0
  32. omdev/oci/media.py +20 -0
  33. omdev/oci/pack/__init__.py +0 -0
  34. omdev/oci/pack/packing.py +185 -0
  35. omdev/oci/pack/repositories.py +162 -0
  36. omdev/oci/pack/unpacking.py +204 -0
  37. omdev/oci/repositories.py +84 -2
  38. omdev/oci/tars.py +144 -0
  39. omdev/pyproject/resources/python.sh +1 -1
  40. omdev/scripts/ci.py +2137 -512
  41. omdev/scripts/interp.py +119 -22
  42. omdev/scripts/pyproject.py +141 -28
  43. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
  44. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
  45. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
  46. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
  47. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
  48. {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, OciDataRef] = {}
69
+ self._blobs: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
34
70
 
35
- def get_blobs(self) -> ta.Dict[str, OciDataRef]:
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
- ) -> None:
80
+ *,
81
+ media_type: ta.Optional[str] = None,
82
+ ) -> Blob:
43
83
  if ri is None:
44
84
  ri = OciDataRefInfo(r)
45
- if ri.digest() in self._blobs:
85
+
86
+ if (dg := ri.digest()) in self._blobs:
46
87
  raise KeyError(ri.digest())
47
- self._blobs[ri.digest()] = r
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(r, ri)
116
+ self.add_blob(
117
+ r,
118
+ ri,
119
+ media_type=obj.media_type,
120
+ )
62
121
 
63
122
  return OciMediaDescriptor(
64
- media_type=getattr(obj, '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
- self.add_blob(obj.data, ri)
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=OCI_IMAGE_LAYER_KIND_MEDIA_TYPES[obj.kind],
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
+ )
@@ -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 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
- @dc.dataclass(frozen=True)
36
- class OciDataRefInfo:
37
- data: OciDataRef
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
- elif isinstance(self.data, BytesOciDataRef):
46
- return hashlib.sha256(self.data.data).hexdigest()
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
- @cached_nullary
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
- elif isinstance(self.data, BytesOciDataRef):
61
- return len(self.data.data)
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
- else:
87
- raise TypeError(data)
87
+ @open_oci_data_ref.register
88
+ def _(data: BytesOciDataRef) -> ta.BinaryIO:
89
+ return io.BytesIO(data.data)
88
90
 
89
91
 
90
- def open_oci_data_ref(data: OciDataRef) -> ta.BinaryIO:
91
- if isinstance(data, FileOciDataRef):
92
- return open(data.path, 'rb')
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
- else:
98
- raise TypeError(data)
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)