genropy-asgi 0.2.0__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.
- genropy_asgi/__init__.py +19 -0
- genropy_asgi/proxy/__init__.py +17 -0
- genropy_asgi/proxy/genropy_proxy.py +102 -0
- genropy_asgi/proxy/py.typed +0 -0
- genropy_asgi/py.typed +0 -0
- genropy_asgi/siteregister/__init__.py +20 -0
- genropy_asgi/siteregister/exceptions.py +36 -0
- genropy_asgi/siteregister/handler.py +28 -0
- genropy_asgi/siteregister/processes.py +12 -0
- genropy_asgi/siteregister/service.py +28 -0
- genropy_asgi/siteregister/siteregister.py +37 -0
- genropy_asgi/siteregister/siteregister_client.py +1139 -0
- genropy_asgi/spa/__init__.py +14 -0
- genropy_asgi/spa/cli.py +108 -0
- genropy_asgi/spa/config.py +70 -0
- genropy_asgi/spa/genropy_commander_application.py +62 -0
- genropy_asgi/spa/genropy_spa_application.py +353 -0
- genropy_asgi/spa/genropy_worker_application.py +68 -0
- genropy_asgi/spa/py.typed +0 -0
- genropy_asgi-0.2.0.dist-info/METADATA +152 -0
- genropy_asgi-0.2.0.dist-info/RECORD +25 -0
- genropy_asgi-0.2.0.dist-info/WHEEL +4 -0
- genropy_asgi-0.2.0.dist-info/entry_points.txt +5 -0
- genropy_asgi-0.2.0.dist-info/licenses/LICENSE +201 -0
- genropy_asgi-0.2.0.dist-info/licenses/NOTICE +9 -0
genropy_asgi/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""genropy-asgi — the bridge between genro-asgi and legacy GenroPy.
|
|
5
|
+
|
|
6
|
+
The generic commander/worker model lives in genro-asgi core
|
|
7
|
+
(``genro_asgi.applications.multi_worker_application``); this package is the
|
|
8
|
+
GenroPy-specific bridge on top of it. Three submodules, the only ``gnr.*``-aware code:
|
|
9
|
+
|
|
10
|
+
- ``genropy_asgi.spa`` — the SPA bridge: ``GenropySpaApplication`` (single) and
|
|
11
|
+
``GenropyWorkerApplication`` (pool child) host a legacy ``GnrWsgiSite``, plus the
|
|
12
|
+
``gnrasgiserve`` CLI and the in-process register client.
|
|
13
|
+
- ``genropy_asgi.proxy`` — the OpenAPI bridge: a ``GnrApp`` behind an
|
|
14
|
+
``OpenApiApplication`` with thread-local db cleanup.
|
|
15
|
+
- ``genropy_asgi.siteregister`` — the daemonless register the legacy imports as
|
|
16
|
+
``gnr.web.daemon`` (entry-point ``gnr.web:daemon``), replacing the register daemon.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""genropy-proxy — GenroPy legacy db behind an OpenApiApplication (collaudo).
|
|
5
|
+
|
|
6
|
+
``GenropyProxyMixin`` hosts a ``GnrApp`` and closes its thread-local db
|
|
7
|
+
connection via the ``route_cleanup`` hook (run in the executor thread by
|
|
8
|
+
genro-asgi's ``make_callable``). ``GenropyProxyOpenApiApplication`` composes the
|
|
9
|
+
mixin with ``OpenApiApplication``. The only ``gnr.*``-aware piece; everything
|
|
10
|
+
REST/OpenAPI comes from genro-asgi.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .genropy_proxy import GenropyProxyMixin, GenropyProxyOpenApiApplication
|
|
15
|
+
|
|
16
|
+
__all__ = ["GenropyProxyMixin", "GenropyProxyOpenApiApplication"]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""GenropyProxyMixin — a GenroPy legacy db behind an OpenApiApplication.
|
|
5
|
+
|
|
6
|
+
An ``OpenApiApplication`` mounts a RoutingClass and exposes it as REST (and, via
|
|
7
|
+
``McpOpenApiApplication``, as MCP). When those handlers need the GenroPy legacy
|
|
8
|
+
db, someone must own its lifecycle: the connection is thread-local and must be
|
|
9
|
+
closed on the thread that opened it — the executor thread where the handler ran,
|
|
10
|
+
not the loop. genro-asgi provides exactly that hook (``route_cleanup``, run in
|
|
11
|
+
the executor thread by ``make_callable``); this mixin fills it.
|
|
12
|
+
|
|
13
|
+
The mixin instantiates a ``GnrApp`` and exposes it as ``self.gnr_app`` (the only
|
|
14
|
+
channel the mounted RoutingClass reads, e.g. through its parent). It overrides
|
|
15
|
+
only what it owns — ``route_cleanup`` (close the current thread's connection) —
|
|
16
|
+
and never the ``OpenApiApplication`` machinery. Direction of dependency is
|
|
17
|
+
contrib -> genro-asgi: this imports ``gnr.*``; genro-asgi never imports GenroPy.
|
|
18
|
+
|
|
19
|
+
Compose it with the base to get a mountable app::
|
|
20
|
+
|
|
21
|
+
class GenropyProxyOpenApiApplication(GenropyProxyMixin, OpenApiApplication):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
NOTE (collaudo): this package currently lives in genro-asgi/contrib for
|
|
25
|
+
end-to-end testing against the real GenroPy legacy. Once proven it will move to
|
|
26
|
+
the genropy-asgi repository.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from genro_asgi.applications.openapi_application import OpenApiApplication
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger("genropy_asgi.proxy")
|
|
37
|
+
|
|
38
|
+
__all__ = ["GenropyProxyMixin", "GenropyProxyOpenApiApplication"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GenropyProxyMixin:
|
|
42
|
+
"""Owns a ``GnrApp`` and closes its db connection in the executor thread.
|
|
43
|
+
|
|
44
|
+
Mixed before an ``OpenApiApplication``: it builds the GnrApp in ``on_init``
|
|
45
|
+
(before delegating to the base, which mounts the RoutingClass), exposes it as
|
|
46
|
+
``gnr_app``, and fills ``route_cleanup`` to release the thread-local db
|
|
47
|
+
connection after each handler — where it is thread-correct.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def on_init(self, instance: str | None = None, debug: bool = False, **kwargs: Any) -> None:
|
|
51
|
+
"""Build the GnrApp, then let the OpenApiApplication base mount the API.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
instance: GenroPy instance name (or path) resolved by GnrApp.
|
|
55
|
+
debug: Passed through to GnrApp.
|
|
56
|
+
**kwargs: Forwarded to ``OpenApiApplication.on_init`` (routing_class,
|
|
57
|
+
module, docs, api_name, ...).
|
|
58
|
+
"""
|
|
59
|
+
from gnr.app.gnrapp import GnrApp
|
|
60
|
+
|
|
61
|
+
if not instance:
|
|
62
|
+
raise ValueError("GenropyProxyMixin requires an 'instance'")
|
|
63
|
+
log.info("Creating GnrApp for instance '%s'", instance)
|
|
64
|
+
self._gnr_app = GnrApp(instance, debug=debug)
|
|
65
|
+
log.info("GnrApp '%s' ready", instance)
|
|
66
|
+
super().on_init(**kwargs) # type: ignore[misc]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def gnr_app(self) -> Any:
|
|
70
|
+
"""The hosted GnrApp — the only channel the mounted RoutingClass reads."""
|
|
71
|
+
return self._gnr_app
|
|
72
|
+
|
|
73
|
+
def route_cleanup(self) -> None:
|
|
74
|
+
"""Close the current thread's db connection after the handler.
|
|
75
|
+
|
|
76
|
+
Runs in the executor thread (via ``make_callable``), which is where the
|
|
77
|
+
GnrApp opened its thread-local connection, so this is where it must be
|
|
78
|
+
closed. The request-level cleanup runs on the loop and cannot do it.
|
|
79
|
+
"""
|
|
80
|
+
db = getattr(self._gnr_app, "db", None)
|
|
81
|
+
if db is not None:
|
|
82
|
+
db.closeConnection()
|
|
83
|
+
|
|
84
|
+
def on_shutdown(self) -> None:
|
|
85
|
+
"""Release the GnrApp on server stop, then the base."""
|
|
86
|
+
db = getattr(self._gnr_app, "db", None)
|
|
87
|
+
if db is not None:
|
|
88
|
+
db.closeConnection()
|
|
89
|
+
super().on_shutdown() # type: ignore[misc]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class GenropyProxyOpenApiApplication(GenropyProxyMixin, OpenApiApplication):
|
|
93
|
+
"""OpenApiApplication hosting a GnrApp, mountable on an AsgiServer.
|
|
94
|
+
|
|
95
|
+
MRO: the mixin owns ``on_init``/``route_cleanup``/``on_shutdown``; the base
|
|
96
|
+
owns the REST + OpenAPI machinery. Mount it like any OpenApiApplication and
|
|
97
|
+
give it an ``instance`` plus a ``routing_class`` (or ``module``).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
pass
|
|
File without changes
|
genropy_asgi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""genropy_asgi.siteregister — the daemonless register the legacy imports.
|
|
5
|
+
|
|
6
|
+
This subpackage is the provider of the ``gnr.web:daemon`` entry-point: the legacy
|
|
7
|
+
``gnr.web.daemon`` switcher installs it as ``gnr.web.daemon`` in ``sys.modules``, so
|
|
8
|
+
``from gnr.web.daemon.siteregister_client import SiteRegisterClient`` and the handful of
|
|
9
|
+
other legacy imports resolve to the files here — with no register daemon behind them.
|
|
10
|
+
|
|
11
|
+
The register is served entirely in-process by ``GenropyRegisterClient`` (the SPA
|
|
12
|
+
application's own registries/surface/mailbox). The ``handler`` / ``service`` /
|
|
13
|
+
``processes`` modules are fakes: they exist only so the ``gnr.web.daemon.*`` namespace
|
|
14
|
+
resolves for the daemon-CLI / ``app.gnrdaemon`` paths (never reached in the request
|
|
15
|
+
path), and raise if actually used — there is no daemon in this build.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__all__ = ["SiteRegisterClient", "GenropyRegisterClient"]
|
|
19
|
+
|
|
20
|
+
from .siteregister_client import GenropyRegisterClient, SiteRegisterClient
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Exceptions for the in-process siteregister.
|
|
5
|
+
|
|
6
|
+
The names mirror the register-daemon hierarchy the legacy code catches (it does
|
|
7
|
+
``except GnrDaemonLocked`` around a store lock). There is no daemon here, so
|
|
8
|
+
``GnrDaemonLocked`` only ever fires on the in-process lock retry budget, and the
|
|
9
|
+
transport errors (``GnrDaemonUnavailable``, ``GnrDaemonMethodNotFound``) can never
|
|
10
|
+
happen — they are kept only so legacy imports and ``except`` clauses resolve.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"GnrDaemonException",
|
|
17
|
+
"GnrDaemonLocked",
|
|
18
|
+
"GnrDaemonMethodNotFound",
|
|
19
|
+
"GnrDaemonUnavailable",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GnrDaemonException(Exception):
|
|
24
|
+
"""Base class for register errors (kept for legacy compatibility)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GnrDaemonLocked(GnrDaemonException):
|
|
28
|
+
"""An in-process item lock could not be acquired within the retry budget."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GnrDaemonMethodNotFound(GnrDaemonException):
|
|
32
|
+
"""Never raised in-process; kept so legacy imports resolve."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GnrDaemonUnavailable(GnrDaemonException):
|
|
36
|
+
"""Never raised in-process (no transport); kept for import compatibility."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Fake ``gnr.web.daemon.handler`` — there is no daemon in this build.
|
|
5
|
+
|
|
6
|
+
The legacy lazily imports ``GnrDaemonProxy`` here only when something reads
|
|
7
|
+
``app.gnrdaemon`` (a control/stop handle for the register daemon). Nothing in the
|
|
8
|
+
request path does, so this is never reached in practice; it exists so the namespace
|
|
9
|
+
resolves, and any actual use is an explicit error rather than a silent no-op.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__all__ = ["GnrDaemonProxy"]
|
|
17
|
+
|
|
18
|
+
_NO_DAEMON = "no register daemon in this build (the register is served in-process)"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GnrDaemonProxy:
|
|
22
|
+
"""Stub daemon-control proxy: constructing or calling it is an explicit error."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
25
|
+
raise RuntimeError(_NO_DAEMON)
|
|
26
|
+
|
|
27
|
+
def proxy(self) -> Any:
|
|
28
|
+
raise RuntimeError(_NO_DAEMON)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Fake ``gnr.web.daemon.processes`` — the daemon service-process machinery is gone.
|
|
5
|
+
|
|
6
|
+
No module outside the daemon package imported this at runtime; it exists only so the
|
|
7
|
+
``gnr.web.daemon.processes`` namespace resolves for any stray import. Empty on purpose.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Fake ``gnr.web.daemon.service`` — there is no daemon service in this build.
|
|
5
|
+
|
|
6
|
+
The legacy CLI (``gnr web daemon``) imports ``DaemonService`` here. In the in-process
|
|
7
|
+
build there is no daemon to start/stop, so running that command is an explicit error;
|
|
8
|
+
the import itself resolves so the CLI dispatch table does not break at load time.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
__all__ = ["DaemonService"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DaemonService:
|
|
19
|
+
"""Stub: there is no register daemon to control in the in-process build."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
22
|
+
self._args = args
|
|
23
|
+
|
|
24
|
+
def run(self) -> None:
|
|
25
|
+
raise RuntimeError(
|
|
26
|
+
"no register daemon in this build: the register is served in-process, "
|
|
27
|
+
"there is nothing to start with `gnr web daemon`"
|
|
28
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2025 Softwell S.r.l.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""The ``gnr.web.daemon.siteregister`` face of the in-process register.
|
|
5
|
+
|
|
6
|
+
The legacy imports a handful of names from ``gnr.web.daemon.siteregister``
|
|
7
|
+
(``DEFAULT_PAGE_MAX_AGE``, ``GnrDaemonException``) and its backward-compat shim
|
|
8
|
+
re-exports ``RegisterResolver`` from here too. This module provides exactly those,
|
|
9
|
+
with no daemon server behind them: the register lives inside the SPA application, not
|
|
10
|
+
in a separate ``GnrSiteRegister`` process.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .exceptions import GnrDaemonException
|
|
18
|
+
|
|
19
|
+
__all__ = ["DEFAULT_PAGE_MAX_AGE", "GnrDaemonException", "RegisterResolver"]
|
|
20
|
+
|
|
21
|
+
# The page eviction window the legacy cleanup config falls back to (seconds).
|
|
22
|
+
DEFAULT_PAGE_MAX_AGE = 120
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RegisterResolver:
|
|
26
|
+
"""Placeholder for the admin-UI lazy resolver (browse users/connections/pages).
|
|
27
|
+
|
|
28
|
+
The legacy re-exports this name; the in-process build does not drive the daemon
|
|
29
|
+
admin browser, so instantiating it is an explicit error rather than a silent
|
|
30
|
+
empty tree. Kept only so the import resolves.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
34
|
+
raise NotImplementedError(
|
|
35
|
+
"RegisterResolver (daemon admin browser) is not available in the "
|
|
36
|
+
"in-process siteregister"
|
|
37
|
+
)
|