omlish 0.0.0.dev220__py3-none-any.whl → 0.0.0.dev221__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 (38) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/algorithm/__init__.py +0 -0
  3. omlish/algorithm/all.py +13 -0
  4. omlish/algorithm/distribute.py +46 -0
  5. omlish/algorithm/toposort.py +26 -0
  6. omlish/algorithm/unify.py +31 -0
  7. omlish/collections/__init__.py +0 -2
  8. omlish/collections/utils.py +0 -46
  9. omlish/docker/oci/building.py +122 -0
  10. omlish/docker/oci/data.py +62 -8
  11. omlish/docker/oci/datarefs.py +98 -0
  12. omlish/docker/oci/loading.py +120 -0
  13. omlish/docker/oci/media.py +44 -14
  14. omlish/docker/oci/repositories.py +72 -0
  15. omlish/graphs/trees.py +2 -1
  16. omlish/http/coro/server.py +42 -33
  17. omlish/http/{simple.py → coro/simple.py} +17 -17
  18. omlish/specs/irc/__init__.py +0 -0
  19. omlish/specs/irc/format/LICENSE +11 -0
  20. omlish/specs/irc/format/__init__.py +61 -0
  21. omlish/specs/irc/format/consts.py +6 -0
  22. omlish/specs/irc/format/errors.py +30 -0
  23. omlish/specs/irc/format/message.py +18 -0
  24. omlish/specs/irc/format/nuh.py +52 -0
  25. omlish/specs/irc/format/parsing.py +155 -0
  26. omlish/specs/irc/format/rendering.py +150 -0
  27. omlish/specs/irc/format/tags.py +99 -0
  28. omlish/specs/irc/format/utils.py +27 -0
  29. omlish/specs/irc/numerics/__init__.py +0 -0
  30. omlish/specs/irc/numerics/formats.py +94 -0
  31. omlish/specs/irc/numerics/numerics.py +808 -0
  32. omlish/specs/irc/numerics/types.py +59 -0
  33. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/METADATA +1 -1
  34. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/RECORD +38 -14
  35. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/LICENSE +0 -0
  36. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/WHEEL +0 -0
  37. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/entry_points.txt +0 -0
  38. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev220'
2
- __revision__ = '3af8855e48e09d340dd21835f45fe8a21a262fa9'
1
+ __version__ = '0.0.0.dev221'
2
+ __revision__ = '2c4c1480e414176972a21bd34529721b39818ad8'
3
3
 
4
4
 
5
5
  #
