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.
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)