celerity-sdk 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.
- celerity/__init__.py +114 -0
- celerity/application.py +83 -0
- celerity/bootstrap/__init__.py +13 -0
- celerity/bootstrap/bootstrap.py +70 -0
- celerity/bootstrap/discovery.py +67 -0
- celerity/bootstrap/module_graph.py +158 -0
- celerity/bootstrap/runtime_entry.py +311 -0
- celerity/bootstrap/runtime_mapper.py +210 -0
- celerity/bootstrap/runtime_orchestrator.py +200 -0
- celerity/cli/__init__.py +0 -0
- celerity/cli/extract.py +97 -0
- celerity/cli/identity.py +89 -0
- celerity/cli/metadata_app.py +338 -0
- celerity/cli/serializer.py +460 -0
- celerity/cli/types.py +151 -0
- celerity/common/__init__.py +6 -0
- celerity/common/debug.py +146 -0
- celerity/common/path_utils.py +30 -0
- celerity/config/__init__.py +27 -0
- celerity/config/backends/__init__.py +1 -0
- celerity/config/backends/aws/__init__.py +1 -0
- celerity/config/backends/aws/lambda_extension.py +62 -0
- celerity/config/backends/aws/parameter_store.py +39 -0
- celerity/config/backends/aws/secrets_manager.py +39 -0
- celerity/config/backends/empty.py +16 -0
- celerity/config/backends/local.py +48 -0
- celerity/config/backends/types.py +21 -0
- celerity/config/layer.py +132 -0
- celerity/config/params.py +88 -0
- celerity/config/service.py +104 -0
- celerity/decorators/__init__.py +98 -0
- celerity/decorators/consumer.py +86 -0
- celerity/decorators/consumer_params.py +71 -0
- celerity/decorators/controller.py +46 -0
- celerity/decorators/guards.py +121 -0
- celerity/decorators/http.py +152 -0
- celerity/decorators/injectable.py +110 -0
- celerity/decorators/invoke.py +36 -0
- celerity/decorators/invoke_params.py +47 -0
- celerity/decorators/layer.py +73 -0
- celerity/decorators/metadata.py +62 -0
- celerity/decorators/module.py +74 -0
- celerity/decorators/params.py +200 -0
- celerity/decorators/resource.py +45 -0
- celerity/decorators/schedule.py +70 -0
- celerity/decorators/schedule_params.py +71 -0
- celerity/decorators/websocket.py +135 -0
- celerity/decorators/websocket_params.py +83 -0
- celerity/di/__init__.py +12 -0
- celerity/di/container.py +311 -0
- celerity/di/dependency_tokens.py +95 -0
- celerity/di/tokens.py +7 -0
- celerity/errors/__init__.py +35 -0
- celerity/errors/http_exception.py +83 -0
- celerity/functions/__init__.py +29 -0
- celerity/functions/consumer.py +60 -0
- celerity/functions/context.py +81 -0
- celerity/functions/custom.py +53 -0
- celerity/functions/guard.py +46 -0
- celerity/functions/http.py +80 -0
- celerity/functions/schedule.py +62 -0
- celerity/functions/websocket.py +56 -0
- celerity/handlers/__init__.py +5 -0
- celerity/handlers/consumer_pipeline.py +71 -0
- celerity/handlers/custom_pipeline.py +65 -0
- celerity/handlers/guard_pipeline.py +207 -0
- celerity/handlers/http_pipeline.py +123 -0
- celerity/handlers/module_resolver.py +52 -0
- celerity/handlers/param_extractor.py +258 -0
- celerity/handlers/registry.py +232 -0
- celerity/handlers/scanners/__init__.py +0 -0
- celerity/handlers/scanners/_utils.py +49 -0
- celerity/handlers/scanners/consumer.py +79 -0
- celerity/handlers/scanners/custom.py +74 -0
- celerity/handlers/scanners/http.py +155 -0
- celerity/handlers/scanners/schedule.py +78 -0
- celerity/handlers/scanners/websocket.py +71 -0
- celerity/handlers/schedule_pipeline.py +69 -0
- celerity/handlers/websocket_pipeline.py +53 -0
- celerity/layers/__init__.py +7 -0
- celerity/layers/dispose.py +26 -0
- celerity/layers/pipeline.py +80 -0
- celerity/layers/system.py +78 -0
- celerity/layers/validate.py +108 -0
- celerity/metadata/__init__.py +6 -0
- celerity/metadata/keys.py +77 -0
- celerity/metadata/store.py +48 -0
- celerity/py.typed +0 -0
- celerity/resources/__init__.py +0 -0
- celerity/resources/_common.py +104 -0
- celerity/resources/_tokens.py +80 -0
- celerity/resources/bucket/__init__.py +41 -0
- celerity/resources/bucket/errors.py +21 -0
- celerity/resources/bucket/factory.py +71 -0
- celerity/resources/bucket/layer.py +82 -0
- celerity/resources/bucket/params.py +65 -0
- celerity/resources/bucket/providers/__init__.py +0 -0
- celerity/resources/bucket/providers/s3/__init__.py +0 -0
- celerity/resources/bucket/providers/s3/client.py +360 -0
- celerity/resources/bucket/providers/s3/config.py +39 -0
- celerity/resources/bucket/providers/s3/listing.py +137 -0
- celerity/resources/bucket/providers/s3/types.py +18 -0
- celerity/resources/bucket/types.py +178 -0
- celerity/resources/cache/__init__.py +33 -0
- celerity/resources/cache/config.py +85 -0
- celerity/resources/cache/credentials.py +153 -0
- celerity/resources/cache/errors.py +14 -0
- celerity/resources/cache/factory.py +52 -0
- celerity/resources/cache/layer.py +96 -0
- celerity/resources/cache/params.py +65 -0
- celerity/resources/cache/providers/__init__.py +0 -0
- celerity/resources/cache/providers/redis/__init__.py +0 -0
- celerity/resources/cache/providers/redis/cache.py +651 -0
- celerity/resources/cache/providers/redis/client.py +138 -0
- celerity/resources/cache/providers/redis/cluster.py +105 -0
- celerity/resources/cache/providers/redis/iam/__init__.py +0 -0
- celerity/resources/cache/providers/redis/iam/elasticache_token.py +57 -0
- celerity/resources/cache/providers/redis/types.py +24 -0
- celerity/resources/cache/types.py +246 -0
- celerity/resources/datastore/__init__.py +61 -0
- celerity/resources/datastore/errors.py +29 -0
- celerity/resources/datastore/factory.py +100 -0
- celerity/resources/datastore/layer.py +84 -0
- celerity/resources/datastore/params.py +65 -0
- celerity/resources/datastore/providers/__init__.py +1 -0
- celerity/resources/datastore/providers/dynamodb/__init__.py +1 -0
- celerity/resources/datastore/providers/dynamodb/client.py +480 -0
- celerity/resources/datastore/providers/dynamodb/config.py +25 -0
- celerity/resources/datastore/providers/dynamodb/expressions.py +227 -0
- celerity/resources/datastore/providers/dynamodb/listing.py +96 -0
- celerity/resources/datastore/providers/dynamodb/marshall.py +27 -0
- celerity/resources/datastore/providers/dynamodb/types.py +17 -0
- celerity/resources/datastore/types.py +251 -0
- celerity/resources/queue/__init__.py +35 -0
- celerity/resources/queue/errors.py +22 -0
- celerity/resources/queue/factory.py +87 -0
- celerity/resources/queue/layer.py +84 -0
- celerity/resources/queue/params.py +65 -0
- celerity/resources/queue/providers/__init__.py +0 -0
- celerity/resources/queue/providers/redis/__init__.py +0 -0
- celerity/resources/queue/providers/redis/client.py +186 -0
- celerity/resources/queue/providers/redis/types.py +12 -0
- celerity/resources/queue/providers/sqs/__init__.py +0 -0
- celerity/resources/queue/providers/sqs/client.py +225 -0
- celerity/resources/queue/providers/sqs/config.py +25 -0
- celerity/resources/queue/providers/sqs/types.py +17 -0
- celerity/resources/queue/types.py +103 -0
- celerity/resources/sql_database/__init__.py +61 -0
- celerity/resources/sql_database/config.py +132 -0
- celerity/resources/sql_database/credentials.py +150 -0
- celerity/resources/sql_database/errors.py +21 -0
- celerity/resources/sql_database/factory.py +123 -0
- celerity/resources/sql_database/layer.py +89 -0
- celerity/resources/sql_database/params.py +160 -0
- celerity/resources/sql_database/providers/__init__.py +0 -0
- celerity/resources/sql_database/providers/rds/__init__.py +0 -0
- celerity/resources/sql_database/providers/rds/iam.py +71 -0
- celerity/resources/sql_database/types.py +85 -0
- celerity/resources/topic/__init__.py +35 -0
- celerity/resources/topic/errors.py +22 -0
- celerity/resources/topic/factory.py +86 -0
- celerity/resources/topic/layer.py +84 -0
- celerity/resources/topic/params.py +54 -0
- celerity/resources/topic/providers/__init__.py +0 -0
- celerity/resources/topic/providers/redis/__init__.py +0 -0
- celerity/resources/topic/providers/redis/client.py +185 -0
- celerity/resources/topic/providers/redis/types.py +12 -0
- celerity/resources/topic/providers/sns/__init__.py +0 -0
- celerity/resources/topic/providers/sns/client.py +225 -0
- celerity/resources/topic/providers/sns/config.py +25 -0
- celerity/resources/topic/providers/sns/types.py +17 -0
- celerity/resources/topic/types.py +102 -0
- celerity/serverless/__init__.py +0 -0
- celerity/serverless/aws/__init__.py +5 -0
- celerity/serverless/aws/adapter.py +313 -0
- celerity/serverless/aws/event_mapper.py +330 -0
- celerity/serverless/aws/websocket_sender.py +54 -0
- celerity/serverless/azure/__init__.py +0 -0
- celerity/serverless/gcp/__init__.py +0 -0
- celerity/telemetry/__init__.py +45 -0
- celerity/telemetry/context.py +29 -0
- celerity/telemetry/env.py +67 -0
- celerity/telemetry/helpers.py +22 -0
- celerity/telemetry/init.py +121 -0
- celerity/telemetry/instrumentations.py +79 -0
- celerity/telemetry/logger.py +122 -0
- celerity/telemetry/noop.py +48 -0
- celerity/telemetry/request_context.py +68 -0
- celerity/telemetry/telemetry_layer.py +195 -0
- celerity/telemetry/tracer.py +59 -0
- celerity/testing/__init__.py +17 -0
- celerity/testing/mocks.py +146 -0
- celerity/testing/test_app.py +278 -0
- celerity/types/__init__.py +106 -0
- celerity/types/common.py +36 -0
- celerity/types/consumer.py +98 -0
- celerity/types/container.py +56 -0
- celerity/types/context.py +81 -0
- celerity/types/guard.py +46 -0
- celerity/types/handler.py +108 -0
- celerity/types/http.py +40 -0
- celerity/types/layer.py +40 -0
- celerity/types/module.py +44 -0
- celerity/types/schedule.py +17 -0
- celerity/types/telemetry.py +74 -0
- celerity/types/websocket.py +72 -0
- celerity_sdk-0.2.0.dist-info/METADATA +92 -0
- celerity_sdk-0.2.0.dist-info/RECORD +211 -0
- celerity_sdk-0.2.0.dist-info/WHEEL +4 -0
- celerity_sdk-0.2.0.dist-info/entry_points.txt +2 -0
- celerity_sdk-0.2.0.dist-info/licenses/LICENSE +201 -0
celerity/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Celerity Python SDK - Build Celerity applications with Python."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
# Decorators
|
|
6
|
+
# Parameter types
|
|
7
|
+
from celerity.decorators import (
|
|
8
|
+
Auth,
|
|
9
|
+
Body,
|
|
10
|
+
ConnectionId,
|
|
11
|
+
ConsumerEvent,
|
|
12
|
+
ConsumerTraceContext,
|
|
13
|
+
ConsumerVendor,
|
|
14
|
+
Cookies,
|
|
15
|
+
EventType,
|
|
16
|
+
Headers,
|
|
17
|
+
InvokeContext,
|
|
18
|
+
MessageBody,
|
|
19
|
+
MessageId,
|
|
20
|
+
Messages,
|
|
21
|
+
Param,
|
|
22
|
+
Payload,
|
|
23
|
+
Query,
|
|
24
|
+
Req,
|
|
25
|
+
RequestContext,
|
|
26
|
+
RequestId,
|
|
27
|
+
ScheduleExpression,
|
|
28
|
+
ScheduleId,
|
|
29
|
+
ScheduleInput,
|
|
30
|
+
Token,
|
|
31
|
+
action,
|
|
32
|
+
consumer,
|
|
33
|
+
controller,
|
|
34
|
+
delete,
|
|
35
|
+
get,
|
|
36
|
+
guard,
|
|
37
|
+
head,
|
|
38
|
+
inject,
|
|
39
|
+
injectable,
|
|
40
|
+
invoke,
|
|
41
|
+
message_handler,
|
|
42
|
+
module,
|
|
43
|
+
on_connect,
|
|
44
|
+
on_disconnect,
|
|
45
|
+
on_message,
|
|
46
|
+
options,
|
|
47
|
+
patch,
|
|
48
|
+
post,
|
|
49
|
+
protected_by,
|
|
50
|
+
public,
|
|
51
|
+
put,
|
|
52
|
+
schedule_handler,
|
|
53
|
+
set_handler_metadata,
|
|
54
|
+
use_layer,
|
|
55
|
+
use_layers,
|
|
56
|
+
use_resource,
|
|
57
|
+
ws_controller,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Parameter types
|
|
62
|
+
"Auth",
|
|
63
|
+
"Body",
|
|
64
|
+
"ConnectionId",
|
|
65
|
+
"ConsumerEvent",
|
|
66
|
+
"ConsumerTraceContext",
|
|
67
|
+
"ConsumerVendor",
|
|
68
|
+
"Cookies",
|
|
69
|
+
"EventType",
|
|
70
|
+
"Headers",
|
|
71
|
+
"InvokeContext",
|
|
72
|
+
"MessageBody",
|
|
73
|
+
"MessageId",
|
|
74
|
+
"Messages",
|
|
75
|
+
"Param",
|
|
76
|
+
"Payload",
|
|
77
|
+
"Query",
|
|
78
|
+
"Req",
|
|
79
|
+
"RequestContext",
|
|
80
|
+
"RequestId",
|
|
81
|
+
"ScheduleExpression",
|
|
82
|
+
"ScheduleId",
|
|
83
|
+
"ScheduleInput",
|
|
84
|
+
"Token",
|
|
85
|
+
"__version__",
|
|
86
|
+
# Decorators
|
|
87
|
+
"action",
|
|
88
|
+
"consumer",
|
|
89
|
+
"controller",
|
|
90
|
+
"delete",
|
|
91
|
+
"get",
|
|
92
|
+
"guard",
|
|
93
|
+
"head",
|
|
94
|
+
"inject",
|
|
95
|
+
"injectable",
|
|
96
|
+
"invoke",
|
|
97
|
+
"message_handler",
|
|
98
|
+
"module",
|
|
99
|
+
"on_connect",
|
|
100
|
+
"on_disconnect",
|
|
101
|
+
"on_message",
|
|
102
|
+
"options",
|
|
103
|
+
"patch",
|
|
104
|
+
"post",
|
|
105
|
+
"protected_by",
|
|
106
|
+
"public",
|
|
107
|
+
"put",
|
|
108
|
+
"schedule_handler",
|
|
109
|
+
"set_handler_metadata",
|
|
110
|
+
"use_layer",
|
|
111
|
+
"use_layers",
|
|
112
|
+
"use_resource",
|
|
113
|
+
"ws_controller",
|
|
114
|
+
]
|
celerity/application.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Application classes and factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from celerity.bootstrap.bootstrap import bootstrap
|
|
9
|
+
from celerity.layers.dispose import dispose_layers
|
|
10
|
+
from celerity.layers.system import create_default_system_layers
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from celerity.bootstrap.module_graph import ModuleGraph
|
|
14
|
+
from celerity.di.container import Container
|
|
15
|
+
from celerity.handlers.registry import HandlerRegistry
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("celerity.factory")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CelerityApplication:
|
|
21
|
+
"""Standard application with handler registry and DI container.
|
|
22
|
+
|
|
23
|
+
Created via ``CelerityFactory.create()``. Holds the bootstrapped
|
|
24
|
+
container, handler registry, and layer pipeline configuration.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
registry: HandlerRegistry,
|
|
30
|
+
container: Container,
|
|
31
|
+
graph: ModuleGraph,
|
|
32
|
+
system_layers: list[Any] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.registry = registry
|
|
35
|
+
self.container = container
|
|
36
|
+
self.graph = graph
|
|
37
|
+
self.system_layers = system_layers or []
|
|
38
|
+
|
|
39
|
+
async def close(self) -> None:
|
|
40
|
+
"""Shut down the application, disposing layers and closing resources."""
|
|
41
|
+
await dispose_layers(self.system_layers)
|
|
42
|
+
await self.container.close_all()
|
|
43
|
+
|
|
44
|
+
def get_container(self) -> Container:
|
|
45
|
+
"""Get the DI container."""
|
|
46
|
+
return self.container
|
|
47
|
+
|
|
48
|
+
def get_registry(self) -> HandlerRegistry:
|
|
49
|
+
"""Get the handler registry."""
|
|
50
|
+
return self.registry
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CelerityFactory:
|
|
54
|
+
"""Factory for creating application instances."""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
async def create(
|
|
58
|
+
root_module: type,
|
|
59
|
+
*,
|
|
60
|
+
layers: list[Any] | None = None,
|
|
61
|
+
) -> CelerityApplication:
|
|
62
|
+
"""Create and bootstrap a ``CelerityApplication``.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
root_module: The root ``@module``-decorated class.
|
|
66
|
+
layers: Optional application-level layers to add after
|
|
67
|
+
system layers.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A bootstrapped application ready to handle requests.
|
|
71
|
+
|
|
72
|
+
Example::
|
|
73
|
+
|
|
74
|
+
app = await CelerityFactory.create(AppModule)
|
|
75
|
+
# use app.registry, app.container
|
|
76
|
+
await app.close()
|
|
77
|
+
"""
|
|
78
|
+
container, registry, graph = await bootstrap(root_module)
|
|
79
|
+
system_layers = create_default_system_layers()
|
|
80
|
+
logger.debug("create: %d system layers", len(system_layers))
|
|
81
|
+
if layers:
|
|
82
|
+
system_layers.extend(layers)
|
|
83
|
+
return CelerityApplication(registry, container, graph, system_layers)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Application bootstrap and module graph."""
|
|
2
|
+
|
|
3
|
+
from celerity.bootstrap.module_graph import (
|
|
4
|
+
build_module_graph,
|
|
5
|
+
register_module_graph,
|
|
6
|
+
walk_module_graph,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"build_module_graph",
|
|
11
|
+
"register_module_graph",
|
|
12
|
+
"walk_module_graph",
|
|
13
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Application bootstrap entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import celerity.common.debug as _ # noqa: F401
|
|
9
|
+
from celerity.bootstrap.module_graph import walk_module_graph
|
|
10
|
+
from celerity.di.container import Container
|
|
11
|
+
from celerity.handlers.registry import HandlerRegistry
|
|
12
|
+
from celerity.handlers.scanners.consumer import scan_consumer_handlers
|
|
13
|
+
from celerity.handlers.scanners.custom import scan_custom_handlers
|
|
14
|
+
from celerity.handlers.scanners.http import scan_http_guards, scan_http_handlers
|
|
15
|
+
from celerity.handlers.scanners.schedule import scan_schedule_handlers
|
|
16
|
+
from celerity.handlers.scanners.websocket import scan_websocket_handlers
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from celerity.bootstrap.module_graph import ModuleGraph
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("celerity.bootstrap")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def bootstrap(
|
|
25
|
+
root_module: type,
|
|
26
|
+
) -> tuple[Container, HandlerRegistry, ModuleGraph]:
|
|
27
|
+
"""Bootstrap the DI container and handler registry from a root module.
|
|
28
|
+
|
|
29
|
+
Performs the full application bootstrap sequence:
|
|
30
|
+
|
|
31
|
+
1. Create ``Container`` and ``HandlerRegistry``
|
|
32
|
+
2. Walk the module graph (build + register providers)
|
|
33
|
+
3. Scan HTTP handlers and guards
|
|
34
|
+
4. Scan WebSocket handlers
|
|
35
|
+
5. Scan consumer handlers
|
|
36
|
+
6. Scan schedule handlers
|
|
37
|
+
7. Scan custom handlers
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
root_module: The root ``@module``-decorated class.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A tuple of ``(container, registry, graph)``.
|
|
44
|
+
|
|
45
|
+
Example::
|
|
46
|
+
|
|
47
|
+
@module(controllers=[OrderController], providers=[OrderService])
|
|
48
|
+
class AppModule:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
container, registry, graph = await bootstrap(AppModule)
|
|
52
|
+
"""
|
|
53
|
+
logger.debug("bootstrap: starting from %s", root_module.__name__)
|
|
54
|
+
container = Container()
|
|
55
|
+
registry = HandlerRegistry()
|
|
56
|
+
|
|
57
|
+
graph = walk_module_graph(root_module, container)
|
|
58
|
+
await scan_http_handlers(graph, container, registry)
|
|
59
|
+
await scan_http_guards(graph, container, registry)
|
|
60
|
+
await scan_websocket_handlers(graph, container, registry)
|
|
61
|
+
await scan_consumer_handlers(graph, container, registry)
|
|
62
|
+
await scan_schedule_handlers(graph, container, registry)
|
|
63
|
+
await scan_custom_handlers(graph, container, registry)
|
|
64
|
+
|
|
65
|
+
logger.debug(
|
|
66
|
+
"bootstrap: complete — %d handlers, %d guards",
|
|
67
|
+
len(registry.get_all_handlers()),
|
|
68
|
+
len(registry.get_all_guards()),
|
|
69
|
+
)
|
|
70
|
+
return container, registry, graph
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Root module discovery for runtime and serverless modes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from celerity.metadata.keys import MODULE, get_metadata
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("celerity.bootstrap")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def discover_module(module_path: str | None = None) -> type:
|
|
17
|
+
"""Discover the root ``@module`` class.
|
|
18
|
+
|
|
19
|
+
Resolution order:
|
|
20
|
+
|
|
21
|
+
1. Explicit ``module_path`` argument
|
|
22
|
+
2. ``CELERITY_MODULE_PATH`` environment variable
|
|
23
|
+
3. Raises ``RuntimeError``
|
|
24
|
+
|
|
25
|
+
The path is converted to a Python module name, imported, and
|
|
26
|
+
scanned for a class decorated with ``@module``.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
module_path: Optional explicit path to the module file.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The root ``@module``-decorated class.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
RuntimeError: If no module path is found or no ``@module``
|
|
36
|
+
class exists in the module.
|
|
37
|
+
"""
|
|
38
|
+
resolved = module_path or os.environ.get("CELERITY_MODULE_PATH")
|
|
39
|
+
if not resolved:
|
|
40
|
+
msg = "No module path provided. Set CELERITY_MODULE_PATH or pass module_path explicitly."
|
|
41
|
+
raise RuntimeError(msg)
|
|
42
|
+
|
|
43
|
+
logger.debug("discover_module: loading %s", resolved)
|
|
44
|
+
|
|
45
|
+
path = Path(resolved)
|
|
46
|
+
|
|
47
|
+
# Ensure the parent directory is importable.
|
|
48
|
+
parent = str(path.parent.resolve())
|
|
49
|
+
if parent not in sys.path:
|
|
50
|
+
sys.path.insert(0, parent)
|
|
51
|
+
|
|
52
|
+
module_name = _path_to_module_name(path)
|
|
53
|
+
imported = importlib.import_module(module_name)
|
|
54
|
+
|
|
55
|
+
for name in dir(imported):
|
|
56
|
+
obj = getattr(imported, name)
|
|
57
|
+
if isinstance(obj, type) and get_metadata(obj, MODULE) is not None:
|
|
58
|
+
logger.debug("discover_module: found %s", obj.__name__)
|
|
59
|
+
return obj
|
|
60
|
+
|
|
61
|
+
msg = f"No @module class found in {resolved}"
|
|
62
|
+
raise RuntimeError(msg)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _path_to_module_name(path: Path) -> str:
|
|
66
|
+
"""Convert a file path to a Python module name."""
|
|
67
|
+
return path.with_suffix("").name
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Module graph builder and DI registrar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from celerity.metadata.keys import MODULE, get_metadata
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from celerity.di.container import Container
|
|
13
|
+
from celerity.types.container import Provider
|
|
14
|
+
from celerity.types.module import (
|
|
15
|
+
FunctionHandlerDefinition,
|
|
16
|
+
GuardDefinition,
|
|
17
|
+
ModuleMetadata,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("celerity.bootstrap")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ModuleNode:
|
|
25
|
+
"""A single node in the module dependency graph."""
|
|
26
|
+
|
|
27
|
+
module_class: type
|
|
28
|
+
own_tokens: set[Any] = field(default_factory=set)
|
|
29
|
+
exports: set[Any] = field(default_factory=set)
|
|
30
|
+
imports: list[type] = field(default_factory=list)
|
|
31
|
+
controllers: list[type] = field(default_factory=list)
|
|
32
|
+
function_handlers: list[FunctionHandlerDefinition] = field(default_factory=list)
|
|
33
|
+
guards: list[type | GuardDefinition] = field(default_factory=list)
|
|
34
|
+
providers: list[type | Provider] = field(default_factory=list)
|
|
35
|
+
layers: list[Any] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
type ModuleGraph = dict[type, ModuleNode]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_module_graph(root_module: type) -> ModuleGraph:
|
|
42
|
+
"""Walk the module tree depth-first, collecting metadata.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
root_module: The root ``@module``-decorated class.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A dict mapping each module class to its ``ModuleNode``.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
RuntimeError: If a circular module import is detected.
|
|
52
|
+
|
|
53
|
+
Example::
|
|
54
|
+
|
|
55
|
+
@module(controllers=[OrderController], providers=[OrderService])
|
|
56
|
+
class AppModule:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
graph = build_module_graph(AppModule)
|
|
60
|
+
"""
|
|
61
|
+
logger.debug("build_module_graph: starting from %s", root_module.__name__)
|
|
62
|
+
graph: ModuleGraph = {}
|
|
63
|
+
resolving: set[type] = set()
|
|
64
|
+
|
|
65
|
+
def walk(module_class: type, import_chain: list[type]) -> None:
|
|
66
|
+
if module_class in graph:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if module_class in resolving:
|
|
70
|
+
cycle = " -> ".join(m.__name__ for m in [*import_chain, module_class])
|
|
71
|
+
raise RuntimeError(f"Circular module import detected: {cycle}")
|
|
72
|
+
|
|
73
|
+
resolving.add(module_class)
|
|
74
|
+
metadata: ModuleMetadata | None = get_metadata(module_class, MODULE)
|
|
75
|
+
|
|
76
|
+
if metadata is None:
|
|
77
|
+
resolving.discard(module_class)
|
|
78
|
+
graph[module_class] = ModuleNode(module_class=module_class)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
logger.debug(
|
|
82
|
+
"walk %s: %d providers, %d controllers, %d guards, %d imports",
|
|
83
|
+
module_class.__name__,
|
|
84
|
+
len(metadata.providers or []),
|
|
85
|
+
len(metadata.controllers or []),
|
|
86
|
+
len(metadata.guards or []),
|
|
87
|
+
len(metadata.imports or []),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for imported in metadata.imports or []:
|
|
91
|
+
walk(imported, [*import_chain, module_class])
|
|
92
|
+
|
|
93
|
+
own_tokens: set[Any] = set()
|
|
94
|
+
for provider in metadata.providers or []:
|
|
95
|
+
own_tokens.add(provider)
|
|
96
|
+
|
|
97
|
+
for ctrl in metadata.controllers or []:
|
|
98
|
+
own_tokens.add(ctrl)
|
|
99
|
+
|
|
100
|
+
for g in metadata.guards or []:
|
|
101
|
+
if isinstance(g, type):
|
|
102
|
+
own_tokens.add(g)
|
|
103
|
+
|
|
104
|
+
resolving.discard(module_class)
|
|
105
|
+
graph[module_class] = ModuleNode(
|
|
106
|
+
module_class=module_class,
|
|
107
|
+
own_tokens=own_tokens,
|
|
108
|
+
exports=set(metadata.exports or []),
|
|
109
|
+
imports=metadata.imports or [],
|
|
110
|
+
controllers=metadata.controllers or [],
|
|
111
|
+
function_handlers=metadata.function_handlers or [],
|
|
112
|
+
guards=metadata.guards or [],
|
|
113
|
+
providers=metadata.providers or [],
|
|
114
|
+
layers=metadata.layers or [],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
walk(root_module, [])
|
|
118
|
+
logger.debug("build_module_graph: complete — %d modules", len(graph))
|
|
119
|
+
return graph
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def register_module_graph(graph: ModuleGraph, container: Container) -> None:
|
|
123
|
+
"""Register all providers from the module graph into a DI container.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
graph: The module graph built by ``build_module_graph``.
|
|
127
|
+
container: The DI container to register providers in.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
for node in graph.values():
|
|
131
|
+
for provider in node.providers:
|
|
132
|
+
if isinstance(provider, type):
|
|
133
|
+
container.register_class(provider)
|
|
134
|
+
else:
|
|
135
|
+
container.register(provider, provider)
|
|
136
|
+
|
|
137
|
+
for ctrl in node.controllers:
|
|
138
|
+
if not container.has(ctrl):
|
|
139
|
+
container.register_class(ctrl)
|
|
140
|
+
|
|
141
|
+
for g in node.guards:
|
|
142
|
+
if isinstance(g, type) and not container.has(g):
|
|
143
|
+
container.register_class(g)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def walk_module_graph(root_module: type, container: Container) -> ModuleGraph:
|
|
147
|
+
"""Build the module graph and register all providers in one call.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
root_module: The root ``@module``-decorated class.
|
|
151
|
+
container: The DI container to register providers in.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The built module graph.
|
|
155
|
+
"""
|
|
156
|
+
graph = build_module_graph(root_module)
|
|
157
|
+
register_module_graph(graph, container)
|
|
158
|
+
return graph
|