genro-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.
Files changed (59) hide show
  1. genro_asgi/__init__.py +150 -0
  2. genro_asgi/__main__.py +99 -0
  3. genro_asgi/applications/__init__.py +9 -0
  4. genro_asgi/applications/asgi_application.py +146 -0
  5. genro_asgi/applications/mcp_application.py +360 -0
  6. genro_asgi/authentication/__init__.py +28 -0
  7. genro_asgi/authentication/base.py +204 -0
  8. genro_asgi/context.py +104 -0
  9. genro_asgi/datastructures/__init__.py +59 -0
  10. genro_asgi/datastructures/address.py +103 -0
  11. genro_asgi/datastructures/headers.py +259 -0
  12. genro_asgi/datastructures/query_params.py +276 -0
  13. genro_asgi/datastructures/state.py +149 -0
  14. genro_asgi/datastructures/url.py +168 -0
  15. genro_asgi/exceptions.py +293 -0
  16. genro_asgi/executors/__init__.py +46 -0
  17. genro_asgi/executors/base.py +180 -0
  18. genro_asgi/executors/local.py +278 -0
  19. genro_asgi/executors/registry.py +250 -0
  20. genro_asgi/lifespan.py +189 -0
  21. genro_asgi/loader.py +314 -0
  22. genro_asgi/middleware/__init__.py +222 -0
  23. genro_asgi/middleware/authentication.py +367 -0
  24. genro_asgi/middleware/cache.py +281 -0
  25. genro_asgi/middleware/compression.py +231 -0
  26. genro_asgi/middleware/cors.py +266 -0
  27. genro_asgi/middleware/errors.py +186 -0
  28. genro_asgi/middleware/logging.py +155 -0
  29. genro_asgi/request.py +701 -0
  30. genro_asgi/resources.py +205 -0
  31. genro_asgi/response.py +476 -0
  32. genro_asgi/routers/__init__.py +12 -0
  33. genro_asgi/routers/static_router.py +385 -0
  34. genro_asgi/server/__init__.py +16 -0
  35. genro_asgi/server/dispatcher.py +105 -0
  36. genro_asgi/server/server.py +333 -0
  37. genro_asgi/server/server_app/__init__.py +8 -0
  38. genro_asgi/server/server_app/server_app.py +199 -0
  39. genro_asgi/server/server_config.py +221 -0
  40. genro_asgi/storage.py +409 -0
  41. genro_asgi/sys_applications/__init__.py +15 -0
  42. genro_asgi/sys_applications/genro_api/__init__.py +8 -0
  43. genro_asgi/sys_applications/genro_api/genro_api_app.py +167 -0
  44. genro_asgi/sys_applications/swagger/__init__.py +8 -0
  45. genro_asgi/sys_applications/swagger/swagger_app.py +83 -0
  46. genro_asgi/types.py +174 -0
  47. genro_asgi/utils/__init__.py +34 -0
  48. genro_asgi/utils/binder.py +142 -0
  49. genro_asgi/websocket.py +1467 -0
  50. genro_asgi/wsx/__init__.py +42 -0
  51. genro_asgi/wsx/protocol.py +191 -0
  52. genro_asgi/wsx/registry.py +0 -0
  53. genro_asgi-0.2.0.dist-info/METADATA +230 -0
  54. genro_asgi-0.2.0.dist-info/RECORD +59 -0
  55. genro_asgi-0.2.0.dist-info/WHEEL +5 -0
  56. genro_asgi-0.2.0.dist-info/entry_points.txt +2 -0
  57. genro_asgi-0.2.0.dist-info/licenses/LICENSE +201 -0
  58. genro_asgi-0.2.0.dist-info/licenses/NOTICE +9 -0
  59. genro_asgi-0.2.0.dist-info/top_level.txt +1 -0
