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.
- omlish/__about__.py +2 -2
- omlish/algorithm/__init__.py +0 -0
- omlish/algorithm/all.py +13 -0
- omlish/algorithm/distribute.py +46 -0
- omlish/algorithm/toposort.py +26 -0
- omlish/algorithm/unify.py +31 -0
- omlish/collections/__init__.py +0 -2
- omlish/collections/utils.py +0 -46
- omlish/docker/oci/building.py +122 -0
- omlish/docker/oci/data.py +62 -8
- omlish/docker/oci/datarefs.py +98 -0
- omlish/docker/oci/loading.py +120 -0
- omlish/docker/oci/media.py +44 -14
- omlish/docker/oci/repositories.py +72 -0
- omlish/graphs/trees.py +2 -1
- omlish/http/coro/server.py +42 -33
- omlish/http/{simple.py → coro/simple.py} +17 -17
- omlish/specs/irc/__init__.py +0 -0
- omlish/specs/irc/format/LICENSE +11 -0
- omlish/specs/irc/format/__init__.py +61 -0
- omlish/specs/irc/format/consts.py +6 -0
- omlish/specs/irc/format/errors.py +30 -0
- omlish/specs/irc/format/message.py +18 -0
- omlish/specs/irc/format/nuh.py +52 -0
- omlish/specs/irc/format/parsing.py +155 -0
- omlish/specs/irc/format/rendering.py +150 -0
- omlish/specs/irc/format/tags.py +99 -0
- omlish/specs/irc/format/utils.py +27 -0
- omlish/specs/irc/numerics/__init__.py +0 -0
- omlish/specs/irc/numerics/formats.py +94 -0
- omlish/specs/irc/numerics/numerics.py +808 -0
- omlish/specs/irc/numerics/types.py +59 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/RECORD +38 -14
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
File without changes
|
omlish/algorithm/all.py
ADDED
@@ -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])
|
omlish/collections/__init__.py
CHANGED
omlish/collections/utils.py
CHANGED
@@ -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(
|
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(
|
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(
|
66
|
+
@dc.dataclass()
|
30
67
|
class RootFs:
|
31
68
|
type: str
|
32
|
-
diff_ids: ta.
|
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.
|
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.
|
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(
|
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.
|
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)
|