omdev 0.0.0.dev221__py3-none-any.whl → 0.0.0.dev223__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 +40 -23
- omdev/ci/ci.py +49 -109
- 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.py → docker/cmds.py} +1 -44
- omdev/ci/docker/imagepulling.py +64 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +15 -5
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- 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 +89 -0
- omdev/oci/__init__.py +0 -0
- omdev/oci/building.py +221 -0
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +151 -0
- omdev/oci/datarefs.py +138 -0
- omdev/oci/dataserver.py +61 -0
- omdev/oci/loading.py +142 -0
- omdev/oci/media.py +179 -0
- omdev/oci/packing.py +381 -0
- omdev/oci/repositories.py +159 -0
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +1841 -384
- omdev/scripts/interp.py +100 -22
- omdev/scripts/pyproject.py +122 -28
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/RECORD +40 -15
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from ..cache import FileCache
|
7
|
+
from ..utils import log_timing_context
|
8
|
+
from .cache import DockerCache
|
9
|
+
from .cmds import is_docker_image_present
|
10
|
+
from .cmds import pull_docker_image
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
class DockerImagePulling(abc.ABC):
|
17
|
+
@abc.abstractmethod
|
18
|
+
def pull_docker_image(self, image: str) -> ta.Awaitable[None]:
|
19
|
+
raise NotImplementedError
|
20
|
+
|
21
|
+
|
22
|
+
class DockerImagePullingImpl(DockerImagePulling):
|
23
|
+
@dc.dataclass(frozen=True)
|
24
|
+
class Config:
|
25
|
+
always_pull: bool = False
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
*,
|
30
|
+
config: Config = Config(),
|
31
|
+
|
32
|
+
file_cache: ta.Optional[FileCache] = None,
|
33
|
+
docker_cache: ta.Optional[DockerCache] = None,
|
34
|
+
) -> None:
|
35
|
+
super().__init__()
|
36
|
+
|
37
|
+
self._config = config
|
38
|
+
|
39
|
+
self._file_cache = file_cache
|
40
|
+
self._docker_cache = docker_cache
|
41
|
+
|
42
|
+
async def _pull_docker_image(self, image: str) -> None:
|
43
|
+
if not self._config.always_pull and (await is_docker_image_present(image)):
|
44
|
+
return
|
45
|
+
|
46
|
+
dep_suffix = image
|
47
|
+
for c in '/:.-_':
|
48
|
+
dep_suffix = dep_suffix.replace(c, '-')
|
49
|
+
|
50
|
+
cache_key = f'docker-{dep_suffix}'
|
51
|
+
if (
|
52
|
+
self._docker_cache is not None and
|
53
|
+
(await self._docker_cache.load_cache_docker_image(cache_key)) is not None
|
54
|
+
):
|
55
|
+
return
|
56
|
+
|
57
|
+
await pull_docker_image(image)
|
58
|
+
|
59
|
+
if self._docker_cache is not None:
|
60
|
+
await self._docker_cache.save_cache_docker_image(cache_key, image)
|
61
|
+
|
62
|
+
async def pull_docker_image(self, image: str) -> None:
|
63
|
+
with log_timing_context(f'Load docker image: {image}'):
|
64
|
+
await self._pull_docker_image(image)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from omlish.lite.inject import InjectorBindingOrBindings
|
5
|
+
from omlish.lite.inject import InjectorBindings
|
6
|
+
from omlish.lite.inject import inj
|
7
|
+
|
8
|
+
from .buildcaching import DockerBuildCaching
|
9
|
+
from .buildcaching import DockerBuildCachingImpl
|
10
|
+
from .cache import DockerCache
|
11
|
+
from .cache import DockerCacheImpl
|
12
|
+
from .imagepulling import DockerImagePulling
|
13
|
+
from .imagepulling import DockerImagePullingImpl
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
def bind_docker(
|
20
|
+
*,
|
21
|
+
build_caching_config: DockerBuildCachingImpl.Config,
|
22
|
+
image_pulling_config: DockerImagePullingImpl.Config = DockerImagePullingImpl.Config(),
|
23
|
+
) -> InjectorBindings:
|
24
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
25
|
+
inj.bind(build_caching_config),
|
26
|
+
inj.bind(DockerBuildCachingImpl, singleton=True),
|
27
|
+
inj.bind(DockerBuildCaching, to_key=DockerBuildCachingImpl),
|
28
|
+
|
29
|
+
inj.bind(DockerCacheImpl, singleton=True),
|
30
|
+
inj.bind(DockerCache, to_key=DockerCacheImpl),
|
31
|
+
|
32
|
+
inj.bind(image_pulling_config),
|
33
|
+
inj.bind(DockerImagePullingImpl, singleton=True),
|
34
|
+
inj.bind(DockerImagePulling, to_key=DockerImagePullingImpl),
|
35
|
+
]
|
36
|
+
|
37
|
+
return inj.as_bindings(*lst)
|
omdev/ci/docker/utils.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- some less stupid Dockerfile hash
|
5
|
+
- doesn't change too much though
|
6
|
+
"""
|
7
|
+
import contextlib
|
8
|
+
import json
|
9
|
+
import tarfile
|
10
|
+
|
11
|
+
from omlish.lite.check import check
|
12
|
+
|
13
|
+
from ..utils import sha256_str
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
def build_docker_file_hash(docker_file: str) -> str:
|
20
|
+
with open(docker_file) as f:
|
21
|
+
contents = f.read()
|
22
|
+
|
23
|
+
return sha256_str(contents)
|
24
|
+
|
25
|
+
|
26
|
+
##
|
27
|
+
|
28
|
+
|
29
|
+
def read_docker_tar_image_tag(tar_file: str) -> str:
|
30
|
+
with tarfile.open(tar_file) as tf:
|
31
|
+
with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
|
32
|
+
m = mf.read()
|
33
|
+
|
34
|
+
manifests = json.loads(m.decode('utf-8'))
|
35
|
+
manifest = check.single(manifests)
|
36
|
+
tag = check.non_empty_str(check.single(manifest['RepoTags']))
|
37
|
+
return tag
|
38
|
+
|
39
|
+
|
40
|
+
def read_docker_tar_image_id(tar_file: str) -> str:
|
41
|
+
with tarfile.open(tar_file) as tf:
|
42
|
+
with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
|
43
|
+
i = mf.read()
|
44
|
+
|
45
|
+
index = json.loads(i.decode('utf-8'))
|
46
|
+
manifest = check.single(index['manifests'])
|
47
|
+
image_id = check.non_empty_str(manifest['digest'])
|
48
|
+
return image_id
|
omdev/ci/github/cache.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
2
3
|
import os.path
|
3
4
|
import typing as ta
|
4
5
|
|
5
6
|
from omlish.lite.check import check
|
6
7
|
from omlish.os.files import unlinking_if_exists
|
7
8
|
|
9
|
+
from ..cache import CacheVersion
|
8
10
|
from ..cache import DirectoryFileCache
|
9
11
|
from ..cache import FileCache
|
10
12
|
from .client import GithubCacheClient
|
@@ -15,16 +17,22 @@ from .client import GithubCacheServiceV1Client
|
|
15
17
|
|
16
18
|
|
17
19
|
class GithubFileCache(FileCache):
|
20
|
+
@dc.dataclass(frozen=True)
|
21
|
+
class Config:
|
22
|
+
dir: str
|
23
|
+
|
18
24
|
def __init__(
|
19
25
|
self,
|
20
|
-
|
26
|
+
config: Config,
|
21
27
|
*,
|
22
28
|
client: ta.Optional[GithubCacheClient] = None,
|
23
|
-
|
29
|
+
version: ta.Optional[CacheVersion] = None,
|
24
30
|
) -> None:
|
25
|
-
super().__init__(
|
31
|
+
super().__init__(
|
32
|
+
version=version,
|
33
|
+
)
|
26
34
|
|
27
|
-
self.
|
35
|
+
self._config = config
|
28
36
|
|
29
37
|
if client is None:
|
30
38
|
client = GithubCacheServiceV1Client(
|
@@ -33,7 +41,9 @@ class GithubFileCache(FileCache):
|
|
33
41
|
self._client: GithubCacheClient = client
|
34
42
|
|
35
43
|
self._local = DirectoryFileCache(
|
36
|
-
|
44
|
+
DirectoryFileCache.Config(
|
45
|
+
dir=check.non_empty_str(config.dir),
|
46
|
+
),
|
37
47
|
version=self._version,
|
38
48
|
)
|
39
49
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from omlish.lite.inject import InjectorBindingOrBindings
|
5
|
+
from omlish.lite.inject import InjectorBindings
|
6
|
+
from omlish.lite.inject import inj
|
7
|
+
|
8
|
+
from ..cache import FileCache
|
9
|
+
from .cache import GithubFileCache
|
10
|
+
|
11
|
+
|
12
|
+
##
|
13
|
+
|
14
|
+
|
15
|
+
def bind_github(
|
16
|
+
*,
|
17
|
+
cache_dir: ta.Optional[str] = None,
|
18
|
+
) -> InjectorBindings:
|
19
|
+
lst: ta.List[InjectorBindingOrBindings] = []
|
20
|
+
|
21
|
+
if cache_dir is not None:
|
22
|
+
lst.extend([
|
23
|
+
inj.bind(GithubFileCache.Config(
|
24
|
+
dir=cache_dir,
|
25
|
+
)),
|
26
|
+
inj.bind(GithubFileCache, singleton=True),
|
27
|
+
inj.bind(FileCache, to_key=GithubFileCache),
|
28
|
+
])
|
29
|
+
|
30
|
+
return inj.as_bindings(*lst)
|
omdev/ci/inject.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from omlish.lite.inject import InjectorBindingOrBindings
|
5
|
+
from omlish.lite.inject import InjectorBindings
|
6
|
+
from omlish.lite.inject import inj
|
7
|
+
|
8
|
+
from .cache import DirectoryFileCache
|
9
|
+
from .cache import FileCache
|
10
|
+
from .ci import Ci
|
11
|
+
from .docker.buildcaching import DockerBuildCachingImpl
|
12
|
+
from .docker.imagepulling import DockerImagePullingImpl
|
13
|
+
from .docker.inject import bind_docker
|
14
|
+
from .github.inject import bind_github
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
|
19
|
+
|
20
|
+
def bind_ci(
|
21
|
+
*,
|
22
|
+
config: Ci.Config,
|
23
|
+
|
24
|
+
github: bool = False,
|
25
|
+
|
26
|
+
cache_dir: ta.Optional[str] = None,
|
27
|
+
) -> InjectorBindings:
|
28
|
+
lst: ta.List[InjectorBindingOrBindings] = [ # noqa
|
29
|
+
inj.bind(config),
|
30
|
+
inj.bind(Ci, singleton=True),
|
31
|
+
]
|
32
|
+
|
33
|
+
lst.append(bind_docker(
|
34
|
+
build_caching_config=DockerBuildCachingImpl.Config(
|
35
|
+
service=config.service,
|
36
|
+
|
37
|
+
always_build=config.always_build,
|
38
|
+
),
|
39
|
+
|
40
|
+
image_pulling_config=DockerImagePullingImpl.Config(
|
41
|
+
always_pull=config.always_pull,
|
42
|
+
),
|
43
|
+
))
|
44
|
+
|
45
|
+
if cache_dir is not None:
|
46
|
+
if github:
|
47
|
+
lst.append(bind_github(
|
48
|
+
cache_dir=cache_dir,
|
49
|
+
))
|
50
|
+
|
51
|
+
else:
|
52
|
+
lst.extend([
|
53
|
+
inj.bind(DirectoryFileCache.Config(
|
54
|
+
dir=cache_dir,
|
55
|
+
)),
|
56
|
+
inj.bind(DirectoryFileCache, singleton=True),
|
57
|
+
inj.bind(FileCache, to_key=DirectoryFileCache),
|
58
|
+
|
59
|
+
])
|
60
|
+
|
61
|
+
return inj.as_bindings(*lst)
|
@@ -0,0 +1 @@
|
|
1
|
+
# @omlish-lite
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
import http.client
|
5
|
+
import io
|
6
|
+
import os
|
7
|
+
import typing as ta
|
8
|
+
import urllib.request
|
9
|
+
|
10
|
+
from omlish.lite.check import check
|
11
|
+
|
12
|
+
from .targets import BytesDataServerTarget
|
13
|
+
from .targets import DataServerTarget
|
14
|
+
from .targets import FileDataServerTarget
|
15
|
+
from .targets import UrlDataServerTarget
|
16
|
+
|
17
|
+
|
18
|
+
DataServerTargetT = ta.TypeVar('DataServerTargetT', bound='DataServerTarget')
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
|
23
|
+
|
24
|
+
@dc.dataclass(frozen=True)
|
25
|
+
class DataServerRequest:
|
26
|
+
method: str
|
27
|
+
path: str
|
28
|
+
|
29
|
+
|
30
|
+
@dc.dataclass(frozen=True)
|
31
|
+
class DataServerResponse:
|
32
|
+
status: int
|
33
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None
|
34
|
+
body: ta.Optional[io.IOBase] = None
|
35
|
+
|
36
|
+
#
|
37
|
+
|
38
|
+
def close(self) -> None:
|
39
|
+
if (body := self.body) is not None:
|
40
|
+
body.close()
|
41
|
+
|
42
|
+
def __enter__(self):
|
43
|
+
return self
|
44
|
+
|
45
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
46
|
+
self.close()
|
47
|
+
|
48
|
+
|
49
|
+
class DataServerError(Exception):
|
50
|
+
pass
|
51
|
+
|
52
|
+
|
53
|
+
class DataServerHandler(abc.ABC):
|
54
|
+
@abc.abstractmethod
|
55
|
+
def handle(self, req: DataServerRequest) -> DataServerResponse:
|
56
|
+
raise NotImplementedError
|
57
|
+
|
58
|
+
|
59
|
+
##
|
60
|
+
|
61
|
+
|
62
|
+
class DataServerTargetHandler(DataServerHandler, abc.ABC, ta.Generic[DataServerTargetT]):
|
63
|
+
def __init__(self, target: DataServerTargetT) -> None:
|
64
|
+
super().__init__()
|
65
|
+
|
66
|
+
self._target = target
|
67
|
+
|
68
|
+
#
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def for_target(cls, tgt: DataServerTarget, **kwargs: ta.Any) -> 'DataServerTargetHandler':
|
72
|
+
try:
|
73
|
+
hc = _DATA_SERVER_TARGET_HANDLERS[type(tgt)]
|
74
|
+
except KeyError:
|
75
|
+
raise TypeError(tgt) # noqa
|
76
|
+
else:
|
77
|
+
return hc(tgt, **kwargs)
|
78
|
+
|
79
|
+
#
|
80
|
+
|
81
|
+
def _make_headers(self) -> ta.Dict[str, str]:
|
82
|
+
dct = {}
|
83
|
+
if (ct := self._target.content_type) is not None:
|
84
|
+
dct['Content-Type'] = ct
|
85
|
+
if (cl := self._target.content_length) is not None:
|
86
|
+
dct['Content-Length'] = str(cl)
|
87
|
+
return dct
|
88
|
+
|
89
|
+
|
90
|
+
#
|
91
|
+
|
92
|
+
|
93
|
+
_DATA_SERVER_TARGET_HANDLERS: ta.Dict[ta.Type[DataServerTarget], ta.Type[DataServerTargetHandler]] = {}
|
94
|
+
|
95
|
+
|
96
|
+
def _register_data_server_target_handler(*tcs):
|
97
|
+
def inner(hc):
|
98
|
+
check.issubclass(hc, DataServerTargetHandler)
|
99
|
+
for tc in tcs:
|
100
|
+
check.issubclass(tc, DataServerTarget)
|
101
|
+
check.not_in(tc, _DATA_SERVER_TARGET_HANDLERS)
|
102
|
+
_DATA_SERVER_TARGET_HANDLERS[tc] = hc
|
103
|
+
return hc
|
104
|
+
return inner
|
105
|
+
|
106
|
+
|
107
|
+
#
|
108
|
+
|
109
|
+
|
110
|
+
@_register_data_server_target_handler(BytesDataServerTarget)
|
111
|
+
class BytesDataServerTargetHandler(DataServerTargetHandler[BytesDataServerTarget]):
|
112
|
+
def _make_headers(self) -> ta.Dict[str, str]:
|
113
|
+
dct = super()._make_headers()
|
114
|
+
if 'Content-Length' not in dct and self._target.data is not None:
|
115
|
+
dct['Content-Length'] = str(len(self._target.data))
|
116
|
+
return dct
|
117
|
+
|
118
|
+
def handle(self, req: DataServerRequest) -> DataServerResponse:
|
119
|
+
if req.method not in ('GET', 'HEAD'):
|
120
|
+
return DataServerResponse(http.HTTPStatus.METHOD_NOT_ALLOWED)
|
121
|
+
|
122
|
+
return DataServerResponse(
|
123
|
+
http.HTTPStatus.OK,
|
124
|
+
headers=self._make_headers(),
|
125
|
+
body=io.BytesIO(self._target.data) if self._target.data is not None and req.method == 'GET' else None,
|
126
|
+
)
|
127
|
+
|
128
|
+
|
129
|
+
#
|
130
|
+
|
131
|
+
|
132
|
+
@_register_data_server_target_handler(FileDataServerTarget)
|
133
|
+
class FileDataServerTargetHandler(DataServerTargetHandler[FileDataServerTarget]):
|
134
|
+
def handle(self, req: DataServerRequest) -> DataServerResponse:
|
135
|
+
if req.method == 'HEAD':
|
136
|
+
try:
|
137
|
+
st = os.stat(check.not_none(self._target.file_path))
|
138
|
+
except FileNotFoundError:
|
139
|
+
return DataServerResponse(http.HTTPStatus.NOT_FOUND)
|
140
|
+
|
141
|
+
return DataServerResponse(
|
142
|
+
http.HTTPStatus.OK,
|
143
|
+
headers={
|
144
|
+
'Content-Length': str(st.st_size),
|
145
|
+
**self._make_headers(),
|
146
|
+
},
|
147
|
+
)
|
148
|
+
|
149
|
+
elif req.method == 'GET':
|
150
|
+
try:
|
151
|
+
f = open(check.not_none(self._target.file_path), 'rb') # noqa
|
152
|
+
except FileNotFoundError:
|
153
|
+
return DataServerResponse(http.HTTPStatus.NOT_FOUND)
|
154
|
+
|
155
|
+
try:
|
156
|
+
sz = os.fstat(f.fileno())
|
157
|
+
|
158
|
+
return DataServerResponse(
|
159
|
+
http.HTTPStatus.OK,
|
160
|
+
headers={
|
161
|
+
'Content-Length': str(sz.st_size),
|
162
|
+
**self._make_headers(),
|
163
|
+
},
|
164
|
+
body=f, # noqa
|
165
|
+
)
|
166
|
+
|
167
|
+
except Exception: # noqa
|
168
|
+
f.close()
|
169
|
+
raise
|
170
|
+
|
171
|
+
else:
|
172
|
+
return DataServerResponse(http.HTTPStatus.METHOD_NOT_ALLOWED)
|
173
|
+
|
174
|
+
|
175
|
+
#
|
176
|
+
|
177
|
+
|
178
|
+
@_register_data_server_target_handler(UrlDataServerTarget)
|
179
|
+
class UrlDataServerTargetHandler(DataServerTargetHandler[UrlDataServerTarget]):
|
180
|
+
def handle(self, req: DataServerRequest) -> DataServerResponse:
|
181
|
+
if req.method not in check.not_none(self._target.methods):
|
182
|
+
return DataServerResponse(http.HTTPStatus.METHOD_NOT_ALLOWED)
|
183
|
+
|
184
|
+
resp: http.client.HTTPResponse = urllib.request.urlopen(urllib.request.Request( # noqa
|
185
|
+
method=req.method,
|
186
|
+
url=check.not_none(self._target.url),
|
187
|
+
))
|
188
|
+
|
189
|
+
try:
|
190
|
+
return DataServerResponse(
|
191
|
+
resp.status,
|
192
|
+
headers=dict(resp.headers.items()),
|
193
|
+
body=resp,
|
194
|
+
)
|
195
|
+
|
196
|
+
except Exception: # noqa
|
197
|
+
resp.close()
|
198
|
+
raise
|
omdev/dataserver/http.py
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- asyncio
|
5
|
+
- chunked transfer - both output and urllib input
|
6
|
+
"""
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from omlish.http.handlers import HttpHandler_
|
10
|
+
from omlish.http.handlers import HttpHandlerRequest
|
11
|
+
from omlish.http.handlers import HttpHandlerResponse
|
12
|
+
from omlish.http.handlers import HttpHandlerResponseStreamedData
|
13
|
+
|
14
|
+
from .handlers import DataServerRequest
|
15
|
+
from .server import DataServer
|
16
|
+
|
17
|
+
|
18
|
+
##
|
19
|
+
|
20
|
+
|
21
|
+
class DataServerHttpHandler(HttpHandler_):
|
22
|
+
DEFAULT_READ_CHUNK_SIZE = 0x10000
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
ps: DataServer,
|
27
|
+
*,
|
28
|
+
read_chunk_size: int = DEFAULT_READ_CHUNK_SIZE,
|
29
|
+
) -> None:
|
30
|
+
super().__init__()
|
31
|
+
|
32
|
+
self._ps = ps
|
33
|
+
self._read_chunk_size = read_chunk_size
|
34
|
+
|
35
|
+
def __call__(self, req: HttpHandlerRequest) -> HttpHandlerResponse:
|
36
|
+
p_req = DataServerRequest(
|
37
|
+
req.method,
|
38
|
+
req.path,
|
39
|
+
)
|
40
|
+
|
41
|
+
p_resp = self._ps.handle(p_req)
|
42
|
+
try:
|
43
|
+
data: ta.Any
|
44
|
+
if (p_body := p_resp.body) is not None:
|
45
|
+
def stream_data():
|
46
|
+
try:
|
47
|
+
while (b := p_body.read(self._read_chunk_size)):
|
48
|
+
yield b
|
49
|
+
finally:
|
50
|
+
p_body.close()
|
51
|
+
|
52
|
+
data = HttpHandlerResponseStreamedData(stream_data())
|
53
|
+
|
54
|
+
else:
|
55
|
+
data = None
|
56
|
+
|
57
|
+
resp = HttpHandlerResponse(
|
58
|
+
status=p_resp.status,
|
59
|
+
headers=p_resp.headers,
|
60
|
+
data=data,
|
61
|
+
close_connection=True,
|
62
|
+
)
|
63
|
+
|
64
|
+
return resp
|
65
|
+
|
66
|
+
except Exception: # noqa
|
67
|
+
p_resp.close()
|
68
|
+
|
69
|
+
raise
|
@@ -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)
|