omdev 0.0.0.dev221__py3-none-any.whl → 0.0.0.dev223__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|