genro_asgi/__init__.py ADDED
@@ -0,0 +1,150 @@
1
+ # Copyright 2025 Softwell S.r.l.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """genro-asgi - Minimal ASGI framework with routing via genro-routes.
16
+
17
+ Main components:
18
+ AsgiServer: ASGI entry point, loads config, mounts apps
19
+ AsgiApplication: Base class for mountable applications
20
+ Response: HTTP response with auto content-type detection
21
+ HttpRequest: HTTP request wrapper with headers, query, body
22
+
23
+ Middleware:
24
+ AuthMiddleware: O(1) authentication (bearer, basic, JWT)
25
+ CORSMiddleware: Cross-Origin Resource Sharing headers
26
+ ErrorMiddleware: Exception handling and error responses
27
+
28
+ Storage:
29
+ LocalStorage: Filesystem storage with mount system
30
+ ResourceLoader: Hierarchical resource loading with fallback
31
+
32
+ Usage:
33
+ from genro_asgi import AsgiServer
34
+
35
+ server = AsgiServer(server_dir=".")
36
+ server.run() # Starts uvicorn
37
+
38
+ See config.yaml for configuration options.
39
+ """
40
+
41
+ __version__ = "0.2.0"
42
+
43
+ from .datastructures import (
44
+ Address,
45
+ Headers,
46
+ QueryParams,
47
+ State,
48
+ URL,
49
+ headers_from_scope,
50
+ query_params_from_scope,
51
+ )
52
+ from .exceptions import (
53
+ HTTPException,
54
+ HTTPForbidden,
55
+ HTTPNotFound,
56
+ HTTPUnauthorized,
57
+ Redirect,
58
+ WebSocketDisconnect,
59
+ WebSocketException,
60
+ )
61
+ from .lifespan import Lifespan, ServerLifespan
62
+ from .request import (
63
+ BaseRequest,
64
+ HttpRequest,
65
+ MsgRequest,
66
+ RequestRegistry,
67
+ REQUEST_FACTORIES,
68
+ )
69
+ from .response import (
70
+ Response,
71
+ make_cookie,
72
+ )
73
+ from .types import ASGIApp, Message, Receive, Scope, Send
74
+ from .utils import AsgiServerEnabler, ServerBinder
75
+ from .executors import (
76
+ BaseExecutor,
77
+ ExecutorError,
78
+ ExecutorOverloadError,
79
+ ExecutorRegistry,
80
+ LocalExecutor,
81
+ )
82
+ from .server import AsgiServer, ServerConfig, Dispatcher
83
+ from .context import AsgiContext
84
+ from .applications import AsgiApplication, McpApplication
85
+ from .routers import StaticRouter
86
+ from .storage import LocalStorage, LocalStorageNode, StorageNode
87
+ from .websocket import WebSocket, WebSocketState
88
+
89
+ __all__ = [
90
+ # Request classes
91
+ "BaseRequest",
92
+ "HttpRequest",
93
+ "MsgRequest",
94
+ "RequestRegistry",
95
+ "REQUEST_FACTORIES",
96
+ # Response classes
97
+ "Response",
98
+ "Lifespan",
99
+ "ServerLifespan",
100
+ # Helper functions
101
+ "make_cookie",
102
+ # Data structures
103
+ "Address",
104
+ "URL",
105
+ "Headers",
106
+ "QueryParams",
107
+ "State",
108
+ "headers_from_scope",
109
+ "query_params_from_scope",
110
+ # Exceptions
111
+ "HTTPException",
112
+ "HTTPForbidden",
113
+ "HTTPNotFound",
114
+ "HTTPUnauthorized",
115
+ "Redirect",
116
+ "WebSocketException",
117
+ "WebSocketDisconnect",
118
+ # ASGI types
119
+ "ASGIApp",
120
+ "Message",
121
+ "Receive",
122
+ "Scope",
123
+ "Send",
124
+ # Server integration
125
+ "AsgiServer",
126
+ "ServerConfig",
127
+ "Dispatcher",
128
+ # Context
129
+ "AsgiContext",
130
+ # Applications
131
+ "AsgiApplication",
132
+ "McpApplication",
133
+ "ServerBinder",
134
+ "AsgiServerEnabler",
135
+ # Executors
136
+ "BaseExecutor",
137
+ "LocalExecutor",
138
+ "ExecutorRegistry",
139
+ "ExecutorError",
140
+ "ExecutorOverloadError",
141
+ # WebSocket
142
+ "WebSocket",
143
+ "WebSocketState",
144
+ # Routers
145
+ "StaticRouter",
146
+ # Storage
147
+ "LocalStorage",
148
+ "LocalStorageNode",
149
+ "StorageNode",
150
+ ]
genro_asgi/__main__.py ADDED
@@ -0,0 +1,99 @@
1
+ # Copyright 2025 Softwell S.r.l.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ genro-asgi CLI entry point.
17
+
18
+ Usage:
19
+ genro-asgi serve ./myapp # Run server from app directory
20
+ genro-asgi serve ./myapp --port 9000 # Override port
21
+
22
+ The directory must contain a config.yaml or genro-asgi.yaml file.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import sys
28
+
29
+
30
+ def cmd_serve(argv: list[str]) -> int:
31
+ """Run the ASGI server."""
32
+ from .server import AsgiServer
33
+
34
+ # Create server - passes argv for SmartOptions parsing
35
+ server = AsgiServer(argv=argv)
36
+
37
+ # Verify server_dir exists
38
+ server_dir = server.config.server["server_dir"]
39
+ if not server_dir.is_dir():
40
+ print(f"Error: '{server_dir}' is not a directory.", file=sys.stderr)
41
+ return 1
42
+
43
+ # Show startup info
44
+ print("genro-asgi starting...", flush=True)
45
+ print(f"Server dir: {server_dir}", flush=True)
46
+ print(f"Server: http://{server.config.server['host']}:{server.config.server['port']}", flush=True)
47
+ if server.config.server["reload"]:
48
+ print("Mode: development (auto-reload enabled)", flush=True)
49
+ print(flush=True)
50
+
51
+ # Run server
52
+ try:
53
+ server.run()
54
+ except KeyboardInterrupt:
55
+ print("\nShutdown.")
56
+
57
+ return 0
58
+
59
+
60
+ def main() -> int:
61
+ """Main entry point."""
62
+ # Handle --version
63
+ if "--version" in sys.argv or "-v" in sys.argv:
64
+ from . import __version__
65
+
66
+ print(f"genro-asgi {__version__}")
67
+ return 0
68
+
69
+ # Handle --help
70
+ if "--help" in sys.argv or "-h" in sys.argv or len(sys.argv) == 1:
71
+ print("Usage: genro-asgi serve <app_dir> [options]")
72
+ print()
73
+ print("Arguments:")
74
+ print(" app_dir Path to app directory")
75
+ print()
76
+ print("Options:")
77
+ print(" --config FILE Config file (default: config.yaml)")
78
+ print(" --host HOST Server host (default: 127.0.0.1)")
79
+ print(" --port PORT Server port (default: 8000)")
80
+ print(" --reload Enable auto-reload")
81
+ print(" --version, -v Show version")
82
+ print(" --help, -h Show this help")
83
+ return 0
84
+
85
+ # Extract subcommand
86
+ if len(sys.argv) < 2:
87
+ print("Error: missing subcommand", file=sys.stderr)
88
+ return 1
89
+
90
+ subcommand = sys.argv[1]
91
+ if subcommand != "serve":
92
+ print(f"Error: unknown subcommand '{subcommand}'", file=sys.stderr)
93
+ return 1
94
+
95
+ return cmd_serve(sys.argv[2:])
96
+
97
+
98
+ if __name__ == "__main__":
99
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ # Copyright 2025 Softwell S.r.l.
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ """Application classes for genro-asgi."""
5
+
6
+ from .asgi_application import AsgiApplication
7
+ from .mcp_application import McpApplication
8
+
9
+ __all__ = ["AsgiApplication", "McpApplication"]
@@ -0,0 +1,146 @@
1
+ # Copyright 2025 Softwell S.r.l.
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ """AsgiApplication - Base class for ASGI applications."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, ClassVar
10
+
11
+ from genro_routes import Router, RoutingClass, route # type: ignore[import-untyped]
12
+
13
+ if TYPE_CHECKING:
14
+ from ..server import AsgiServer
15
+
16
+ __all__ = ["AsgiApplication"]
17
+
18
+
19
+ class AsgiApplication(RoutingClass):
20
+ """Base class for apps mounted on AsgiServer.
21
+
22
+ Provides default `main` router and `index()` method. Subclasses define
23
+ `openapi_info` for metadata and add routes with @route() decorator.
24
+
25
+ Example::
26
+
27
+ class MyApp(AsgiApplication):
28
+ openapi_info = {"title": "My API", "version": "1.0.0"}
29
+
30
+ @route() # Uses the only router (self.main) automatically
31
+ def hello(self):
32
+ return "Hello!"
33
+
34
+ def __init__(self, **kwargs):
35
+ super().__init__(**kwargs) # Required: creates self.main
36
+ self.backoffice = Router(self, name="backoffice")
37
+
38
+ @route("backoffice") # Must specify when multiple routers
39
+ def admin(self):
40
+ return "Admin panel"
41
+ """
42
+
43
+ openapi_info: ClassVar[dict[str, Any]] = {}
44
+
45
+ def __init__(self, **kwargs: Any) -> None:
46
+ """Initialize app with default main router."""
47
+ self.base_dir = kwargs.pop("base_dir", None)
48
+ self.main = Router(self, name="main")
49
+ self.on_init(**kwargs)
50
+
51
+ def on_init(self, **kwargs: Any) -> None:
52
+ """Called after base initialization. Override for custom setup.
53
+
54
+ Args:
55
+ **kwargs: Parameters from config.yaml app definition.
56
+ """
57
+ pass
58
+
59
+ @property
60
+ def server(self) -> AsgiServer | None:
61
+ """Return the server that mounted this app (semantic alias for _routing_parent)."""
62
+ return getattr(self, "_routing_parent", None)
63
+
64
+ def on_startup(self) -> None:
65
+ """Called when server starts. Override for custom initialization.
66
+
67
+ Can be sync or async. Called after all apps are mounted.
68
+ """
69
+ pass
70
+
71
+ def on_shutdown(self) -> None:
72
+ """Called when server stops. Override for custom cleanup.
73
+
74
+ Can be sync or async. Called in reverse order of startup.
75
+ """
76
+ pass
77
+
78
+ @property
79
+ def resources_dir(self) -> Path | None:
80
+ """Return path to app's resources directory.
81
+
82
+ Works both when mounted on server and standalone.
83
+ """
84
+ if self.base_dir:
85
+ return Path(self.base_dir) / "resources"
86
+ return None
87
+
88
+ def load_resource(self, *args: str, name: str) -> tuple[bytes, str] | None:
89
+ """Load resource file content with mime type.
90
+
91
+ When mounted on server: uses server's ResourceLoader with fallback to local.
92
+ Standalone: reads directly from resources_dir.
93
+
94
+ Returns:
95
+ Tuple of (content_bytes, mime_type) or None if not found.
96
+ """
97
+ if self.server:
98
+ mount_name = getattr(self, "_mount_name", "")
99
+ result = self.server.resource_loader.load(mount_name, *args, name=name)
100
+ if result:
101
+ return result
102
+ # Fallback to local resources_dir (for sys_apps under _sys/)
103
+
104
+ # Local mode: read from resources_dir
105
+ if not self.resources_dir:
106
+ return None
107
+ resource_path = self.resources_dir / "/".join(args) / name if args else self.resources_dir / name
108
+ if resource_path.exists():
109
+ # Simple mime type detection
110
+ suffix = resource_path.suffix.lower()
111
+ mime_types = {
112
+ ".html": "text/html",
113
+ ".css": "text/css",
114
+ ".js": "application/javascript",
115
+ ".json": "application/json",
116
+ ".txt": "text/plain",
117
+ }
118
+ mime_type = mime_types.get(suffix, "application/octet-stream")
119
+ return resource_path.read_bytes(), mime_type
120
+ return None
121
+
122
+ @route(meta_mime_type="text/html")
123
+ def index(self) -> str:
124
+ """Return HTML splash page. Override for custom index."""
125
+ info = getattr(self, "openapi_info", {})
126
+ title = info.get("title", self.__class__.__name__)
127
+ version = info.get("version", "")
128
+ description = info.get("description", "")
129
+
130
+ version_html = f"<p>Version: {version}</p>" if version else ""
131
+ desc_html = f"<p>{description}</p>" if description else ""
132
+
133
+ return f"""<!DOCTYPE html>
134
+ <html>
135
+ <head><title>{title}</title>
136
+ <style>
137
+ body {{ font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }}
138
+ h1 {{ color: #333; }}
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <h1>{title}</h1>
143
+ {version_html}
144
+ {desc_html}
145
+ </body>
146
+ </html>"""