File without changes
@@ -0,0 +1,13 @@
1
+ from .distribute import ( # noqa
2
+ distribute_evenly,
3
+ )
4
+
5
+ from .toposort import ( # noqa
6
+ mut_toposort,
7
+ toposort,
8
+ )
9
+
10
+ from .unify import ( # noqa
11
+ mut_unify_sets,
12
+ unify_sets,
13
+ )
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import collections
4
+ import heapq
5
+ import typing as ta
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+
10
+
11
+ def distribute_evenly(
12
+ items: ta.Iterable[ta.Tuple[T, float]],
13
+ n_bins: int,
14
+ ) -> ta.List[ta.List[ta.Tuple[T, float]]]:
15
+ """
16
+ Distribute items into n bins as evenly as possible in terms of total size.
17
+ - Sorting ensures larger items are placed first, preventing large leftover gaps in bins.
18
+ - A min-heap efficiently finds the least loaded bin in O(log n), keeping the distribution balanced.
19
+ - Each item is placed in the lightest bin, preventing a few bins from getting overloaded early.
20
+
21
+ :param items: List of tuples (item, size).
22
+ :param n_bins: Number of bins.
23
+ :return: List of n_bins lists, each containing items assigned to that bin.
24
+ """
25
+
26
+ # Sort items by size in descending order
27
+ items_sorted = sorted(items, key=lambda x: x[1], reverse=True)
28
+
29
+ # Min-heap to track bin loads (size, index)
30
+ bins = [(0, i) for i in range(n_bins)] # (current size, bin index)
31
+ heapq.heapify(bins)
32
+
33
+ # Allocate items to bins
34
+ bin_contents = collections.defaultdict(list)
35
+
36
+ for item, size in items_sorted:
37
+ # Get the least loaded bin
38
+ bin_size, bin_index = heapq.heappop(bins)
39
+
40
+ # Assign item to this bin
41
+ bin_contents[bin_index].append((item, size))
42
+
43
+ # Update bin load and push back to heap
44
+ heapq.heappush(bins, (bin_size + size, bin_index)) # type: ignore
45
+
46
+ return [bin_contents[i] for i in range(n_bins)]
@@ -0,0 +1,26 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import functools
4
+ import typing as ta
5
+
6
+
7
+ T = ta.TypeVar('T')
8
+
9
+
10
+ def mut_toposort(data: ta.Dict[T, ta.Set[T]]) -> ta.Iterator[ta.Set[T]]:
11
+ for k, v in data.items():
12
+ v.discard(k)
13
+ extra_items_in_deps = functools.reduce(set.union, data.values()) - set(data.keys())
14
+ data.update({item: set() for item in extra_items_in_deps})
15
+ while True:
16
+ ordered = {item for item, dep in data.items() if not dep}
17
+ if not ordered:
18
+ break
19
+ yield ordered
20
+ data = {item: (dep - ordered) for item, dep in data.items() if item not in ordered}
21
+ if data:
22
+ raise ValueError('Cyclic dependencies exist among these items: ' + ' '.join(repr(x) for x in data.items()))
23
+
24
+
25
+ def toposort(data: ta.Mapping[T, ta.AbstractSet[T]]) -> ta.Iterator[ta.Set[T]]:
26
+ return mut_toposort({k: set(v) for k, v in data.items()})
@@ -0,0 +1,31 @@
1
+ import itertools
2
+ import typing as ta
3
+
4
+
5
+ T = ta.TypeVar('T')
6
+
7
+
8
+ def mut_unify_sets(sets: ta.Iterable[set[T]]) -> list[set[T]]:
9
+ rem: list[set[T]] = list(sets)
10
+ ret: list[set[T]] = []
11
+ while rem:
12
+ cur = rem.pop()
13
+ while True:
14
+ moved = False
15
+ for i in range(len(rem) - 1, -1, -1):
16
+ if any(e in cur for e in rem[i]):
17
+ cur.update(rem.pop(i))
18
+ moved = True
19
+ if not moved:
20
+ break
21
+ ret.append(cur)
22
+ if ret:
23
+ all_ = set(itertools.chain.from_iterable(ret))
24
+ num = sum(map(len, ret))
25
+ if len(all_) != num:
26
+ raise ValueError('Length mismatch')
27
+ return ret
28
+
29
+
30
+ def unify_sets(sets: ta.Iterable[ta.AbstractSet[T]]) -> list[set[T]]:
31
+ return mut_unify_sets([set(s) for s in sets])
@@ -126,8 +126,6 @@ from .utils import ( # noqa
126
126
  make_map_by,
127
127
  multi_map,
128
128
  multi_map_by,
129
- mut_toposort,
130
129
  partition,
131
- toposort,
132
130
  unique,
133
131
  )
@@ -1,8 +1,5 @@
1
- import functools
2
- import itertools
3
1
  import typing as ta
4
2
 
5
- from .. import check
6
3
  from .. import lang
7
4
  from .exceptions import DuplicateKeyError
8
5
  from .identity import IdentityKeyDict
@@ -17,28 +14,6 @@ V = ta.TypeVar('V')
17
14
  ##
18
15
 
19
16
 
