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.
Files changed (40) hide show
  1. omdev/ci/cache.py +40 -23
  2. omdev/ci/ci.py +49 -109
  3. omdev/ci/cli.py +24 -23
  4. omdev/ci/docker/__init__.py +0 -0
  5. omdev/ci/docker/buildcaching.py +69 -0
  6. omdev/ci/docker/cache.py +57 -0
  7. omdev/ci/{docker.py → docker/cmds.py} +1 -44
  8. omdev/ci/docker/imagepulling.py +64 -0
  9. omdev/ci/docker/inject.py +37 -0
  10. omdev/ci/docker/utils.py +48 -0
  11. omdev/ci/github/cache.py +15 -5
  12. omdev/ci/github/inject.py +30 -0
  13. omdev/ci/inject.py +61 -0
  14. omdev/dataserver/__init__.py +1 -0
  15. omdev/dataserver/handlers.py +198 -0
  16. omdev/dataserver/http.py +69 -0
  17. omdev/dataserver/routes.py +49 -0
  18. omdev/dataserver/server.py +90 -0
  19. omdev/dataserver/targets.py +89 -0
  20. omdev/oci/__init__.py +0 -0
  21. omdev/oci/building.py +221 -0
  22. omdev/oci/compression.py +8 -0
  23. omdev/oci/data.py +151 -0
  24. omdev/oci/datarefs.py +138 -0
  25. omdev/oci/dataserver.py +61 -0
  26. omdev/oci/loading.py +142 -0
  27. omdev/oci/media.py +179 -0
  28. omdev/oci/packing.py +381 -0
  29. omdev/oci/repositories.py +159 -0
  30. omdev/oci/tars.py +144 -0
  31. omdev/pyproject/resources/python.sh +1 -1
  32. omdev/scripts/ci.py +1841 -384
  33. omdev/scripts/interp.py +100 -22
  34. omdev/scripts/pyproject.py +122 -28
  35. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/METADATA +2 -2
  36. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/RECORD +40 -15
  37. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/LICENSE +0 -0
  38. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/WHEEL +0 -0
  39. {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/entry_points.txt +0 -0
  40. {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)
@@ -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
- dir: str, # noqa
26
+ config: Config,
21
27
  *,
22
28
  client: ta.Optional[GithubCacheClient] = None,
23
- **kwargs: ta.Any,
29
+ version: ta.Optional[CacheVersion] = None,
24
30
  ) -> None:
25
- super().__init__(**kwargs)
31
+ super().__init__(
32
+ version=version,
33
+ )
26
34
 
27
- self._dir = check.not_none(dir)
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
- self._dir,
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
@@ -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)