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.
- omserv-0.0.0.dev7/LICENSE +21 -0
- omserv-0.0.0.dev7/MANIFEST.in +1 -0
- omserv-0.0.0.dev7/PKG-INFO +20 -0
- omserv-0.0.0.dev7/README.rst +1 -0
- omserv-0.0.0.dev7/omserv/__about__.py +28 -0
- omserv-0.0.0.dev7/omserv/__init__.py +0 -0
- omserv-0.0.0.dev7/omserv/apps/__init__.py +0 -0
- omserv-0.0.0.dev7/omserv/apps/base.py +23 -0
- omserv-0.0.0.dev7/omserv/apps/inject.py +89 -0
- omserv-0.0.0.dev7/omserv/apps/markers.py +41 -0
- omserv-0.0.0.dev7/omserv/apps/routes.py +139 -0
- omserv-0.0.0.dev7/omserv/apps/sessions.py +57 -0
- omserv-0.0.0.dev7/omserv/apps/templates.py +90 -0
- omserv-0.0.0.dev7/omserv/dbs.py +24 -0
- omserv-0.0.0.dev7/omserv/node/__init__.py +0 -0
- omserv-0.0.0.dev7/omserv/node/models.py +53 -0
- omserv-0.0.0.dev7/omserv/node/registry.py +124 -0
- omserv-0.0.0.dev7/omserv/node/sql.py +131 -0
- omserv-0.0.0.dev7/omserv/secrets.py +12 -0
- omserv-0.0.0.dev7/omserv/server/__init__.py +18 -0
- omserv-0.0.0.dev7/omserv/server/config.py +51 -0
- omserv-0.0.0.dev7/omserv/server/debug.py +14 -0
- omserv-0.0.0.dev7/omserv/server/events.py +83 -0
- omserv-0.0.0.dev7/omserv/server/headers.py +36 -0
- omserv-0.0.0.dev7/omserv/server/lifespans.py +132 -0
- omserv-0.0.0.dev7/omserv/server/multiprocess.py +157 -0
- omserv-0.0.0.dev7/omserv/server/protocols/__init__.py +1 -0
- omserv-0.0.0.dev7/omserv/server/protocols/h11.py +334 -0
- omserv-0.0.0.dev7/omserv/server/protocols/h2.py +407 -0
- omserv-0.0.0.dev7/omserv/server/protocols/protocols.py +91 -0
- omserv-0.0.0.dev7/omserv/server/protocols/types.py +18 -0
- omserv-0.0.0.dev7/omserv/server/resources/__init__.py +8 -0
- omserv-0.0.0.dev7/omserv/server/sockets.py +111 -0
- omserv-0.0.0.dev7/omserv/server/ssl.py +47 -0
- omserv-0.0.0.dev7/omserv/server/streams/__init__.py +0 -0
- omserv-0.0.0.dev7/omserv/server/streams/httpstream.py +237 -0
- omserv-0.0.0.dev7/omserv/server/streams/utils.py +53 -0
- omserv-0.0.0.dev7/omserv/server/streams/wsstream.py +447 -0
- omserv-0.0.0.dev7/omserv/server/taskspawner.py +111 -0
- omserv-0.0.0.dev7/omserv/server/tcpserver.py +173 -0
- omserv-0.0.0.dev7/omserv/server/types.py +94 -0
- omserv-0.0.0.dev7/omserv/server/workercontext.py +52 -0
- omserv-0.0.0.dev7/omserv/server/workers.py +193 -0
- omserv-0.0.0.dev7/omserv.egg-info/PKG-INFO +20 -0
- omserv-0.0.0.dev7/omserv.egg-info/SOURCES.txt +48 -0
- omserv-0.0.0.dev7/omserv.egg-info/dependency_links.txt +1 -0
- omserv-0.0.0.dev7/omserv.egg-info/requires.txt +7 -0
- omserv-0.0.0.dev7/omserv.egg-info/top_level.txt +1 -0
- omserv-0.0.0.dev7/pyproject.toml +47 -0
- 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)
|