omserv 0.0.0.dev7__tar.gz

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 (50) hide show
  1. omserv-0.0.0.dev7/LICENSE +21 -0
  2. omserv-0.0.0.dev7/MANIFEST.in +1 -0
  3. omserv-0.0.0.dev7/PKG-INFO +20 -0
  4. omserv-0.0.0.dev7/README.rst +1 -0
  5. omserv-0.0.0.dev7/omserv/__about__.py +28 -0
  6. omserv-0.0.0.dev7/omserv/__init__.py +0 -0
  7. omserv-0.0.0.dev7/omserv/apps/__init__.py +0 -0
  8. omserv-0.0.0.dev7/omserv/apps/base.py +23 -0
  9. omserv-0.0.0.dev7/omserv/apps/inject.py +89 -0
  10. omserv-0.0.0.dev7/omserv/apps/markers.py +41 -0
  11. omserv-0.0.0.dev7/omserv/apps/routes.py +139 -0
  12. omserv-0.0.0.dev7/omserv/apps/sessions.py +57 -0
  13. omserv-0.0.0.dev7/omserv/apps/templates.py +90 -0
  14. omserv-0.0.0.dev7/omserv/dbs.py +24 -0
  15. omserv-0.0.0.dev7/omserv/node/__init__.py +0 -0
  16. omserv-0.0.0.dev7/omserv/node/models.py +53 -0
  17. omserv-0.0.0.dev7/omserv/node/registry.py +124 -0
  18. omserv-0.0.0.dev7/omserv/node/sql.py +131 -0
  19. omserv-0.0.0.dev7/omserv/secrets.py +12 -0
  20. omserv-0.0.0.dev7/omserv/server/__init__.py +18 -0
  21. omserv-0.0.0.dev7/omserv/server/config.py +51 -0
  22. omserv-0.0.0.dev7/omserv/server/debug.py +14 -0
  23. omserv-0.0.0.dev7/omserv/server/events.py +83 -0
  24. omserv-0.0.0.dev7/omserv/server/headers.py +36 -0
  25. omserv-0.0.0.dev7/omserv/server/lifespans.py +132 -0
  26. omserv-0.0.0.dev7/omserv/server/multiprocess.py +157 -0
  27. omserv-0.0.0.dev7/omserv/server/protocols/__init__.py +1 -0
  28. omserv-0.0.0.dev7/omserv/server/protocols/h11.py +334 -0
  29. omserv-0.0.0.dev7/omserv/server/protocols/h2.py +407 -0
  30. omserv-0.0.0.dev7/omserv/server/protocols/protocols.py +91 -0
  31. omserv-0.0.0.dev7/omserv/server/protocols/types.py +18 -0
  32. omserv-0.0.0.dev7/omserv/server/resources/__init__.py +8 -0
  33. omserv-0.0.0.dev7/omserv/server/sockets.py +111 -0
  34. omserv-0.0.0.dev7/omserv/server/ssl.py +47 -0
  35. omserv-0.0.0.dev7/omserv/server/streams/__init__.py +0 -0
  36. omserv-0.0.0.dev7/omserv/server/streams/httpstream.py +237 -0
  37. omserv-0.0.0.dev7/omserv/server/streams/utils.py +53 -0
  38. omserv-0.0.0.dev7/omserv/server/streams/wsstream.py +447 -0
  39. omserv-0.0.0.dev7/omserv/server/taskspawner.py +111 -0
  40. omserv-0.0.0.dev7/omserv/server/tcpserver.py +173 -0
  41. omserv-0.0.0.dev7/omserv/server/types.py +94 -0
  42. omserv-0.0.0.dev7/omserv/server/workercontext.py +52 -0
  43. omserv-0.0.0.dev7/omserv/server/workers.py +193 -0
  44. omserv-0.0.0.dev7/omserv.egg-info/PKG-INFO +20 -0
  45. omserv-0.0.0.dev7/omserv.egg-info/SOURCES.txt +48 -0
  46. omserv-0.0.0.dev7/omserv.egg-info/dependency_links.txt +1 -0
  47. omserv-0.0.0.dev7/omserv.egg-info/requires.txt +7 -0
  48. omserv-0.0.0.dev7/omserv.egg-info/top_level.txt +1 -0
  49. omserv-0.0.0.dev7/pyproject.toml +47 -0
  50. omserv-0.0.0.dev7/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ Copyright 2023- wrmsr
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
+ following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
+ disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
10
+ disclaimer in the documentation and/or other materials provided with the distribution.
11
+
12
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
21
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ global-exclude **/conftest.py
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: omserv
3
+ Version: 0.0.0.dev7
4
+ Summary: omserv
5
+ Author: wrmsr
6
+ License: BSD-3-Clause
7
+ Project-URL: source, https://github.com/wrmsr/omlish
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Operating System :: POSIX
13
+ Requires-Python: >=3.12
14
+ License-File: LICENSE
15
+ Requires-Dist: omlish==0.0.0.dev7
16
+ Provides-Extra: server
17
+ Requires-Dist: h11>=0.14; extra == "server"
18
+ Requires-Dist: h2>=4.1; extra == "server"
19
+ Requires-Dist: priority>=2; extra == "server"
20
+ Requires-Dist: wsproto>=1.2; extra == "server"
@@ -0,0 +1 @@
1
+ *omlish*
@@ -0,0 +1,28 @@
1
+ from omlish.__about__ import ProjectBase
2
+ from omlish.__about__ import SetuptoolsBase
3
+ from omlish.__about__ import __version__
4
+
5
+
6
+ class Project(ProjectBase):
7
+ name = 'omserv'
8
+ description = 'omserv'
9
+
10
+ dependencies = [
11
+ f'omlish == {__version__}',
12
+ ]
13
+
14
+ optional_dependencies = {
15
+ 'server': [
16
+ 'h11 >= 0.14',
17
+ 'h2 >= 4.1',
18
+ 'priority >= 2',
19
+ 'wsproto >= 1.2',
20
+ ],
21
+ }
22
+
23
+
24
+ class Setuptools(SetuptoolsBase):
25
+ find_packages = {
26
+ 'include': ['omserv', 'omserv.*'],
27
+ 'exclude': [*SetuptoolsBase.find_packages['exclude']],
28
+ }
File without changes
File without changes
@@ -0,0 +1,23 @@
1
+ import contextvars
2
+ import typing as ta
3
+
4
+ from omlish.http.asgi import AsgiScope
5
+
6
+
7
+ ##
8
+
9
+
10
+ SCOPE: contextvars.ContextVar[AsgiScope] = contextvars.ContextVar('scope')
11
+
12
+
13
+ ##
14
+
15
+
16
+ BaseServerUrl = ta.NewType('BaseServerUrl', str)
17
+
18
+
19
+ BASE_SERVER_URL: contextvars.ContextVar[BaseServerUrl] = contextvars.ContextVar('base_server_url')
20
+
21
+
22
+ def url_for(s: str) -> str:
23
+ return BASE_SERVER_URL.get() + s
@@ -0,0 +1,89 @@
1
+ import typing as ta
2
+
3
+ from omlish import inject as inj
4
+ from omlish.http import sessions
5
+ from omlish.http.asgi import AsgiApp
6
+ from omlish.http.asgi import AsgiScope
7
+
8
+ from .base import SCOPE
9
+ from .markers import AppMarker
10
+ from .markers import AppMarkerProcessor
11
+ from .markers import NopAppMarkerProcessor
12
+ from .markers import get_app_markers
13
+ from .routes import Handler_
14
+ from .routes import Route
15
+ from .routes import _HandlesAppMarker
16
+ from .sessions import SESSION
17
+ from .sessions import _WithSessionAppMarker
18
+ from .sessions import _WithSessionAppMarkerProcessor
19
+ from .templates import J2Namespace
20
+ from .templates import J2Templates
21
+
22
+
23
+ def bind_handler(hc: type[Handler_]) -> inj.Elemental:
24
+ return inj.as_elements(
25
+ inj.bind(hc, singleton=True),
26
+ inj.set_binder[Handler_]().bind(hc),
27
+ )
28
+
29
+
30
+ def bind_app_marker_processor(mc: type[AppMarker], pc: type[AppMarkerProcessor]) -> inj.Elemental:
31
+ return inj.as_elements(
32
+ inj.bind(pc),
33
+ inj.map_binder[type[AppMarker], AppMarkerProcessor]().bind(mc, pc),
34
+ )
35
+
36
+
37
+ def _build_route_handler_map(
38
+ handlers: ta.AbstractSet[Handler_],
39
+ processors: ta.Mapping[type[AppMarker], AppMarkerProcessor],
40
+ ) -> ta.Mapping[Route, AsgiApp]:
41
+ route_handlers: dict[Route, AsgiApp] = {}
42
+ for h in handlers:
43
+ for rh in h.get_route_handlers():
44
+ app = rh.handler
45
+ markers = get_app_markers(rh.handler)
46
+ for m in markers:
47
+ mp = processors[type(m)]
48
+ if mp is not None:
49
+ app = mp(app)
50
+ route_handlers[rh.route] = app
51
+ return route_handlers
52
+
53
+
54
+ def bind_route_handler_map() -> inj.Elemental:
55
+ return inj.as_elements(
56
+ inj.bind(_build_route_handler_map, singleton=True),
57
+ inj.map_binder[type[AppMarker], AppMarkerProcessor](),
58
+ )
59
+
60
+
61
+ def bind() -> inj.Elemental:
62
+ return inj.as_elements(
63
+ inj.bind(ta.Callable[[], AsgiScope], to_const=SCOPE.get),
64
+ inj.bind(ta.Callable[[], sessions.Session], to_const=SESSION.get),
65
+
66
+ ##
67
+
68
+ inj.map_binder[type[AppMarker], AppMarkerProcessor](),
69
+
70
+ bind_app_marker_processor(_WithSessionAppMarker, _WithSessionAppMarkerProcessor),
71
+ bind_app_marker_processor(_HandlesAppMarker, NopAppMarkerProcessor),
72
+
73
+ ##
74
+
75
+ inj.set_binder[Handler_](),
76
+ )
77
+
78
+
79
+ def _build_j2_namespaces(ns: ta.Annotated[ta.Mapping[str, ta.Any], inj.Tag(J2Namespace)]) -> J2Namespace:
80
+ return J2Namespace(ns)
81
+
82
+
83
+ def bind_templates() -> inj.Elemental:
84
+ return inj.as_elements(
85
+ inj.bind(J2Templates, singleton=True),
86
+
87
+ inj.map_binder[str, ta.Any](tag=J2Namespace),
88
+ inj.bind(_build_j2_namespaces),
89
+ )
@@ -0,0 +1,41 @@
1
+ import abc
2
+ import typing as ta
3
+
4
+ from omlish import lang
5
+ from omlish.http.asgi import AsgiApp
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+
10
+
11
+ class AppMarker(lang.Abstract):
12
+ pass
13
+
14
+
15
+ APP_MARKERS_ATTR = '__app_markers__'
16
+
17
+
18
+ def append_app_marker(obj: T, *markers: AppMarker) -> T:
19
+ tgt = lang.unwrap_func(obj) # type: ignore
20
+ tgt.__dict__.setdefault(APP_MARKERS_ATTR, []).extend(markers)
21
+ return obj
22
+
23
+
24
+ def get_app_markers(obj: ta.Any) -> ta.Sequence[AppMarker]:
25
+ tgt = lang.unwrap_func(obj)
26
+ try:
27
+ dct = tgt.__dict__
28
+ except AttributeError:
29
+ return ()
30
+ return dct.get(APP_MARKERS_ATTR, ())
31
+
32
+
33
+ class AppMarkerProcessor(lang.Abstract):
34
+ @abc.abstractmethod
35
+ def __call__(self, app: AsgiApp) -> AsgiApp:
36
+ return app
37
+
38
+
39
+ class NopAppMarkerProcessor(AppMarkerProcessor, lang.Final):
40
+ def __call__(self, app: AsgiApp) -> AsgiApp:
41
+ return app
@@ -0,0 +1,139 @@
1
+ import contextlib
2
+ import dataclasses as dc
3
+ import logging
4
+ import typing as ta
5
+
6
+ from omlish import check
7
+ from omlish import lang
8
+ from omlish.http.asgi import AsgiApp
9
+ from omlish.http.asgi import AsgiApp_
10
+ from omlish.http.asgi import AsgiRecv
11
+ from omlish.http.asgi import AsgiScope
12
+ from omlish.http.asgi import AsgiSend
13
+ from omlish.http.asgi import send_response
14
+ from omlish.http.asgi import stub_lifespan
15
+
16
+ from .base import BASE_SERVER_URL
17
+ from .base import SCOPE
18
+ from .base import BaseServerUrl
19
+ from .markers import AppMarker
20
+ from .markers import append_app_marker
21
+ from .markers import get_app_markers
22
+
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ ##
28
+
29
+
30
+ class Route(ta.NamedTuple):
31
+ method: str
32
+ path: str
33
+
34
+ @classmethod
35
+ def get(cls, path: str) -> 'Route':
36
+ return cls('GET', path)
37
+
38
+ @classmethod
39
+ def post(cls, path: str) -> 'Route':
40
+ return cls('POST', path)
41
+
42
+ @classmethod
43
+ def put(cls, path: str) -> 'Route':
44
+ return cls('PUT', path)
45
+
46
+ @classmethod
47
+ def delete(cls, path: str) -> 'Route':
48
+ return cls('DELETE', path)
49
+
50
+
51
+ class RouteHandler(ta.NamedTuple):
52
+ route: Route
53
+ handler: AsgiApp
54
+
55
+
56
+ @dc.dataclass(frozen=True)
57
+ class _HandlesAppMarker(AppMarker, lang.Final):
58
+ routes: ta.Sequence[Route]
59
+
60
+
61
+ def handles(*routes: Route):
62
+ def inner(fn):
63
+ append_app_marker(fn, _HandlesAppMarker(routes))
64
+ return fn
65
+
66
+ routes = tuple(map(check.of_isinstance(Route), routes))
67
+ return inner
68
+
69
+
70
+ ##
71
+
72
+
73
+ class Handler_(lang.Abstract): # noqa
74
+ def get_route_handlers(self) -> ta.Iterable[RouteHandler]:
75
+ return get_marked_route_handlers(self)
76
+
77
+
78
+ def get_marked_route_handlers(h: Handler_) -> ta.Sequence[RouteHandler]:
79
+ ret: list[RouteHandler] = []
80
+
81
+ cdct: dict[str, ta.Any] = {}
82
+ for mcls in reversed(type(h).__mro__):
83
+ cdct.update(**mcls.__dict__)
84
+
85
+ for att, obj in cdct.items():
86
+ if not (mks := get_app_markers(obj)):
87
+ continue
88
+ if not (hms := [m for m in mks if isinstance(m, _HandlesAppMarker)]):
89
+ continue
90
+ if not (rs := [r for hm in hms for r in hm.routes]):
91
+ continue
92
+
93
+ app = getattr(h, att)
94
+ ret.extend(RouteHandler(r, app) for r in rs)
95
+
96
+ return ret
97
+
98
+
99
+ ##
100
+
101
+
102
+ @dc.dataclass(frozen=True)
103
+ class RouteHandlerApp(AsgiApp_):
104
+ route_handlers: ta.Mapping[Route, AsgiApp]
105
+ base_server_url: BaseServerUrl | None = None
106
+
107
+ async def __call__(self, scope: AsgiScope, recv: AsgiRecv, send: AsgiSend) -> None:
108
+ with contextlib.ExitStack() as es:
109
+ es.enter_context(lang.context_var_setting(SCOPE, scope))
110
+
111
+ match scope_ty := scope['type']:
112
+ case 'lifespan':
113
+ await stub_lifespan(scope, recv, send)
114
+ return
115
+
116
+ case 'http':
117
+ if self.base_server_url is not None:
118
+ bsu = self.base_server_url
119
+ else:
120
+ sch = scope['scheme']
121
+ h, p = scope['server']
122
+ if (sch, p) not in (('http', 80), ('https', 443)):
123
+ ps = f':{p}'
124
+ else:
125
+ ps = ''
126
+ bsu = BaseServerUrl(f'{sch}://{h}{ps}/')
127
+ es.enter_context(lang.context_var_setting(BASE_SERVER_URL, bsu))
128
+
129
+ route = Route(scope['method'], scope['raw_path'].decode())
130
+ handler = self.route_handlers.get(route)
131
+
132
+ if handler is not None:
133
+ await handler(scope, recv, send)
134
+
135
+ else:
136
+ await send_response(send, 404)
137
+
138
+ case _:
139
+ raise ValueError(f'Unhandled scope type: {scope_ty!r}')
@@ -0,0 +1,57 @@
1
+ import contextvars
2
+ import dataclasses as dc
3
+ import logging
4
+
5
+ from omlish import lang
6
+ from omlish.http import sessions
7
+ from omlish.http.asgi import AsgiApp
8
+ from omlish.http.asgi import AsgiRecv
9
+ from omlish.http.asgi import AsgiScope
10
+ from omlish.http.asgi import AsgiSend
11
+
12
+ from .markers import AppMarker
13
+ from .markers import AppMarkerProcessor
14
+ from .markers import append_app_marker
15
+
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ ##
21
+
22
+
23
+ SESSION: contextvars.ContextVar[sessions.Session] = contextvars.ContextVar('session')
24
+
25
+
26
+ class _WithSessionAppMarker(AppMarker, lang.Singleton, lang.Final):
27
+ pass
28
+
29
+
30
+ def with_session(fn):
31
+ return append_app_marker(fn, _WithSessionAppMarker())
32
+
33
+
34
+ @dc.dataclass(frozen=True)
35
+ class _WithSessionAppMarkerProcessor(AppMarkerProcessor):
36
+ _ss: sessions.CookieSessionStore
37
+
38
+ async def _wrap(self, fn: AsgiApp, scope: AsgiScope, recv: AsgiRecv, send: AsgiSend) -> None:
39
+ async def _send(obj):
40
+ if obj['type'] == 'http.response.start':
41
+ out_session = SESSION.get()
42
+ obj = {
43
+ **obj,
44
+ 'headers': [
45
+ *obj.get('headers', []),
46
+ *self._ss.build_headers(out_session),
47
+ ],
48
+ }
49
+
50
+ await send(obj)
51
+
52
+ in_session = self._ss.extract(scope)
53
+ with lang.context_var_setting(SESSION, in_session):
54
+ await fn(scope, recv, _send)
55
+
56
+ def __call__(self, app: AsgiApp) -> AsgiApp:
57
+ return lang.decorator(self._wrap)(app) # noqa
@@ -0,0 +1,90 @@
1
+ import dataclasses as dc
2
+ import importlib.resources
3
+ import logging
4
+ import typing as ta
5
+
6
+ import jinja2
7
+
8
+ from .base import url_for
9
+
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ ##
15
+
16
+
17
+ J2_DEFAULT_NAMESPACE = {}
18
+
19
+
20
+ def j2_helper(fn):
21
+ J2_DEFAULT_NAMESPACE[fn.__name__] = fn
22
+ return fn
23
+
24
+
25
+ j2_helper(url_for)
26
+
27
+
28
+ ##
29
+
30
+
31
+ J2Namespace = ta.NewType('J2Namespace', ta.Mapping[str, ta.Any])
32
+
33
+
34
+ class J2Templates:
35
+ @dc.dataclass(frozen=True)
36
+ class Config:
37
+ resource_root: str
38
+ reload: bool = False
39
+
40
+ def __init__(self, config: Config, ns: J2Namespace) -> None:
41
+ super().__init__()
42
+
43
+ self._config = config
44
+ self._ns = ns
45
+
46
+ self._env = jinja2.Environment(
47
+ loader=self._Loader(self),
48
+ autoescape=True,
49
+ )
50
+
51
+ self._all: ta.Mapping[str, jinja2.Template] | None = None
52
+
53
+ class _Loader(jinja2.BaseLoader):
54
+ def __init__(self, owner: 'J2Templates') -> None:
55
+ super().__init__()
56
+ self._owner = owner
57
+
58
+ def get_source(self, environment, template):
59
+ raise TypeError
60
+
61
+ def list_templates(self):
62
+ raise TypeError
63
+
64
+ def load(self, environment, name, globals=None): # noqa
65
+ return self._owner.load(name)
66
+
67
+ def _load_all(self) -> ta.Mapping[str, jinja2.Template]:
68
+ ret: dict[str, jinja2.Template] = {}
69
+ for fn in importlib.resources.files(self._config.resource_root).iterdir():
70
+ if fn.name.endswith('.j2'):
71
+ ret[fn.name] = self._env.from_string(fn.read_text())
72
+ return ret
73
+
74
+ def load_all(self) -> ta.Mapping[str, jinja2.Template]:
75
+ if self._config.reload:
76
+ return self._load_all()
77
+
78
+ if self._all is None:
79
+ self._all = self._load_all()
80
+ return self._all
81
+
82
+ def load(self, name: str) -> jinja2.Template:
83
+ return self.load_all()[name]
84
+
85
+ def render(self, template_name: str, **kwargs: ta.Any) -> bytes:
86
+ return self.load(template_name).render(**{
87
+ **J2_DEFAULT_NAMESPACE,
88
+ **self._ns,
89
+ **kwargs,
90
+ }).encode()
@@ -0,0 +1,24 @@
1
+ import os
2
+
3
+ from .secrets import load_secrets # noqa
4
+
5
+
6
+ def get_secret_db_url() -> str:
7
+ cfg = load_secrets()
8
+ return f'postgresql+asyncpg://{cfg["postgres_user"]}:{cfg["postgres_pass"]}@{cfg["postgres_host"]}:5432'
9
+
10
+
11
+ def get_docker_db_url() -> str:
12
+ from omlish import docker
13
+ cc = docker.ComposeConfig('omlish-', file_path='docker/compose.yml')
14
+ svc = cc.get_services()['postgres']
15
+ port = docker.get_compose_port(svc, 5432)
16
+ env = svc['environment']
17
+ return f'postgresql+asyncpg://{env["POSTGRES_USER"]}:{env["POSTGRES_PASSWORD"]}@127.0.0.1:{port}'
18
+
19
+
20
+ def get_db_url() -> str:
21
+ if os.environ.get('PROD', '').lower() in ('1', 'true'): # FIXME: lol
22
+ return get_secret_db_url()
23
+ else:
24
+ return get_docker_db_url()
File without changes
@@ -0,0 +1,53 @@
1
+ import typing as ta
2
+
3
+ import sqlalchemy as sa
4
+ import sqlalchemy.dialects.postgresql as sapg
5
+ import sqlalchemy.orm
6
+
7
+ from omlish import sql
8
+
9
+ from .sql import CREATE_UPDATED_AT_FUNCTION_STATEMENT
10
+ from .sql import IdMixin
11
+ from .sql import TimestampsMixin
12
+ from .sql import install_updated_at_trigger
13
+
14
+
15
+ ##
16
+
17
+
18
+ Metadata = sa.MetaData()
19
+ Base: ta.Any = sa.orm.declarative_base(metadata=Metadata)
20
+
21
+
22
+ class Node(
23
+ IdMixin,
24
+ TimestampsMixin,
25
+ Base,
26
+ ):
27
+ __tablename__ = 'nodes'
28
+ __table_args__ = (
29
+ sa.Index('nodes_by_uuid', 'uuid', unique=True),
30
+ )
31
+
32
+ uuid = sa.Column(sa.String(50), nullable=False, unique=True)
33
+ hostname = sa.Column(sa.String(100), nullable=False)
34
+
35
+ heartbeat_at = sa.Column(sa.TIMESTAMP(timezone=True))
36
+
37
+ extra = sa.Column(sapg.JSONB)
38
+
39
+
40
+ Nodes = Node.__table__
41
+
42
+
43
+ install_updated_at_trigger(Metadata, 'nodes')
44
+
45
+
46
+ async def setup_db(engine: sql.AsyncEngineLike, *, drop: bool = False) -> None:
47
+ conn: sql.AsyncConnection
48
+ async with sql.async_adapt(engine).connect() as conn:
49
+ async with conn.begin():
50
+ if drop:
51
+ await conn.run_sync(Metadata.drop_all)
52
+ await conn.execute(sa.text(CREATE_UPDATED_AT_FUNCTION_STATEMENT))
53
+ await conn.run_sync(Metadata.create_all)