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.
Files changed (211) hide show
  1. celerity/__init__.py +114 -0
  2. celerity/application.py +83 -0
  3. celerity/bootstrap/__init__.py +13 -0
  4. celerity/bootstrap/bootstrap.py +70 -0
  5. celerity/bootstrap/discovery.py +67 -0
  6. celerity/bootstrap/module_graph.py +158 -0
  7. celerity/bootstrap/runtime_entry.py +311 -0
  8. celerity/bootstrap/runtime_mapper.py +210 -0
  9. celerity/bootstrap/runtime_orchestrator.py +200 -0
  10. celerity/cli/__init__.py +0 -0
  11. celerity/cli/extract.py +97 -0
  12. celerity/cli/identity.py +89 -0
  13. celerity/cli/metadata_app.py +338 -0
  14. celerity/cli/serializer.py +460 -0
  15. celerity/cli/types.py +151 -0
  16. celerity/common/__init__.py +6 -0
  17. celerity/common/debug.py +146 -0
  18. celerity/common/path_utils.py +30 -0
  19. celerity/config/__init__.py +27 -0
  20. celerity/config/backends/__init__.py +1 -0
  21. celerity/config/backends/aws/__init__.py +1 -0
  22. celerity/config/backends/aws/lambda_extension.py +62 -0
  23. celerity/config/backends/aws/parameter_store.py +39 -0
  24. celerity/config/backends/aws/secrets_manager.py +39 -0
  25. celerity/config/backends/empty.py +16 -0
  26. celerity/config/backends/local.py +48 -0
  27. celerity/config/backends/types.py +21 -0
  28. celerity/config/layer.py +132 -0
  29. celerity/config/params.py +88 -0
  30. celerity/config/service.py +104 -0
  31. celerity/decorators/__init__.py +98 -0
  32. celerity/decorators/consumer.py +86 -0
  33. celerity/decorators/consumer_params.py +71 -0
  34. celerity/decorators/controller.py +46 -0
  35. celerity/decorators/guards.py +121 -0
  36. celerity/decorators/http.py +152 -0
  37. celerity/decorators/injectable.py +110 -0
  38. celerity/decorators/invoke.py +36 -0
  39. celerity/decorators/invoke_params.py +47 -0
  40. celerity/decorators/layer.py +73 -0
  41. celerity/decorators/metadata.py +62 -0
  42. celerity/decorators/module.py +74 -0
  43. celerity/decorators/params.py +200 -0
  44. celerity/decorators/resource.py +45 -0
  45. celerity/decorators/schedule.py +70 -0
  46. celerity/decorators/schedule_params.py +71 -0
  47. celerity/decorators/websocket.py +135 -0
  48. celerity/decorators/websocket_params.py +83 -0
  49. celerity/di/__init__.py +12 -0
  50. celerity/di/container.py +311 -0
  51. celerity/di/dependency_tokens.py +95 -0
  52. celerity/di/tokens.py +7 -0
  53. celerity/errors/__init__.py +35 -0
  54. celerity/errors/http_exception.py +83 -0
  55. celerity/functions/__init__.py +29 -0
  56. celerity/functions/consumer.py +60 -0
  57. celerity/functions/context.py +81 -0
  58. celerity/functions/custom.py +53 -0
  59. celerity/functions/guard.py +46 -0
  60. celerity/functions/http.py +80 -0
  61. celerity/functions/schedule.py +62 -0
  62. celerity/functions/websocket.py +56 -0
  63. celerity/handlers/__init__.py +5 -0
  64. celerity/handlers/consumer_pipeline.py +71 -0
  65. celerity/handlers/custom_pipeline.py +65 -0
  66. celerity/handlers/guard_pipeline.py +207 -0
  67. celerity/handlers/http_pipeline.py +123 -0
  68. celerity/handlers/module_resolver.py +52 -0
  69. celerity/handlers/param_extractor.py +258 -0
  70. celerity/handlers/registry.py +232 -0
  71. celerity/handlers/scanners/__init__.py +0 -0
  72. celerity/handlers/scanners/_utils.py +49 -0
  73. celerity/handlers/scanners/consumer.py +79 -0
  74. celerity/handlers/scanners/custom.py +74 -0
  75. celerity/handlers/scanners/http.py +155 -0
  76. celerity/handlers/scanners/schedule.py +78 -0
  77. celerity/handlers/scanners/websocket.py +71 -0
  78. celerity/handlers/schedule_pipeline.py +69 -0
  79. celerity/handlers/websocket_pipeline.py +53 -0
  80. celerity/layers/__init__.py +7 -0
  81. celerity/layers/dispose.py +26 -0
  82. celerity/layers/pipeline.py +80 -0
  83. celerity/layers/system.py +78 -0
  84. celerity/layers/validate.py +108 -0
  85. celerity/metadata/__init__.py +6 -0
  86. celerity/metadata/keys.py +77 -0
  87. celerity/metadata/store.py +48 -0
  88. celerity/py.typed +0 -0
  89. celerity/resources/__init__.py +0 -0
  90. celerity/resources/_common.py +104 -0
  91. celerity/resources/_tokens.py +80 -0
  92. celerity/resources/bucket/__init__.py +41 -0
  93. celerity/resources/bucket/errors.py +21 -0
  94. celerity/resources/bucket/factory.py +71 -0
  95. celerity/resources/bucket/layer.py +82 -0
  96. celerity/resources/bucket/params.py +65 -0
  97. celerity/resources/bucket/providers/__init__.py +0 -0
  98. celerity/resources/bucket/providers/s3/__init__.py +0 -0
  99. celerity/resources/bucket/providers/s3/client.py +360 -0
  100. celerity/resources/bucket/providers/s3/config.py +39 -0
  101. celerity/resources/bucket/providers/s3/listing.py +137 -0
  102. celerity/resources/bucket/providers/s3/types.py +18 -0
  103. celerity/resources/bucket/types.py +178 -0
  104. celerity/resources/cache/__init__.py +33 -0
  105. celerity/resources/cache/config.py +85 -0
  106. celerity/resources/cache/credentials.py +153 -0
  107. celerity/resources/cache/errors.py +14 -0
  108. celerity/resources/cache/factory.py +52 -0
  109. celerity/resources/cache/layer.py +96 -0
  110. celerity/resources/cache/params.py +65 -0
  111. celerity/resources/cache/providers/__init__.py +0 -0
  112. celerity/resources/cache/providers/redis/__init__.py +0 -0
  113. celerity/resources/cache/providers/redis/cache.py +651 -0
  114. celerity/resources/cache/providers/redis/client.py +138 -0
  115. celerity/resources/cache/providers/redis/cluster.py +105 -0
  116. celerity/resources/cache/providers/redis/iam/__init__.py +0 -0
  117. celerity/resources/cache/providers/redis/iam/elasticache_token.py +57 -0
  118. celerity/resources/cache/providers/redis/types.py +24 -0
  119. celerity/resources/cache/types.py +246 -0
  120. celerity/resources/datastore/__init__.py +61 -0
  121. celerity/resources/datastore/errors.py +29 -0
  122. celerity/resources/datastore/factory.py +100 -0
  123. celerity/resources/datastore/layer.py +84 -0
  124. celerity/resources/datastore/params.py +65 -0
  125. celerity/resources/datastore/providers/__init__.py +1 -0
  126. celerity/resources/datastore/providers/dynamodb/__init__.py +1 -0
  127. celerity/resources/datastore/providers/dynamodb/client.py +480 -0
  128. celerity/resources/datastore/providers/dynamodb/config.py +25 -0
  129. celerity/resources/datastore/providers/dynamodb/expressions.py +227 -0
  130. celerity/resources/datastore/providers/dynamodb/listing.py +96 -0
  131. celerity/resources/datastore/providers/dynamodb/marshall.py +27 -0
  132. celerity/resources/datastore/providers/dynamodb/types.py +17 -0
  133. celerity/resources/datastore/types.py +251 -0
  134. celerity/resources/queue/__init__.py +35 -0
  135. celerity/resources/queue/errors.py +22 -0
  136. celerity/resources/queue/factory.py +87 -0
  137. celerity/resources/queue/layer.py +84 -0
  138. celerity/resources/queue/params.py +65 -0
  139. celerity/resources/queue/providers/__init__.py +0 -0
  140. celerity/resources/queue/providers/redis/__init__.py +0 -0
  141. celerity/resources/queue/providers/redis/client.py +186 -0
  142. celerity/resources/queue/providers/redis/types.py +12 -0
  143. celerity/resources/queue/providers/sqs/__init__.py +0 -0
  144. celerity/resources/queue/providers/sqs/client.py +225 -0
  145. celerity/resources/queue/providers/sqs/config.py +25 -0
  146. celerity/resources/queue/providers/sqs/types.py +17 -0
  147. celerity/resources/queue/types.py +103 -0
  148. celerity/resources/sql_database/__init__.py +61 -0
  149. celerity/resources/sql_database/config.py +132 -0
  150. celerity/resources/sql_database/credentials.py +150 -0
  151. celerity/resources/sql_database/errors.py +21 -0
  152. celerity/resources/sql_database/factory.py +123 -0
  153. celerity/resources/sql_database/layer.py +89 -0
  154. celerity/resources/sql_database/params.py +160 -0
  155. celerity/resources/sql_database/providers/__init__.py +0 -0
  156. celerity/resources/sql_database/providers/rds/__init__.py +0 -0
  157. celerity/resources/sql_database/providers/rds/iam.py +71 -0
  158. celerity/resources/sql_database/types.py +85 -0
  159. celerity/resources/topic/__init__.py +35 -0
  160. celerity/resources/topic/errors.py +22 -0
  161. celerity/resources/topic/factory.py +86 -0
  162. celerity/resources/topic/layer.py +84 -0
  163. celerity/resources/topic/params.py +54 -0
  164. celerity/resources/topic/providers/__init__.py +0 -0
  165. celerity/resources/topic/providers/redis/__init__.py +0 -0
  166. celerity/resources/topic/providers/redis/client.py +185 -0
  167. celerity/resources/topic/providers/redis/types.py +12 -0
  168. celerity/resources/topic/providers/sns/__init__.py +0 -0
  169. celerity/resources/topic/providers/sns/client.py +225 -0
  170. celerity/resources/topic/providers/sns/config.py +25 -0
  171. celerity/resources/topic/providers/sns/types.py +17 -0
  172. celerity/resources/topic/types.py +102 -0
  173. celerity/serverless/__init__.py +0 -0
  174. celerity/serverless/aws/__init__.py +5 -0
  175. celerity/serverless/aws/adapter.py +313 -0
  176. celerity/serverless/aws/event_mapper.py +330 -0
  177. celerity/serverless/aws/websocket_sender.py +54 -0
  178. celerity/serverless/azure/__init__.py +0 -0
  179. celerity/serverless/gcp/__init__.py +0 -0
  180. celerity/telemetry/__init__.py +45 -0
  181. celerity/telemetry/context.py +29 -0
  182. celerity/telemetry/env.py +67 -0
  183. celerity/telemetry/helpers.py +22 -0
  184. celerity/telemetry/init.py +121 -0
  185. celerity/telemetry/instrumentations.py +79 -0
  186. celerity/telemetry/logger.py +122 -0
  187. celerity/telemetry/noop.py +48 -0
  188. celerity/telemetry/request_context.py +68 -0
  189. celerity/telemetry/telemetry_layer.py +195 -0
  190. celerity/telemetry/tracer.py +59 -0
  191. celerity/testing/__init__.py +17 -0
  192. celerity/testing/mocks.py +146 -0
  193. celerity/testing/test_app.py +278 -0
  194. celerity/types/__init__.py +106 -0
  195. celerity/types/common.py +36 -0
  196. celerity/types/consumer.py +98 -0
  197. celerity/types/container.py +56 -0
  198. celerity/types/context.py +81 -0
  199. celerity/types/guard.py +46 -0
  200. celerity/types/handler.py +108 -0
  201. celerity/types/http.py +40 -0
  202. celerity/types/layer.py +40 -0
  203. celerity/types/module.py +44 -0
  204. celerity/types/schedule.py +17 -0
  205. celerity/types/telemetry.py +74 -0
  206. celerity/types/websocket.py +72 -0
  207. celerity_sdk-0.2.0.dist-info/METADATA +92 -0
  208. celerity_sdk-0.2.0.dist-info/RECORD +211 -0
  209. celerity_sdk-0.2.0.dist-info/WHEEL +4 -0
  210. celerity_sdk-0.2.0.dist-info/entry_points.txt +2 -0
  211. 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
+ ]
@@ -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