20
- def mut_toposort(data: dict[T, set[T]]) -> ta.Iterator[set[T]]:
21
- for k, v in data.items():
22
- v.discard(k)
23
- extra_items_in_deps = functools.reduce(set.union, data.values()) - set(data.keys())
24
- data.update({item: set() for item in extra_items_in_deps})
25
- while True:
26
- ordered = {item for item, dep in data.items() if not dep}
27
- if not ordered:
28
- break
29
- yield ordered
30
- data = {item: (dep - ordered) for item, dep in data.items() if item not in ordered}
31
- if data:
32
- raise ValueError('Cyclic dependencies exist among these items: ' + ' '.join(repr(x) for x in data.items()))
33
-
34
-
35
- def toposort(data: ta.Mapping[T, ta.AbstractSet[T]]) -> ta.Iterator[set[T]]:
36
- return mut_toposort({k: set(v) for k, v in data.items()})
37
-
38
-
39
- ##
40
-
41
-
42
17
  class PartitionResult(ta.NamedTuple, ta.Generic[T]):
43
18
  t: list[T]
44
19
  f: list[T]
@@ -153,24 +128,3 @@ def key_cmp(fn: ta.Callable[[K, K], int]) -> ta.Callable[[tuple[K, V], tuple[K,
153
128
 
154
129
  def indexes(it: ta.Iterable[T]) -> dict[T, int]:
155
130
  return {e: i for i, e in enumerate(it)}
156
-
157
-
158
- def mut_unify_sets(sets: ta.Iterable[set[T]]) -> list[set[T]]:
159
- rem: list[set[T]] = list(sets)
160
- ret: list[set[T]] = []
161
- while rem:
162
- cur = rem.pop()
163
- while True:
164
- moved = False
165
- for i in range(len(rem) - 1, -1, -1):
166
- if any(e in cur for e in rem[i]):
167
- cur.update(rem.pop(i))
168
- moved = True
169
- if not moved:
170
- break
171
- ret.append(cur)
172
- if ret:
173
- all_ = set(itertools.chain.from_iterable(ret))
174
- num = sum(map(len, ret))
175
- check.equal(len(all_), num)
176
- return ret
@@ -0,0 +1,122 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+ from ...lite.check import check
7
+ from ...lite.json import json_dumps_compact
8
+ from ...lite.marshal import marshal_obj
9
+ from .data import OciDataclass
10
+ from .data import OciImageConfig
11
+ from .data import OciImageIndex
12
+ from .data import OciImageLayer
13
+ from .data import OciImageManifest
14
+ from .datarefs import BytesOciDataRef
15
+ from .datarefs import OciDataRef
16
+ from .datarefs import OciDataRefInfo
17
+ from .media import OCI_IMAGE_LAYER_KIND_MEDIA_TYPES
18
+ from .media import OciMediaDataclass
19
+ from .media import OciMediaDescriptor
20
+ from .media import OciMediaImageConfig
21
+ from .media import OciMediaImageIndex
22
+ from .media import OciMediaImageManifest
23
+
24
+
25
+ ##
26
+
27
+
28
+ class OciRepositoryBuilder:
29
+ def __init__(self) -> None:
30
+ super().__init__()
31
+
32
+ self._blobs: ta.Dict[str, OciDataRef] = {}
33
+
34
+ def get_blobs(self) -> ta.Dict[str, OciDataRef]:
35
+ return dict(self._blobs)
36
+
37
+ def add_blob(
38
+ self,
39
+ r: OciDataRef,
40
+ ri: ta.Optional[OciDataRefInfo] = None,
41
+ ) -> None:
42
+ if ri is None:
43
+ ri = OciDataRefInfo(r)
44
+ if ri.digest() in self._blobs:
45
+ raise KeyError(ri.digest())
46
+ self._blobs[ri.digest()] = r
47
+
48
+ def marshal_media(self, obj: OciMediaDataclass) -> bytes:
49
+ check.isinstance(obj, OciMediaDataclass)
50
+ m = marshal_obj(obj)
51
+ j = json_dumps_compact(m)
52
+ b = j.encode('utf-8')
53
+ return b
54
+
55
+ def add_media(self, obj: OciMediaDataclass) -> OciMediaDescriptor:
56
+ b = self.marshal_media(obj)
57
+
58
+ r = BytesOciDataRef(b)
59
+ ri = OciDataRefInfo(r)
60
+ self.add_blob(r, ri)
61
+
62
+ return OciMediaDescriptor(
63
+ media_type=getattr(obj, 'media_type'),
64
+ digest=ri.digest(),
65
+ size=ri.size(),
66
+ )
67
+
68
+ def to_media(self, obj: OciDataclass) -> ta.Union[OciMediaDataclass, OciMediaDescriptor]:
69
+ def make_kw(*exclude):
70
+ return {
71
+ a: v
72
+ for f in dc.fields(obj)
73
+ if (a := f.name) not in exclude
74
+ for v in [getattr(obj, a)]
75
+ if v is not None
76
+ }
77
+
78
+ if isinstance(obj, OciImageIndex):
79
+ return OciMediaImageIndex(
80
+ **make_kw('manifests'),
81
+ manifests=[
82
+ self.add_data(m)
83
+ for m in obj.manifests
84
+ ],
85
+ )
86
+
87
+ elif isinstance(obj, OciImageManifest):
88
+ return OciMediaImageManifest(
89
+ **make_kw('config', 'layers'),
90
+ config=self.add_data(obj.config),
91
+ layers=[
92
+ self.add_data(l)
93
+ for l in obj.layers
94
+ ],
95
+ )
96
+
97
+ elif isinstance(obj, OciImageLayer):
98
+ ri = OciDataRefInfo(obj.data)
99
+ self.add_blob(obj.data, ri)
100
+ return OciMediaDescriptor(
101
+ media_type=OCI_IMAGE_LAYER_KIND_MEDIA_TYPES[obj.kind],
102
+ digest=ri.digest(),
103
+ size=ri.size(),
104
+ )
105
+
106
+ elif isinstance(obj, OciImageConfig):
107
+ return OciMediaImageConfig(**make_kw())
108
+
109
+ else:
110
+ raise TypeError(obj)
111
+
112
+ def add_data(self, obj: OciDataclass) -> OciMediaDescriptor:
113
+ ret = self.to_media(obj)
114
+
115
+ if isinstance(ret, OciMediaDataclass):
116
+ return self.add_media(ret)
117
+
118
+ elif isinstance(ret, OciMediaDescriptor):
119
+ return ret
120
+
121
+ else:
122
+ raise TypeError(ret)
omlish/docker/oci/data.py CHANGED
@@ -2,16 +2,18 @@
2
2
  # @omlish-lite
3
3
  import abc
4
4
  import dataclasses as dc
5
+ import enum
5
6
  import typing as ta
6
7
 
7
8
  from ...lite.marshal import OBJ_MARSHALER_FIELD_KEY
8
9
  from ...lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
10
+ from .datarefs import OciDataRef
9
11
 
10
12
 
11
13
  ##
12
14
 
13
15
 
14
- @dc.dataclass(frozen=True)
16
+ @dc.dataclass()
15
17
  class OciDataclass(abc.ABC): # noqa
16
18
  pass
17
19
 
@@ -19,17 +21,52 @@ class OciDataclass(abc.ABC): # noqa
19
21
  ##
20
22
 
21
23
 
22
- @dc.dataclass(frozen=True)
24
+ @dc.dataclass()
25
+ class OciImageIndex(OciDataclass):
26
+ manifests: ta.List[ta.Union['OciImageIndex', 'OciImageManifest']]
27
+
28
+ annotations: ta.Optional[ta.Dict[str, str]] = None
29
+
30
+
31
+ #
32
+
33
+
34
+ @dc.dataclass()
35
+ class OciImageManifest(OciDataclass):
36
+ config: 'OciImageConfig'
37
+
38
+ layers: ta.List['OciImageLayer']
39
+
40
+
41
+ #
42
+
43
+
44
+ @dc.dataclass()
45
+ class OciImageLayer(OciDataclass):
46
+ class Kind(enum.Enum):
47
+ TAR = enum.auto()
48
+ TAR_GZIP = enum.auto()
49
+ TAR_ZSTD = enum.auto()
50
+
51
+ kind: Kind
52
+
53
+ data: OciDataRef
54
+
55
+
56
+ #
57
+
58
+
59
+ @dc.dataclass()
23
60
  class OciImageConfig(OciDataclass):
24
61
  """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/config.md"""
25
62
 
26
63
  architecture: str
27
64
  os: str
28
65
 
29
- @dc.dataclass(frozen=True)
66
+ @dc.dataclass()
30
67
  class RootFs:
31
68
  type: str
32
- diff_ids: ta.Sequence[str]
69
+ diff_ids: ta.List[str]
33
70
 
34
71
  rootfs: RootFs
35
72
 
@@ -38,7 +75,7 @@ class OciImageConfig(OciDataclass):
38
75
  created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
39
76
  author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
40
77
  os_version: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.version', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
41
- os_features: ta.Optional[ta.Sequence[str]] = dc.field(default=None, metadata={OBJ_MARSHALER_FIELD_KEY: 'os.features', OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
78
+ 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
42
79
  variant: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
43
80
 
44
81
  """
@@ -58,9 +95,9 @@ class OciImageConfig(OciDataclass):
58
95
  CpuShares integer, OPTIONAL
59
96
  Healthcheck object, OPTIONAL
60
97
  """
61
- config: ta.Optional[ta.Mapping[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
98
+ config: ta.Optional[ta.Dict[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
62
99
 
63
- @dc.dataclass(frozen=True)
100
+ @dc.dataclass()
64
101
  class History:
65
102
  created: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
66
103
  author: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
@@ -68,4 +105,21 @@ class OciImageConfig(OciDataclass):
68
105
  comment: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
69
106
  empty_layer: ta.Optional[bool] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
70
107
 
71
- history: ta.Optional[ta.Sequence[History]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
108
+ history: ta.Optional[ta.List[History]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
109
+
110
+
111
+ ##
112
+
113
+
114
+ def is_empty_oci_dataclass(obj: OciDataclass) -> bool:
115
+ if not isinstance(obj, OciDataclass):
116
+ raise TypeError(obj)
117
+
118
+ elif isinstance(obj, OciImageIndex):
119
+ return not obj.manifests
120
+
121
+ elif isinstance(obj, OciImageManifest):
122
+ return not obj.layers
123
+
124
+ else:
125
+ return False
@@ -0,0 +1,98 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import hashlib
6
+ import io
7
+ import os.path
8
+ import shutil
9
+ import typing as ta
10
+
11
+ from omlish.lite.cached import cached_nullary
12
+
13
+
14
+ ##
15
+
16
+
17
+ @dc.dataclass(frozen=True)
18
+ class OciDataRef(abc.ABC): # noqa
19
+ pass
20
+
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class BytesOciDataRef(OciDataRef):
24
+ data: bytes
25
+
26
+
27
+ @dc.dataclass(frozen=True)
28
+ class FileOciDataRef(OciDataRef):
29
+ path: str
30
+
31
+
32
+ ##
33
+
34
+
35
+ @dc.dataclass(frozen=True)
36
+ class OciDataRefInfo:
37
+ data: OciDataRef
38
+
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
+
45
+ elif isinstance(self.data, BytesOciDataRef):
46
+ return hashlib.sha256(self.data.data).hexdigest()
47
+
48
+ else:
49
+ raise TypeError(self.data)
50
+
51
+ @cached_nullary
52
+ def digest(self) -> str:
53
+ return f'sha256:{self.sha256()}'
54
+
55
+ @cached_nullary
56
+ def size(self) -> int:
57
+ if isinstance(self.data, FileOciDataRef):
58
+ return os.path.getsize(self.data.path)
59
+
60
+ elif isinstance(self.data, BytesOciDataRef):
61
+ return len(self.data.data)
62
+
63
+ else:
64
+ raise TypeError(self.data)
65
+
66
+
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
+ else:
87
+ raise TypeError(data)
88
+
89
+
90
+ def open_oci_data_ref(data: OciDataRef) -> ta.BinaryIO:
91
+ if isinstance(data, FileOciDataRef):
92
+ return open(data.path, 'rb')
93
+
94
+ elif isinstance(data, BytesOciDataRef):
95
+ return io.BytesIO(data.data)
96
+
97
+ else:
98
+ raise TypeError(data)
@@ -0,0 +1,120 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import json
5
+ import typing as ta
6
+
7
+ from ...lite.check import check
8
+ from .data import OciImageConfig
9
+ from .data import OciImageIndex
10
+ from .data import OciImageLayer
11
+ from .data import OciImageManifest
12
+ from .data import is_empty_oci_dataclass
13
+ from .media import OCI_IMAGE_LAYER_KIND_MEDIA_TYPES_
14
+ from .media import OCI_MEDIA_FIELDS
15
+ from .media import OciMediaDescriptor
16
+ from .media import OciMediaImageConfig
17
+ from .media import OciMediaImageIndex
18
+ from .media import OciMediaImageManifest
19
+ from .media import unmarshal_oci_media_dataclass
20
+ from .repositories import OciRepository
21
+
22
+
23
+ T = ta.TypeVar('T')
24
+
25
+
26
+ ##
27
+
28
+
29
+ class OciRepositoryLoader:
30
+ def __init__(
31
+ self,
32
+ repo: OciRepository,
33
+ ) -> None:
34
+ super().__init__()
35
+
36
+ self._repo = repo
37
+
38
+ #
39
+
40
+ def load_object(
41
+ self,
42
+ data: bytes,
43
+ cls: ta.Type[T] = object, # type: ignore[assignment]
44
+ *,
45
+ media_type: ta.Optional[str] = None,
46
+ ) -> T:
47
+ text = data.decode('utf-8')
48
+ dct = json.loads(text)
49
+ obj = unmarshal_oci_media_dataclass(
50
+ dct,
51
+ media_type=media_type,
52
+ )
53
+ return check.isinstance(obj, cls)
54
+
55
+ def read_object(
56
+ self,
57
+ digest: str,
58
+ cls: ta.Type[T] = object, # type: ignore[assignment]
59
+ *,
60
+ media_type: ta.Optional[str] = None,
61
+ ) -> T:
62
+ data = self._repo.read_blob(digest)
63
+ return self.load_object(
64
+ data,
65
+ cls,
66
+ media_type=media_type,
67
+ )
68
+
69
+ def read_descriptor(
70
+ self,
71
+ desc: OciMediaDescriptor,
72
+ cls: ta.Type[T] = object, # type: ignore[assignment]
73
+ ) -> ta.Any:
74
+ return self.read_object(
75
+ desc.digest,
76
+ cls,
77
+ media_type=desc.media_type,
78
+ )
79
+
80
+ #
81
+
82
+ def from_media(self, obj: ta.Any) -> ta.Any:
83
+ def make_kw(*exclude):
84
+ return {
85
+ a: getattr(obj, a)
86
+ for f in dc.fields(obj)
87
+ if (a := f.name) not in OCI_MEDIA_FIELDS
88
+ and a not in exclude
89
+ }
90
+
91
+ if isinstance(obj, OciMediaImageConfig):
92
+ return OciImageConfig(**make_kw())
93
+
94
+ elif isinstance(obj, OciMediaImageManifest):
95
+ return OciImageManifest(
96
+ **make_kw('config', 'layers'),
97
+ config=self.from_media(self.read_descriptor(obj.config)),
98
+ layers=[
99
+ OciImageLayer(
100
+ kind=lk,
101
+ data=self._repo.ref_blob(l.digest),
102
+ )
103
+ for l in obj.layers
104
+ if (lk := OCI_IMAGE_LAYER_KIND_MEDIA_TYPES_.get(l.media_type)) is not None
105
+ ],
106
+ )
107
+
108
+ elif isinstance(obj, OciMediaImageIndex):
109
+ return OciImageIndex(
110
+ **make_kw('manifests'),
111
+ manifests=[
112
+ fm
113
+ for m in obj.manifests
114
+ for fm in [self.from_media(self.read_descriptor(m))]
115
+ if not is_empty_oci_dataclass(fm)
116
+ ],
117
+ )
118
+
119
+ else:
120
+ raise TypeError(obj)