omlish 0.0.0.dev40__py3-none-any.whl → 0.0.0.dev42__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/dataclasses/__init__.py +1 -0
- omlish/dataclasses/impl/__init__.py +1 -0
- omlish/dataclasses/utils.py +4 -0
- omlish/docker/__init__.py +27 -0
- omlish/docker/cli.py +101 -0
- omlish/docker/compose.py +51 -0
- omlish/docker/helpers.py +48 -0
- omlish/docker/hub.py +75 -0
- omlish/docker/manifests.py +166 -0
- omlish/lang/__init__.py +1 -0
- omlish/lang/strings.py +25 -37
- omlish/marshal/__init__.py +1 -0
- omlish/marshal/dataclasses.py +68 -15
- omlish/marshal/helpers.py +2 -8
- omlish/marshal/objects.py +58 -15
- omlish/specs/openapi/__init__.py +5 -0
- omlish/specs/openapi/marshal.py +63 -0
- omlish/specs/openapi/openapi.py +443 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/RECORD +25 -17
- omlish/docker.py +0 -273
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/dataclasses/__init__.py
CHANGED
omlish/dataclasses/utils.py
CHANGED
@@ -36,6 +36,10 @@ def opt_repr(o: ta.Any) -> str | None:
|
|
36
36
|
#
|
37
37
|
|
38
38
|
|
39
|
+
def fields_dict(cls_or_instance: ta.Any) -> dict[str, dc.Field]:
|
40
|
+
return {f.name: f for f in dc.fields(cls_or_instance)}
|
41
|
+
|
42
|
+
|
39
43
|
class field_modifier: # noqa
|
40
44
|
def __init__(self, fn: ta.Callable[[dc.Field], dc.Field]) -> None:
|
41
45
|
super().__init__()
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from .cli import ( # noqa
|
2
|
+
Inspect,
|
3
|
+
Port,
|
4
|
+
PsItem,
|
5
|
+
cli_inspect,
|
6
|
+
cli_ps,
|
7
|
+
has_cli,
|
8
|
+
parse_port,
|
9
|
+
)
|
10
|
+
|
11
|
+
from .compose import ( # noqa
|
12
|
+
ComposeConfig,
|
13
|
+
get_compose_port,
|
14
|
+
)
|
15
|
+
|
16
|
+
from .helpers import ( # noqa
|
17
|
+
DOCKER_FOR_MAC_HOSTNAME,
|
18
|
+
DOCKER_HOST_PLATFORM_KEY,
|
19
|
+
get_docker_host_platform,
|
20
|
+
is_likely_in_docker,
|
21
|
+
timebomb_payload,
|
22
|
+
)
|
23
|
+
|
24
|
+
from .hub import ( # noqa
|
25
|
+
HubRepoInfo,
|
26
|
+
get_hub_repo_info,
|
27
|
+
)
|
omlish/docker/cli.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- https://github.com/mag37/dockcheck/blob/3d122f2b868eb53a25a3014f0f6bd499390a3a29/dockcheck.sh
|
4
|
+
- https://github.com/regclient/regclient
|
5
|
+
- https://stackoverflow.com/questions/71409458/how-to-download-docker-image-using-http-api-using-docker-hub-credentials
|
6
|
+
- https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
|
7
|
+
- https://ops.tips/blog/inspecting-docker-image-without-pull/
|
8
|
+
""" # noqa
|
9
|
+
import datetime
|
10
|
+
import re
|
11
|
+
import subprocess
|
12
|
+
import typing as ta
|
13
|
+
|
14
|
+
from .. import check
|
15
|
+
from .. import dataclasses as dc
|
16
|
+
from .. import lang
|
17
|
+
from .. import marshal as msh
|
18
|
+
from ..formats import json
|
19
|
+
|
20
|
+
|
21
|
+
@dc.dataclass(frozen=True)
|
22
|
+
@msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
|
23
|
+
@msh.update_fields_metadata(['id'], name='ID')
|
24
|
+
class PsItem(lang.Final):
|
25
|
+
command: str
|
26
|
+
created_at: datetime.datetime
|
27
|
+
id: str
|
28
|
+
image: str
|
29
|
+
labels: str
|
30
|
+
local_volumes: str
|
31
|
+
mounts: str
|
32
|
+
names: str
|
33
|
+
networks: str
|
34
|
+
ports: str
|
35
|
+
running_for: str
|
36
|
+
size: str
|
37
|
+
state: str
|
38
|
+
status: str
|
39
|
+
|
40
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
41
|
+
|
42
|
+
|
43
|
+
class Port(ta.NamedTuple):
|
44
|
+
ip: str
|
45
|
+
from_port: int
|
46
|
+
to_port: int
|
47
|
+
proto: str
|
48
|
+
|
49
|
+
|
50
|
+
_PORT_PAT = re.compile(r'(?P<ip>[^:]+):(?P<from_port>\d+)->(?P<to_port>\d+)/(?P<proto>\w+)')
|
51
|
+
|
52
|
+
|
53
|
+
def parse_port(s: str) -> Port:
|
54
|
+
# '0.0.0.0:35221->22/tcp, 0.0.0.0:35220->8000/tcp'
|
55
|
+
m = check.not_none(_PORT_PAT.fullmatch(s))
|
56
|
+
return Port(
|
57
|
+
m.group('ip'),
|
58
|
+
int(m.group('from_port')),
|
59
|
+
int(m.group('to_port')),
|
60
|
+
m.group('proto'),
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
def cli_ps() -> list[PsItem]:
|
65
|
+
o = subprocess.check_output([
|
66
|
+
'docker',
|
67
|
+
'ps',
|
68
|
+
'--no-trunc',
|
69
|
+
'--format', '{{json .}}',
|
70
|
+
])
|
71
|
+
|
72
|
+
ret: list[PsItem] = []
|
73
|
+
for l in o.decode().splitlines():
|
74
|
+
d = json.loads(l)
|
75
|
+
pi = msh.unmarshal(d, PsItem)
|
76
|
+
ret.append(pi)
|
77
|
+
|
78
|
+
return ret
|
79
|
+
|
80
|
+
|
81
|
+
@dc.dataclass(frozen=True)
|
82
|
+
@msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
|
83
|
+
class Inspect(lang.Final):
|
84
|
+
id: str
|
85
|
+
created: datetime.datetime
|
86
|
+
|
87
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
88
|
+
|
89
|
+
|
90
|
+
def cli_inspect(ids: list[str]) -> list[Inspect]:
|
91
|
+
o = subprocess.check_output(['docker', 'inspect', *ids])
|
92
|
+
return msh.unmarshal(json.loads(o.decode()), list[Inspect])
|
93
|
+
|
94
|
+
|
95
|
+
def has_cli() -> bool:
|
96
|
+
try:
|
97
|
+
proc = subprocess.run(['docker', '--version']) # noqa
|
98
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
99
|
+
return False
|
100
|
+
else:
|
101
|
+
return not proc.returncode
|
omlish/docker/compose.py
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- merged compose configs: https://github.com/wrmsr/bane/blob/27647abdcfb323b73e6982a5c318c7029496b203/core/dev/docker/compose.go#L38
|
4
|
+
""" # noqa
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from .. import check
|
8
|
+
from .. import lang
|
9
|
+
|
10
|
+
|
11
|
+
if ta.TYPE_CHECKING:
|
12
|
+
import yaml
|
13
|
+
else:
|
14
|
+
yaml = lang.proxy_import('yaml')
|
15
|
+
|
16
|
+
|
17
|
+
class ComposeConfig:
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
prefix: str,
|
21
|
+
*,
|
22
|
+
file_path: str | None = None,
|
23
|
+
) -> None:
|
24
|
+
super().__init__()
|
25
|
+
|
26
|
+
self._prefix = prefix
|
27
|
+
self._file_path = file_path
|
28
|
+
|
29
|
+
@lang.cached_function
|
30
|
+
def get_config(self) -> ta.Mapping[str, ta.Any]:
|
31
|
+
with open(check.not_none(self._file_path)) as f:
|
32
|
+
buf = f.read()
|
33
|
+
return yaml.safe_load(buf)
|
34
|
+
|
35
|
+
@lang.cached_function
|
36
|
+
def get_services(self) -> ta.Mapping[str, ta.Any]:
|
37
|
+
ret = {}
|
38
|
+
for n, c in self.get_config()['services'].items():
|
39
|
+
check.state(n.startswith(self._prefix))
|
40
|
+
ret[n[len(self._prefix):]] = c
|
41
|
+
|
42
|
+
return ret
|
43
|
+
|
44
|
+
|
45
|
+
def get_compose_port(cfg: ta.Mapping[str, ta.Any], default: int) -> int:
|
46
|
+
return check.single(
|
47
|
+
int(l)
|
48
|
+
for p in cfg['ports']
|
49
|
+
for l, r in [p.split(':')]
|
50
|
+
if int(r) == default
|
51
|
+
)
|
omlish/docker/helpers.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import shlex
|
4
|
+
import sys
|
5
|
+
|
6
|
+
|
7
|
+
##
|
8
|
+
|
9
|
+
|
10
|
+
_DEFAULT_TIMEBOMB_NAME = '-'.join([*__name__.split('.'), 'timebomb'])
|
11
|
+
|
12
|
+
|
13
|
+
def timebomb_payload(delay_s: float, name: str = _DEFAULT_TIMEBOMB_NAME) -> str:
|
14
|
+
return (
|
15
|
+
'('
|
16
|
+
f'echo {shlex.quote(name)} && '
|
17
|
+
f'sleep {delay_s:g} && '
|
18
|
+
'sh -c \'killall5 -9 -o $PPID -o $$ ; kill 1\''
|
19
|
+
') &'
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
|
25
|
+
|
26
|
+
DOCKER_FOR_MAC_HOSTNAME = 'docker.for.mac.localhost'
|
27
|
+
|
28
|
+
|
29
|
+
_LIKELY_IN_DOCKER_PATTERN = re.compile(r'^overlay / .*/docker/')
|
30
|
+
|
31
|
+
|
32
|
+
def is_likely_in_docker() -> bool:
|
33
|
+
if getattr(sys, 'platform') != 'linux':
|
34
|
+
return False
|
35
|
+
with open('/proc/mounts') as f:
|
36
|
+
ls = f.readlines()
|
37
|
+
return any(_LIKELY_IN_DOCKER_PATTERN.match(l) for l in ls)
|
38
|
+
|
39
|
+
|
40
|
+
##
|
41
|
+
|
42
|
+
|
43
|
+
# Set by pyproject, docker-dev script
|
44
|
+
DOCKER_HOST_PLATFORM_KEY = 'DOCKER_HOST_PLATFORM'
|
45
|
+
|
46
|
+
|
47
|
+
def get_docker_host_platform() -> str | None:
|
48
|
+
return os.environ.get(DOCKER_HOST_PLATFORM_KEY)
|
omlish/docker/hub.py
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
import typing as ta
|
2
|
+
import urllib.request
|
3
|
+
|
4
|
+
from .. import dataclasses as dc
|
5
|
+
from ..formats import json
|
6
|
+
|
7
|
+
|
8
|
+
@dc.dataclass(frozen=True)
|
9
|
+
class HubRepoInfo:
|
10
|
+
repo: str
|
11
|
+
tags: ta.Mapping[str, ta.Any]
|
12
|
+
latest_manifests: ta.Mapping[str, ta.Any]
|
13
|
+
|
14
|
+
|
15
|
+
def get_hub_repo_info(
|
16
|
+
repo: str,
|
17
|
+
*,
|
18
|
+
auth_url: str = 'https://auth.docker.io/',
|
19
|
+
api_url: str = 'https://registry-1.docker.io/v2/',
|
20
|
+
) -> HubRepoInfo:
|
21
|
+
"""
|
22
|
+
https://stackoverflow.com/a/39376254
|
23
|
+
|
24
|
+
==
|
25
|
+
|
26
|
+
repo=library/nginx
|
27
|
+
token=$(
|
28
|
+
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
|
29
|
+
| jq -r '.token' \
|
30
|
+
)
|
31
|
+
curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
|
32
|
+
curl \
|
33
|
+
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
34
|
+
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
35
|
+
-H "Authorization: Bearer $token" \
|
36
|
+
-s "https://registry-1.docker.io/v2/${repo}/manifests/latest" \
|
37
|
+
| jq .
|
38
|
+
"""
|
39
|
+
|
40
|
+
auth_url = auth_url.rstrip('/')
|
41
|
+
api_url = api_url.rstrip('/')
|
42
|
+
|
43
|
+
#
|
44
|
+
|
45
|
+
def req_json(url: str, **kwargs: ta.Any) -> ta.Any:
|
46
|
+
with urllib.request.urlopen(urllib.request.Request(url, **kwargs)) as resp: # noqa
|
47
|
+
return json.loads(resp.read().decode('utf-8'))
|
48
|
+
|
49
|
+
#
|
50
|
+
|
51
|
+
token_dct = req_json(f'{auth_url}/token?service=registry.docker.io&scope=repository:{repo}:pull')
|
52
|
+
token = token_dct['token']
|
53
|
+
|
54
|
+
req_hdrs = {'Authorization': f'Bearer {token}'}
|
55
|
+
|
56
|
+
#
|
57
|
+
|
58
|
+
tags_dct = req_json(
|
59
|
+
f'{api_url}/{repo}/tags/list',
|
60
|
+
headers=req_hdrs,
|
61
|
+
)
|
62
|
+
|
63
|
+
latest_mani_dct = req_json(
|
64
|
+
f'{api_url}/{repo}/manifests/latest',
|
65
|
+
headers={
|
66
|
+
**req_hdrs,
|
67
|
+
'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
|
68
|
+
},
|
69
|
+
)
|
70
|
+
|
71
|
+
return HubRepoInfo(
|
72
|
+
repo,
|
73
|
+
tags_dct,
|
74
|
+
latest_mani_dct,
|
75
|
+
)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""
|
2
|
+
https://github.com/openshift/docker-distribution/blob/master/docs/spec/manifest-v2-2.md
|
3
|
+
"""
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from .. import dataclasses as dc
|
7
|
+
from .. import lang
|
8
|
+
from .. import marshal as msh
|
9
|
+
|
10
|
+
|
11
|
+
SCHEMA_VERSION = 2
|
12
|
+
|
13
|
+
|
14
|
+
class MediaTypes(lang.Namespace):
|
15
|
+
# schema1 (existing manifest format)
|
16
|
+
MANIFEST_V1 = 'application/vnd.docker.distribution.manifest.v1+json'
|
17
|
+
|
18
|
+
# New image manifest format (schemaVersion = 2)
|
19
|
+
MANIFEST_V2 = 'application/vnd.docker.distribution.manifest.v2+json'
|
20
|
+
|
21
|
+
# Manifest list, aka "fat manifest"
|
22
|
+
MANIFEST_LIST = 'application/vnd.docker.distribution.manifest.list.v2+json'
|
23
|
+
|
24
|
+
# Container config JSON
|
25
|
+
CONTAINER_CONFIG = 'application/vnd.docker.container.image.v1+json'
|
26
|
+
|
27
|
+
# "Layer", as a gzipped tar
|
28
|
+
LAYER = 'application/vnd.docker.image.rootfs.diff.tar.gzip'
|
29
|
+
|
30
|
+
# "Layer", as a gzipped tar that should never be pushed
|
31
|
+
LAYER_NEVER_PUSH = 'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip'
|
32
|
+
|
33
|
+
# Plugin config JSON
|
34
|
+
PLUGIN_CONFIG = 'application/vnd.docker.plugin.v1+json'
|
35
|
+
|
36
|
+
|
37
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
38
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
39
|
+
@msh.update_fields_metadata(['os_version'], name='os.version')
|
40
|
+
@msh.update_fields_metadata(['os_features'], name='os.features')
|
41
|
+
class Platform(lang.Final):
|
42
|
+
# The architecture field specifies the CPU architecture, for example amd64 or ppc64le.
|
43
|
+
architecture: str
|
44
|
+
|
45
|
+
# The os field specifies the operating system, for example linux or windows.
|
46
|
+
os: str
|
47
|
+
|
48
|
+
# The optional os.version field specifies the operating system version, for example 10.0.10586.
|
49
|
+
os_version: str | None = None
|
50
|
+
|
51
|
+
# The optional os.features field specifies an array of strings, each listing a required OS feature (for example on
|
52
|
+
# Windows win32k).
|
53
|
+
os_features: ta.Sequence[ta.Any] | None = None
|
54
|
+
|
55
|
+
# The optional variant field specifies a variant of the CPU, for example v6 to specify a particular CPU variant of
|
56
|
+
# the ARM CPU.
|
57
|
+
variant: str | None = None
|
58
|
+
|
59
|
+
# The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4
|
60
|
+
# or aes).
|
61
|
+
features: ta.Sequence[ta.Any] | None = None
|
62
|
+
|
63
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
64
|
+
|
65
|
+
|
66
|
+
@dc.dataclass(frozen=True)
|
67
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
68
|
+
class Manifest(lang.Final):
|
69
|
+
# The MIME type of the referenced object. This will generally be
|
70
|
+
# application/vnd.docker.distribution.manifest.v2+json, but it could also be
|
71
|
+
# application/vnd.docker.distribution.manifest.v1+json if the manifest list references a legacy schema-1 manifest.
|
72
|
+
media_type: str
|
73
|
+
|
74
|
+
# The size in bytes of the object. This field exists so that a client will have an expected size for the content
|
75
|
+
# before validating. If the length of the retrieved content does not match the specified length, the content should
|
76
|
+
# not be trusted.
|
77
|
+
size: int
|
78
|
+
|
79
|
+
# The digest of the content, as defined by the Registry V2 HTTP API Specification.
|
80
|
+
digest: str
|
81
|
+
|
82
|
+
# The platform object describes the platform which the image in the manifest runs on. A full list of valid operating
|
83
|
+
# system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH
|
84
|
+
platform: Platform | None = None
|
85
|
+
|
86
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
87
|
+
|
88
|
+
|
89
|
+
@dc.dataclass(frozen=True)
|
90
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
91
|
+
class ManifestList(lang.Final):
|
92
|
+
# This field specifies the image manifest schema version as an integer. This schema uses the version 2.
|
93
|
+
schema_version: int
|
94
|
+
dc.validate(lambda self: self.schema_version == SCHEMA_VERSION)
|
95
|
+
|
96
|
+
# The MIME type of the manifest list. This should be set to
|
97
|
+
# application/vnd.docker.distribution.manifest.list.v2+json.
|
98
|
+
media_type: str
|
99
|
+
dc.validate(lambda self: self.media_type == MediaTypes.MANIFEST_LIST)
|
100
|
+
|
101
|
+
# The manifests field contains a list of manifests for specific platforms.
|
102
|
+
manifests: ta.Sequence[Manifest]
|
103
|
+
|
104
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
105
|
+
|
106
|
+
|
107
|
+
@dc.dataclass(frozen=True)
|
108
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
109
|
+
class ImageManifest(lang.Final):
|
110
|
+
# This field specifies the image manifest schema version as an integer. This schema uses version 2.
|
111
|
+
schema_version: int
|
112
|
+
dc.validate(lambda self: self.schema_version == SCHEMA_VERSION)
|
113
|
+
|
114
|
+
# The MIME type of the manifest. This should be set to application/vnd.docker.distribution.manifest.v2+json.
|
115
|
+
media_type: str
|
116
|
+
dc.validate(lambda self: self.media_type == MediaTypes.MANIFEST_V2)
|
117
|
+
|
118
|
+
@dc.dataclass(frozen=True)
|
119
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
120
|
+
class Config(lang.Final):
|
121
|
+
# The MIME type of the referenced object. This should generally be
|
122
|
+
# application/vnd.docker.container.image.v1+json.
|
123
|
+
media_type: str
|
124
|
+
|
125
|
+
# The size in bytes of the object. This field exists so that a client will have an expected size for the content
|
126
|
+
# before validating. If the length of the retrieved content does not match the specified length, the content
|
127
|
+
# should not be trusted.
|
128
|
+
size: int
|
129
|
+
|
130
|
+
# The digest of the content, as defined by the Registry V2 HTTP API Specification.
|
131
|
+
digest: str
|
132
|
+
|
133
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
134
|
+
|
135
|
+
# The config field references a configuration object for a container, by digest. This configuration item is a JSON
|
136
|
+
# blob that the runtime uses to set up the container. This new schema uses a tweaked version of this configuration
|
137
|
+
# o allow image content-addressability on the daemon side.
|
138
|
+
config: Config | None = None
|
139
|
+
|
140
|
+
@dc.dataclass(frozen=True)
|
141
|
+
@msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
|
142
|
+
class Layer(lang.Final):
|
143
|
+
# The MIME type of the referenced object. This should generally be
|
144
|
+
# application/vnd.docker.image.rootfs.diff.tar.gzip. Layers of type
|
145
|
+
# application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but they should
|
146
|
+
# never be pushed.
|
147
|
+
media_type: str
|
148
|
+
|
149
|
+
# The size in bytes of the object. This field exists so that a client will have an expected size for the content
|
150
|
+
# before validating. If the length of the retrieved content does not match the specified length, the content
|
151
|
+
# should not be trusted.
|
152
|
+
size: int
|
153
|
+
|
154
|
+
# The digest of the content, as defined by the Registry V2 HTTP API Specification.
|
155
|
+
digest: str
|
156
|
+
|
157
|
+
# Provides a list of URLs from which the content may be fetched. Content must be verified against the digest and
|
158
|
+
# size. This field is optional and uncommon.
|
159
|
+
urls: ta.Sequence[str] | None = None
|
160
|
+
|
161
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
162
|
+
|
163
|
+
# The layer list is ordered starting from the base image (opposite order of schema1).
|
164
|
+
layers: ta.Sequence[Layer] | None = None
|
165
|
+
|
166
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
omlish/lang/__init__.py
CHANGED
omlish/lang/strings.py
CHANGED
@@ -2,63 +2,51 @@ import typing as ta
|
|
2
2
|
import unicodedata
|
3
3
|
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
@ta.overload
|
8
|
-
def prefix_delimited(s: str, p: str, d: str) -> str:
|
9
|
-
...
|
5
|
+
StrOrBytes: ta.TypeAlias = str | bytes
|
6
|
+
StrOrBytesT = ta.TypeVar('StrOrBytesT', bound=StrOrBytes)
|
10
7
|
|
11
8
|
|
12
|
-
|
13
|
-
def prefix_delimited(s: bytes, p: bytes, d: bytes) -> bytes:
|
14
|
-
...
|
9
|
+
##
|
15
10
|
|
16
11
|
|
17
|
-
def prefix_delimited(s, p, d):
|
18
|
-
return d.join([p + l for l in s.split(d)])
|
12
|
+
def prefix_delimited(s: StrOrBytesT, p: StrOrBytesT, d: StrOrBytesT) -> StrOrBytesT:
|
13
|
+
return d.join([p + l for l in s.split(d)]) # type: ignore
|
19
14
|
|
20
15
|
|
21
|
-
def prefix_lines(s:
|
22
|
-
return prefix_delimited(s, p, '\n')
|
16
|
+
def prefix_lines(s: StrOrBytesT, p: StrOrBytesT) -> StrOrBytesT:
|
17
|
+
return prefix_delimited(s, p, '\n' if isinstance(s, str) else b'\n')
|
23
18
|
|
24
19
|
|
25
|
-
def indent_lines(s:
|
26
|
-
return prefix_lines(s, ' ' * num)
|
20
|
+
def indent_lines(s: StrOrBytesT, num: StrOrBytesT) -> StrOrBytesT:
|
21
|
+
return prefix_lines(s, (' ' if isinstance(s, str) else b' ') * num) # type: ignore
|
27
22
|
|
28
23
|
|
29
24
|
##
|
30
25
|
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
...
|
35
|
-
|
36
|
-
|
37
|
-
@ta.overload
|
38
|
-
def strip_prefix(s: bytes, pfx: bytes) -> bytes:
|
39
|
-
...
|
40
|
-
|
41
|
-
|
42
|
-
def strip_prefix(s, pfx):
|
43
|
-
if not s.startswith(pfx):
|
27
|
+
def strip_prefix(s: StrOrBytesT, pfx: StrOrBytesT) -> StrOrBytesT:
|
28
|
+
if not s.startswith(pfx): # type: ignore
|
44
29
|
raise ValueError(f'{s!r} does not start with {pfx!r}')
|
45
|
-
return s[len(pfx):]
|
30
|
+
return s[len(pfx):] # type: ignore
|
46
31
|
|
47
32
|
|
48
|
-
|
49
|
-
|
50
|
-
|
33
|
+
def strip_suffix(s: StrOrBytesT, sfx: StrOrBytesT) -> StrOrBytesT:
|
34
|
+
if not s.endswith(sfx): # type: ignore
|
35
|
+
raise ValueError(f'{s!r} does not end with {sfx!r}')
|
36
|
+
return s[:-len(sfx)] # type: ignore
|
51
37
|
|
52
38
|
|
53
|
-
|
54
|
-
def strip_suffix(s: bytes, sfx: bytes) -> bytes:
|
55
|
-
...
|
39
|
+
##
|
56
40
|
|
57
41
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
|
42
|
+
def replace_many(
|
43
|
+
s: StrOrBytesT,
|
44
|
+
old: ta.Iterable[StrOrBytesT],
|
45
|
+
new: StrOrBytesT, count_each: int = -1,
|
46
|
+
) -> StrOrBytesT:
|
47
|
+
for o in old:
|
48
|
+
s = s.replace(o, new, count_each) # type: ignore
|
49
|
+
return s
|
62
50
|
|
63
51
|
|
64
52
|
##
|