forge-core-di 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.
- forge_core_di-0.2.0.dist-info/METADATA +58 -0
- forge_core_di-0.2.0.dist-info/RECORD +49 -0
- forge_core_di-0.2.0.dist-info/WHEEL +4 -0
- forge_core_di-0.2.0.dist-info/licenses/LICENSE +9 -0
- forgecore/__init__.py +88 -0
- forgecore/application/__init__.py +7 -0
- forgecore/application/app.py +94 -0
- forgecore/application/metadata.py +11 -0
- forgecore/config/__init__.py +5 -0
- forgecore/config/config.py +23 -0
- forgecore/context/__init__.py +5 -0
- forgecore/context/context.py +23 -0
- forgecore/core/__init__.py +31 -0
- forgecore/core/constants.py +7 -0
- forgecore/core/exceptions.py +18 -0
- forgecore/core/types.py +4 -0
- forgecore/core/version.py +3 -0
- forgecore/lifecycle/__init__.py +7 -0
- forgecore/lifecycle/manager.py +40 -0
- forgecore/lifecycle/state.py +11 -0
- forgecore/logging/__init__.py +5 -0
- forgecore/logging/logger.py +23 -0
- forgecore/plugins/__init__.py +7 -0
- forgecore/plugins/manager.py +31 -0
- forgecore/plugins/plugin.py +21 -0
- forgecore/providers/__init__.py +5 -0
- forgecore/providers/provider.py +14 -0
- forgecore/registry/__init__.py +7 -0
- forgecore/registry/exceptions.py +2 -0
- forgecore/registry/registry.py +31 -0
- forgecore/runtime/__init__.py +9 -0
- forgecore/runtime/bus.py +50 -0
- forgecore/runtime/event.py +10 -0
- forgecore/runtime/exceptions.py +2 -0
- forgecore/services/__init__.py +20 -0
- forgecore/services/autowire.py +27 -0
- forgecore/services/container.py +103 -0
- forgecore/services/descriptor.py +16 -0
- forgecore/services/exceptions.py +6 -0
- forgecore/services/factory.py +22 -0
- forgecore/services/resolver.py +165 -0
- forgecore/services/scope.py +7 -0
- forgecore/services/scope_context.py +31 -0
- forgecore/utils/__init__.py +9 -0
- forgecore/utils/inspect.py +7 -0
- forgecore/utils/system.py +15 -0
- forgecore/validation/__init__.py +15 -0
- forgecore/validation/validators.py +21 -0
- forgecore/version.py +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-core-di
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Lightweight Dependency Injection and Application Runtime Core for Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/akmallmline/forgecore
|
|
6
|
+
Project-URL: Repository, https://github.com/akmallmline/forgecore
|
|
7
|
+
Author-email: Akmal Maulana <akmallmline@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: application-kernel,autowiring,dependency-injection,di-container,framework,lifecycle,plugin,python
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# ForgeCore
|
|
22
|
+
|
|
23
|
+
Lightweight Dependency Injection and Application Runtime Core for Python.
|
|
24
|
+
|
|
25
|
+
ForgeCore is a framework engine — not just a DI container. It provides the
|
|
26
|
+
foundation for building web frameworks, CLI tools, bots, trading systems,
|
|
27
|
+
and AI agents in Python.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Explicit DI container — no magic auto-registration
|
|
34
|
+
- Constructor autowiring via type hints
|
|
35
|
+
- Singleton, Transient, and Scoped lifecycles
|
|
36
|
+
- Thread-safe scoped instances via `threading.local()`
|
|
37
|
+
- Circular dependency detection with clear error messages
|
|
38
|
+
- Optional injection (`Cache | None = None`)
|
|
39
|
+
- Service binding — abstraction to implementation mapping
|
|
40
|
+
- Custom factory support
|
|
41
|
+
- Application kernel with boot pipeline
|
|
42
|
+
- Service provider system for modular registration
|
|
43
|
+
- Lifecycle hooks (`on_start`, `on_stop`)
|
|
44
|
+
- Event system with subscribe, emit, and unsubscribe
|
|
45
|
+
- Plugin system for runtime extension
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Python 3.12+
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install forgecore
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
forgecore/__init__.py,sha256=RFUPWZq6cx9eKEbAhwYbi6dh4bz7AHeGvjNwjm10BfA,1844
|
|
2
|
+
forgecore/version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
3
|
+
forgecore/application/__init__.py,sha256=CybBmP2gBeXOT7HFENFlpqc3Qaz1iCgeh4Tm6Fcgszo,126
|
|
4
|
+
forgecore/application/app.py,sha256=6XukXSCyX2nJ0UThoUNfWVxxMxtiTmIYVtCyQzGd6Ak,2576
|
|
5
|
+
forgecore/application/metadata.py,sha256=-_W7NUZypbu0R31flRwabQPkQLLoo9ZAWmQyPCRQ7Is,213
|
|
6
|
+
forgecore/config/__init__.py,sha256=0RaUZX5uZjJ6Xw8ShA63aflFq9se3QCq0rZ_jRUlePE,56
|
|
7
|
+
forgecore/config/config.py,sha256=I8oZYsHnt6ulntZ7e3mqlOodKGdP2NW7s34MlGG2ZVw,586
|
|
8
|
+
forgecore/context/__init__.py,sha256=yG8qxX4kXW8FMVHF2ThA90c8JpyRh-bFAgsmtcy7X6s,81
|
|
9
|
+
forgecore/context/context.py,sha256=XiR9MAB4FFwiQ6TjLyrQWKRTAjXOsYOIvp7b6JMRZf4,693
|
|
10
|
+
forgecore/core/__init__.py,sha256=q0W5AJvgMXy55WwaMuVAZeLfSZW0tGcqNEqqSJmNZFo,622
|
|
11
|
+
forgecore/core/constants.py,sha256=oprb761hTUWhWtzCKqXLwaT4ZxhHGx58DVxAW37-yac,94
|
|
12
|
+
forgecore/core/exceptions.py,sha256=GRLE7HhH_1Fxjqya98Yb--niRrYfcxC9j-IBMnKn18A,258
|
|
13
|
+
forgecore/core/types.py,sha256=7cGY2289h8ITPA_DUdpHd0p1c8G2UCwwA_16efV9lJY,101
|
|
14
|
+
forgecore/core/version.py,sha256=zpHIM4v-sIMttitw4o_oPc-eiqXwmlmmcSwU6b3-XYw,63
|
|
15
|
+
forgecore/lifecycle/__init__.py,sha256=Qzb7y4-1r3Gi0Rl9rYoK3_LCyTeatozUtGMj1JORwws,133
|
|
16
|
+
forgecore/lifecycle/manager.py,sha256=K7dDScD90iwpm8ee_3bxj4sIngjP8JtW1oDsGf4nV88,1019
|
|
17
|
+
forgecore/lifecycle/state.py,sha256=__lsC57gjdvand2xfVr4CNdqT412PPf8_fZd4RPH80w,225
|
|
18
|
+
forgecore/logging/__init__.py,sha256=7O9U4WeO02q8Wdo5t57SwkfcWdFlj_pONt_yS77fqKM,64
|
|
19
|
+
forgecore/logging/logger.py,sha256=TKKIn5pDKSjBSZEMB6YCrU_SGnPLKljxhEYHD3LiSDA,473
|
|
20
|
+
forgecore/plugins/__init__.py,sha256=So8druVzdZh_0oMVAthd9g7F7gFBgXKgyQaYLK2QXXg,112
|
|
21
|
+
forgecore/plugins/manager.py,sha256=H4VbYM2i2kp69d6ePWPq6JFo8d8WZpgdoUFPdZGQCh8,881
|
|
22
|
+
forgecore/plugins/plugin.py,sha256=6PSA7ji51MuFgtFDpwujHTRVmvLt4ruIzwSJRyoPxZs,442
|
|
23
|
+
forgecore/providers/__init__.py,sha256=KH3PLIy3j2poxlkVuAGYDp-LWfcRpZfnVUtnkXOCJJc,76
|
|
24
|
+
forgecore/providers/provider.py,sha256=z60qg3V6aGoYZoIE9DAlboOI_ZPiJ9kQi00_6Q-BaHY,375
|
|
25
|
+
forgecore/registry/__init__.py,sha256=Idcw3PMSfRgbhoYNTdQYwRS39FQuAVKuYS36i3X1xhQ,127
|
|
26
|
+
forgecore/registry/exceptions.py,sha256=y2a-WMmN7g5ke8c4xxhXDQtCiQaZ3_nh-0mNjALfWvw,88
|
|
27
|
+
forgecore/registry/registry.py,sha256=8F_qtK-1BEtZ_UdIgbLDyXZN_eF-k2AMcuHOazcgQGY,760
|
|
28
|
+
forgecore/runtime/__init__.py,sha256=WG-BKQVmHfEw2hzoIygv103RErx9QAEph8QZLFPMeMY,148
|
|
29
|
+
forgecore/runtime/bus.py,sha256=3G53_WqvbCmU3nrMuFFQZoL-LlP3RFATNOQOoQvlNe8,1378
|
|
30
|
+
forgecore/runtime/event.py,sha256=XuOSsiMkSXUiwL8cu5yBIqcOjOhBJIwjGfcLZwqmE_c,223
|
|
31
|
+
forgecore/runtime/exceptions.py,sha256=CO6XUqOWxYZdaKpp8CyTnuo5xt5yC0RSVVY9UGiWrJU,38
|
|
32
|
+
forgecore/services/__init__.py,sha256=1tNor8SQGwpqHd6pnpKN3MAA5xMeJXDHDHqAs_yHYY8,585
|
|
33
|
+
forgecore/services/autowire.py,sha256=ZRR-Bq2q4ucWuWFW3bzB1kZSuNVAsYKsz0s736GhbTU,678
|
|
34
|
+
forgecore/services/container.py,sha256=UzFwBeiPfBHb7SR5svI6i_jR0vTfkA6atv-VAqqAukU,2985
|
|
35
|
+
forgecore/services/descriptor.py,sha256=CfBcSiSEtszMMLnYbnKcfqS0PAVvXpf7S17uTApNlIo,393
|
|
36
|
+
forgecore/services/exceptions.py,sha256=_mOMlzWb7lRJaBMjtOqr5J0DGVx96t4Ir253yyayuHE,103
|
|
37
|
+
forgecore/services/factory.py,sha256=IC3dxbaIbnTZFArjP9Ya0FDvuAcv1oK_aM0HiPHJ7Mo,420
|
|
38
|
+
forgecore/services/resolver.py,sha256=y30CB41bdX3AFXrYENYOIwmcFqC1mwkxfYzQS65Y7rY,5494
|
|
39
|
+
forgecore/services/scope.py,sha256=9Rzl8I80OtDiatYGqAwXa4HRFxXwKtp6C9zoCMjOzeQ,133
|
|
40
|
+
forgecore/services/scope_context.py,sha256=cUill6RM9nfPaOUohBTPqHYmBPknba4E8a1rKT2rNEU,847
|
|
41
|
+
forgecore/utils/__init__.py,sha256=DKLqJfaX_VE7q0vey86hC54Y06d8i6UyY5xciXNbupw,198
|
|
42
|
+
forgecore/utils/inspect.py,sha256=RDrELdiOSVPT5p8YHmmZCY3_T3JfATTbZfCu84g835A,147
|
|
43
|
+
forgecore/utils/system.py,sha256=JHcahq-Fi2PtoG3yfRmrKhyNroFgMXmSdnZGAgHxGmE,236
|
|
44
|
+
forgecore/validation/__init__.py,sha256=Ho1O6qPjirsYP0hmy7usPJ_ILY4Fv1p36PuPSQbH6IU,225
|
|
45
|
+
forgecore/validation/validators.py,sha256=B2_8NZ8eK8J5Z9U5y16-9-8q-aJqkNq9qF9D0gtrcrA,443
|
|
46
|
+
forge_core_di-0.2.0.dist-info/METADATA,sha256=Wsz3b35js6JAhQ3uk8K_ND7JsWiLGyGpXEc60uixKkE,1878
|
|
47
|
+
forge_core_di-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
48
|
+
forge_core_di-0.2.0.dist-info/licenses/LICENSE,sha256=lLcy2cvfVluby9-jyzLHJGt2v2m3NkTAhEnVSDSJC0c,377
|
|
49
|
+
forge_core_di-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akmal Maulana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software.
|
forgecore/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from .application import ApplicationMetadata, ForgeApp
|
|
2
|
+
from .config import Config
|
|
3
|
+
from .core import (
|
|
4
|
+
DEFAULT_ENCODING,
|
|
5
|
+
DEFAULT_TIMEOUT,
|
|
6
|
+
JSON,
|
|
7
|
+
MAX_RETRIES,
|
|
8
|
+
PACKAGE_NAME,
|
|
9
|
+
VERSION,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
ForgeCoreError,
|
|
12
|
+
InitializationError,
|
|
13
|
+
JSONList,
|
|
14
|
+
UnsupportedPlatformError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
__version__,
|
|
17
|
+
)
|
|
18
|
+
from .lifecycle import LifecycleManager, LifecycleState
|
|
19
|
+
from .logging import get_logger
|
|
20
|
+
from .plugins import Plugin, PluginManager
|
|
21
|
+
from .providers import ServiceProvider
|
|
22
|
+
from .registry import Registry
|
|
23
|
+
from .runtime import Event, EventBus, EventError
|
|
24
|
+
from .services import (
|
|
25
|
+
CircularDependencyError,
|
|
26
|
+
ScopeContext,
|
|
27
|
+
ServiceContainer,
|
|
28
|
+
ServiceDescriptor,
|
|
29
|
+
ServiceNotFoundError,
|
|
30
|
+
ServiceScope,
|
|
31
|
+
)
|
|
32
|
+
from .utils import (
|
|
33
|
+
machine,
|
|
34
|
+
operating_system,
|
|
35
|
+
package_version,
|
|
36
|
+
python_version,
|
|
37
|
+
)
|
|
38
|
+
from .validation import (
|
|
39
|
+
is_boolean,
|
|
40
|
+
is_float,
|
|
41
|
+
is_integer,
|
|
42
|
+
is_non_empty_string,
|
|
43
|
+
is_string,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"__version__",
|
|
48
|
+
"VERSION",
|
|
49
|
+
"PACKAGE_NAME",
|
|
50
|
+
"DEFAULT_ENCODING",
|
|
51
|
+
"DEFAULT_TIMEOUT",
|
|
52
|
+
"MAX_RETRIES",
|
|
53
|
+
"ForgeCoreError",
|
|
54
|
+
"ConfigurationError",
|
|
55
|
+
"ValidationError",
|
|
56
|
+
"InitializationError",
|
|
57
|
+
"UnsupportedPlatformError",
|
|
58
|
+
"JSON",
|
|
59
|
+
"JSONList",
|
|
60
|
+
"ForgeApp",
|
|
61
|
+
"ApplicationMetadata",
|
|
62
|
+
"ServiceContainer",
|
|
63
|
+
"ServiceProvider",
|
|
64
|
+
"ServiceScope",
|
|
65
|
+
"ServiceDescriptor",
|
|
66
|
+
"ServiceNotFoundError",
|
|
67
|
+
"CircularDependencyError",
|
|
68
|
+
"ScopeContext",
|
|
69
|
+
"Event",
|
|
70
|
+
"EventBus",
|
|
71
|
+
"EventError",
|
|
72
|
+
"LifecycleManager",
|
|
73
|
+
"LifecycleState",
|
|
74
|
+
"Registry",
|
|
75
|
+
"Plugin",
|
|
76
|
+
"PluginManager",
|
|
77
|
+
"Config",
|
|
78
|
+
"get_logger",
|
|
79
|
+
"package_version",
|
|
80
|
+
"python_version",
|
|
81
|
+
"operating_system",
|
|
82
|
+
"machine",
|
|
83
|
+
"is_string",
|
|
84
|
+
"is_integer",
|
|
85
|
+
"is_float",
|
|
86
|
+
"is_boolean",
|
|
87
|
+
"is_non_empty_string",
|
|
88
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from forgecore.context import ApplicationContext
|
|
4
|
+
from forgecore.lifecycle import LifecycleState
|
|
5
|
+
from forgecore.providers import ServiceProvider
|
|
6
|
+
from forgecore.runtime import Event
|
|
7
|
+
|
|
8
|
+
from .metadata import ApplicationMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ForgeApp:
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
metadata: ApplicationMetadata | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
if metadata is None:
|
|
18
|
+
metadata = ApplicationMetadata(
|
|
19
|
+
name="Forge Application",
|
|
20
|
+
version="0.0.0",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
self.context = ApplicationContext(metadata)
|
|
24
|
+
self._providers: list[ServiceProvider] = []
|
|
25
|
+
self._booted: bool = False
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def metadata(self) -> ApplicationMetadata:
|
|
29
|
+
return self.context.metadata
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def services(self):
|
|
33
|
+
return self.context.services
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def registry(self):
|
|
37
|
+
return self.context.registry
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def events(self):
|
|
41
|
+
return self.context.events
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def lifecycle(self):
|
|
45
|
+
return self.context.lifecycle
|
|
46
|
+
|
|
47
|
+
def register_provider(self, provider: ServiceProvider) -> None:
|
|
48
|
+
if self._booted:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"Cannot register provider after application has booted."
|
|
51
|
+
)
|
|
52
|
+
self._providers.append(provider)
|
|
53
|
+
|
|
54
|
+
def boot(self) -> None:
|
|
55
|
+
if self._booted:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
for provider in self._providers:
|
|
59
|
+
provider.register(self.context.services)
|
|
60
|
+
|
|
61
|
+
for provider in self._providers:
|
|
62
|
+
provider.boot(self.context.services)
|
|
63
|
+
|
|
64
|
+
self._booted = True
|
|
65
|
+
self.context.events.emit(Event(name="app.booted"))
|
|
66
|
+
|
|
67
|
+
def run(self) -> None:
|
|
68
|
+
if self.lifecycle.state is not LifecycleState.INITIALIZED:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
"Application is already running or has been stopped."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.boot()
|
|
74
|
+
|
|
75
|
+
self.lifecycle.on_start(
|
|
76
|
+
lambda: self.context.events.emit(Event(name="app.starting"))
|
|
77
|
+
)
|
|
78
|
+
self.lifecycle.on_stop(
|
|
79
|
+
lambda: self.context.events.emit(Event(name="app.stopping"))
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.lifecycle.start()
|
|
83
|
+
self.lifecycle.run()
|
|
84
|
+
self.context.events.emit(Event(name="app.running"))
|
|
85
|
+
|
|
86
|
+
def stop(self) -> None:
|
|
87
|
+
if self.lifecycle.state is not LifecycleState.RUNNING:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"Application is not running."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.lifecycle.stop()
|
|
93
|
+
self.lifecycle.shutdown()
|
|
94
|
+
self.context.events.emit(Event(name="app.stopped"))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Config:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._data: dict[str, Any] = {}
|
|
11
|
+
|
|
12
|
+
def load_json(self, path: str | Path) -> None:
|
|
13
|
+
with open(path, encoding="utf-8") as file:
|
|
14
|
+
self._data = json.load(file)
|
|
15
|
+
|
|
16
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
17
|
+
return self._data.get(key, default)
|
|
18
|
+
|
|
19
|
+
def set(self, key: str, value: Any) -> None:
|
|
20
|
+
self._data[key] = value
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
return dict(self._data)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from forgecore.application.metadata import ApplicationMetadata
|
|
4
|
+
from forgecore.lifecycle import LifecycleManager
|
|
5
|
+
from forgecore.registry import Registry
|
|
6
|
+
from forgecore.runtime import EventBus
|
|
7
|
+
from forgecore.services import ServiceContainer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApplicationContext:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
metadata: ApplicationMetadata | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
self.metadata = metadata or ApplicationMetadata(
|
|
16
|
+
name="Forge Application",
|
|
17
|
+
version="0.0.0",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.services = ServiceContainer()
|
|
21
|
+
self.registry = Registry()
|
|
22
|
+
self.events = EventBus()
|
|
23
|
+
self.lifecycle = LifecycleManager()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .constants import (
|
|
2
|
+
DEFAULT_ENCODING,
|
|
3
|
+
DEFAULT_TIMEOUT,
|
|
4
|
+
MAX_RETRIES,
|
|
5
|
+
PACKAGE_NAME,
|
|
6
|
+
)
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
ConfigurationError,
|
|
9
|
+
ForgeCoreError,
|
|
10
|
+
InitializationError,
|
|
11
|
+
UnsupportedPlatformError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .types import JSON, JSONList
|
|
15
|
+
from .version import VERSION, __version__
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"PACKAGE_NAME",
|
|
19
|
+
"DEFAULT_ENCODING",
|
|
20
|
+
"DEFAULT_TIMEOUT",
|
|
21
|
+
"MAX_RETRIES",
|
|
22
|
+
"ForgeCoreError",
|
|
23
|
+
"ConfigurationError",
|
|
24
|
+
"ValidationError",
|
|
25
|
+
"InitializationError",
|
|
26
|
+
"UnsupportedPlatformError",
|
|
27
|
+
"VERSION",
|
|
28
|
+
"__version__",
|
|
29
|
+
"JSON",
|
|
30
|
+
"JSONList",
|
|
31
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ForgeCoreError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(ForgeCoreError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ValidationError(ForgeCoreError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InitializationError(ForgeCoreError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnsupportedPlatformError(ForgeCoreError):
|
|
18
|
+
pass
|
forgecore/core/types.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from .state import LifecycleState
|
|
6
|
+
|
|
7
|
+
Hook = Callable[[], None]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LifecycleManager:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._state = LifecycleState.INITIALIZED
|
|
13
|
+
self._on_start_hooks: list[Hook] = []
|
|
14
|
+
self._on_stop_hooks: list[Hook] = []
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def state(self) -> LifecycleState:
|
|
18
|
+
return self._state
|
|
19
|
+
|
|
20
|
+
def on_start(self, hook: Hook) -> None:
|
|
21
|
+
self._on_start_hooks.append(hook)
|
|
22
|
+
|
|
23
|
+
def on_stop(self, hook: Hook) -> None:
|
|
24
|
+
self._on_stop_hooks.append(hook)
|
|
25
|
+
|
|
26
|
+
def start(self) -> None:
|
|
27
|
+
self._state = LifecycleState.STARTING
|
|
28
|
+
for hook in self._on_start_hooks:
|
|
29
|
+
hook()
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
self._state = LifecycleState.RUNNING
|
|
33
|
+
|
|
34
|
+
def stop(self) -> None:
|
|
35
|
+
self._state = LifecycleState.STOPPING
|
|
36
|
+
for hook in reversed(self._on_stop_hooks):
|
|
37
|
+
hook()
|
|
38
|
+
|
|
39
|
+
def shutdown(self) -> None:
|
|
40
|
+
self._state = LifecycleState.STOPPED
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
_FORMAT = "[%(asctime)s] %(levelname)s %(name)s: %(message)s"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_logger(name: str) -> logging.Logger:
|
|
10
|
+
logger = logging.getLogger(name)
|
|
11
|
+
|
|
12
|
+
if logger.handlers:
|
|
13
|
+
return logger
|
|
14
|
+
|
|
15
|
+
logger.setLevel(logging.INFO)
|
|
16
|
+
|
|
17
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
18
|
+
handler.setFormatter(logging.Formatter(_FORMAT))
|
|
19
|
+
|
|
20
|
+
logger.addHandler(handler)
|
|
21
|
+
logger.propagate = False
|
|
22
|
+
|
|
23
|
+
return logger
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from forgecore.context import ApplicationContext
|
|
4
|
+
|
|
5
|
+
from .plugin import Plugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PluginManager:
|
|
9
|
+
def __init__(self, context: ApplicationContext) -> None:
|
|
10
|
+
self._context = context
|
|
11
|
+
self._plugins: dict[str, Plugin] = {}
|
|
12
|
+
|
|
13
|
+
def register(self, plugin: Plugin) -> None:
|
|
14
|
+
self._plugins[plugin.name] = plugin
|
|
15
|
+
|
|
16
|
+
def unregister(self, name: str) -> None:
|
|
17
|
+
self._plugins.pop(name, None)
|
|
18
|
+
|
|
19
|
+
def get(self, name: str) -> Plugin | None:
|
|
20
|
+
return self._plugins.get(name)
|
|
21
|
+
|
|
22
|
+
def all(self) -> tuple[Plugin, ...]:
|
|
23
|
+
return tuple(self._plugins.values())
|
|
24
|
+
|
|
25
|
+
def start_all(self) -> None:
|
|
26
|
+
for plugin in self._plugins.values():
|
|
27
|
+
plugin.start(self._context)
|
|
28
|
+
|
|
29
|
+
def stop_all(self) -> None:
|
|
30
|
+
for plugin in reversed(tuple(self._plugins.values())):
|
|
31
|
+
plugin.stop(self._context)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from forgecore.context import ApplicationContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Plugin(ABC):
|
|
9
|
+
@property
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def name(self) -> str: ...
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def version(self) -> str: ...
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def start(self, context: ApplicationContext) -> None: ...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def stop(self, context: ApplicationContext) -> None: ...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from forgecore.services import ServiceContainer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ServiceProvider(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def register(self, container: ServiceContainer) -> None:
|
|
11
|
+
"""Register services into the container."""
|
|
12
|
+
|
|
13
|
+
def boot(self, container: ServiceContainer) -> None:
|
|
14
|
+
"""Boot the provider."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .exceptions import RegistryKeyError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Registry:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._items: dict[str, Any] = {}
|
|
11
|
+
|
|
12
|
+
def register(self, key: str, value: Any) -> None:
|
|
13
|
+
self._items[key] = value
|
|
14
|
+
|
|
15
|
+
def get(self, key: str) -> Any:
|
|
16
|
+
try:
|
|
17
|
+
return self._items[key]
|
|
18
|
+
except KeyError as exc:
|
|
19
|
+
raise RegistryKeyError(f"Registry key '{key}' is not registered.") from exc
|
|
20
|
+
|
|
21
|
+
def contains(self, key: str) -> bool:
|
|
22
|
+
return key in self._items
|
|
23
|
+
|
|
24
|
+
def remove(self, key: str) -> None:
|
|
25
|
+
self._items.pop(key, None)
|
|
26
|
+
|
|
27
|
+
def clear(self) -> None:
|
|
28
|
+
self._items.clear()
|
|
29
|
+
|
|
30
|
+
def __len__(self) -> int:
|
|
31
|
+
return len(self._items)
|
forgecore/runtime/bus.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from .event import Event
|
|
7
|
+
from .exceptions import EventError
|
|
8
|
+
|
|
9
|
+
Listener = Callable[[Event], None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EventBus:
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._listeners: dict[str, list[Listener]] = defaultdict(list)
|
|
16
|
+
|
|
17
|
+
def subscribe(
|
|
18
|
+
self,
|
|
19
|
+
event_name: str,
|
|
20
|
+
listener: Listener,
|
|
21
|
+
) -> None:
|
|
22
|
+
self._listeners[event_name].append(listener)
|
|
23
|
+
|
|
24
|
+
def unsubscribe(
|
|
25
|
+
self,
|
|
26
|
+
event_name: str,
|
|
27
|
+
listener: Listener,
|
|
28
|
+
) -> None:
|
|
29
|
+
if event_name in self._listeners:
|
|
30
|
+
self._listeners[event_name] = [
|
|
31
|
+
l for l in self._listeners[event_name] if l is not listener
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def emit(self, event: Event) -> None:
|
|
35
|
+
for listener in list(self._listeners[event.name]):
|
|
36
|
+
try:
|
|
37
|
+
listener(event)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
raise EventError(
|
|
40
|
+
f"Listener failed for event '{event.name}': {exc}"
|
|
41
|
+
) from exc
|
|
42
|
+
|
|
43
|
+
def has_listeners(self, event_name: str) -> bool:
|
|
44
|
+
return bool(self._listeners.get(event_name))
|
|
45
|
+
|
|
46
|
+
def clear(self, event_name: str | None = None) -> None:
|
|
47
|
+
if event_name is not None:
|
|
48
|
+
self._listeners.pop(event_name, None)
|
|
49
|
+
else:
|
|
50
|
+
self._listeners.clear()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .autowire import constructor_dependencies
|
|
2
|
+
from .container import ServiceContainer
|
|
3
|
+
from .descriptor import ServiceDescriptor
|
|
4
|
+
from .exceptions import CircularDependencyError, ServiceNotFoundError
|
|
5
|
+
from .factory import ServiceFactory
|
|
6
|
+
from .resolver import ServiceResolver
|
|
7
|
+
from .scope import ServiceScope
|
|
8
|
+
from .scope_context import ScopeContext
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ServiceContainer",
|
|
12
|
+
"ServiceNotFoundError",
|
|
13
|
+
"CircularDependencyError",
|
|
14
|
+
"ServiceScope",
|
|
15
|
+
"ServiceDescriptor",
|
|
16
|
+
"constructor_dependencies",
|
|
17
|
+
"ServiceResolver",
|
|
18
|
+
"ServiceFactory",
|
|
19
|
+
"ScopeContext",
|
|
20
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from inspect import signature
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def constructor_dependencies(cls: type[Any]) -> list[type[Any]]:
|
|
8
|
+
"""
|
|
9
|
+
Return constructor dependencies from type annotations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
parameters = signature(cls.__init__).parameters.values()
|
|
13
|
+
|
|
14
|
+
dependencies: list[type[Any]] = []
|
|
15
|
+
|
|
16
|
+
for parameter in parameters:
|
|
17
|
+
if parameter.name == "self":
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
if parameter.annotation is parameter.empty:
|
|
21
|
+
raise TypeError(
|
|
22
|
+
f"{cls.__name__}.{parameter.name} is missing a type annotation."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
dependencies.append(parameter.annotation)
|
|
26
|
+
|
|
27
|
+
return dependencies
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .descriptor import ServiceDescriptor
|
|
6
|
+
from .exceptions import ServiceNotFoundError
|
|
7
|
+
from .factory import ServiceFactory
|
|
8
|
+
from .resolver import ServiceResolver
|
|
9
|
+
from .scope import ServiceScope
|
|
10
|
+
from .scope_context import ScopeContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ServiceContainer:
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._services: dict[type[Any], ServiceDescriptor] = {}
|
|
17
|
+
self._bindings: dict[type[Any], type[Any]] = {}
|
|
18
|
+
self._resolver = ServiceResolver(self)
|
|
19
|
+
|
|
20
|
+
def register(
|
|
21
|
+
self,
|
|
22
|
+
service: Any,
|
|
23
|
+
*,
|
|
24
|
+
scope: ServiceScope = ServiceScope.SINGLETON,
|
|
25
|
+
) -> None:
|
|
26
|
+
service_type = type(service)
|
|
27
|
+
self._services[service_type] = ServiceDescriptor(
|
|
28
|
+
service_type=service_type,
|
|
29
|
+
implementation=service_type,
|
|
30
|
+
scope=scope,
|
|
31
|
+
instance=service,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def bind(
|
|
35
|
+
self,
|
|
36
|
+
abstraction: type[Any],
|
|
37
|
+
implementation: type[Any],
|
|
38
|
+
) -> None:
|
|
39
|
+
self._bindings[abstraction] = implementation
|
|
40
|
+
|
|
41
|
+
def singleton(
|
|
42
|
+
self,
|
|
43
|
+
service_type: type[Any],
|
|
44
|
+
factory: callable | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
descriptor = ServiceDescriptor(
|
|
47
|
+
service_type=service_type,
|
|
48
|
+
implementation=service_type,
|
|
49
|
+
scope=ServiceScope.SINGLETON,
|
|
50
|
+
)
|
|
51
|
+
if factory is not None:
|
|
52
|
+
descriptor.factory = ServiceFactory(factory)
|
|
53
|
+
self._services[service_type] = descriptor
|
|
54
|
+
|
|
55
|
+
def transient(
|
|
56
|
+
self,
|
|
57
|
+
service_type: type[Any],
|
|
58
|
+
factory: callable | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
descriptor = ServiceDescriptor(
|
|
61
|
+
service_type=service_type,
|
|
62
|
+
implementation=service_type,
|
|
63
|
+
scope=ServiceScope.TRANSIENT,
|
|
64
|
+
)
|
|
65
|
+
if factory is not None:
|
|
66
|
+
descriptor.factory = ServiceFactory(factory)
|
|
67
|
+
self._services[service_type] = descriptor
|
|
68
|
+
|
|
69
|
+
def scoped(
|
|
70
|
+
self,
|
|
71
|
+
service_type: type[Any],
|
|
72
|
+
factory: callable | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
descriptor = ServiceDescriptor(
|
|
75
|
+
service_type=service_type,
|
|
76
|
+
implementation=service_type,
|
|
77
|
+
scope=ServiceScope.SCOPED,
|
|
78
|
+
)
|
|
79
|
+
if factory is not None:
|
|
80
|
+
descriptor.factory = ServiceFactory(factory)
|
|
81
|
+
self._services[service_type] = descriptor
|
|
82
|
+
|
|
83
|
+
def begin_scope(self) -> ScopeContext:
|
|
84
|
+
return self._resolver.begin_scope()
|
|
85
|
+
|
|
86
|
+
def end_scope(self) -> None:
|
|
87
|
+
self._resolver.end_scope()
|
|
88
|
+
|
|
89
|
+
def get(self, service_type: type[Any]) -> Any:
|
|
90
|
+
return self._resolver.resolve(service_type)
|
|
91
|
+
|
|
92
|
+
def resolve(self, service_type: type[Any]) -> Any:
|
|
93
|
+
return self._resolver.resolve(service_type)
|
|
94
|
+
|
|
95
|
+
def contains(self, service_type: type[Any]) -> bool:
|
|
96
|
+
return service_type in self._services
|
|
97
|
+
|
|
98
|
+
def clear(self) -> None:
|
|
99
|
+
self._services.clear()
|
|
100
|
+
self._bindings.clear()
|
|
101
|
+
|
|
102
|
+
def __len__(self) -> int:
|
|
103
|
+
return len(self._services)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .factory import ServiceFactory
|
|
7
|
+
from .scope import ServiceScope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ServiceDescriptor:
|
|
12
|
+
service_type: type[Any]
|
|
13
|
+
implementation: type[Any]
|
|
14
|
+
scope: ServiceScope = ServiceScope.SINGLETON
|
|
15
|
+
instance: Any | None = None
|
|
16
|
+
factory: ServiceFactory | None = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServiceFactory:
|
|
8
|
+
"""
|
|
9
|
+
Wraps a callable used to create a service instance.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
factory: Callable[[Any], Any],
|
|
15
|
+
) -> None:
|
|
16
|
+
self._factory = factory
|
|
17
|
+
|
|
18
|
+
def create(
|
|
19
|
+
self,
|
|
20
|
+
container: Any,
|
|
21
|
+
) -> Any:
|
|
22
|
+
return self._factory(container)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import types
|
|
7
|
+
import typing
|
|
8
|
+
from typing import Any, get_type_hints
|
|
9
|
+
|
|
10
|
+
from .exceptions import CircularDependencyError, ServiceNotFoundError
|
|
11
|
+
from .scope import ServiceScope
|
|
12
|
+
from .scope_context import ScopeContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_optional(hint: Any) -> tuple[bool, Any]:
|
|
16
|
+
origin = typing.get_origin(hint)
|
|
17
|
+
|
|
18
|
+
if origin is typing.Union:
|
|
19
|
+
args = typing.get_args(hint)
|
|
20
|
+
non_none = [a for a in args if a is not type(None)]
|
|
21
|
+
if len(args) - len(non_none) > 0 and len(non_none) == 1:
|
|
22
|
+
return True, non_none[0]
|
|
23
|
+
|
|
24
|
+
if origin is types.UnionType:
|
|
25
|
+
args = typing.get_args(hint)
|
|
26
|
+
non_none = [a for a in args if a is not type(None)]
|
|
27
|
+
if len(args) - len(non_none) > 0 and len(non_none) == 1:
|
|
28
|
+
return True, non_none[0]
|
|
29
|
+
|
|
30
|
+
return False, hint
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ServiceResolver:
|
|
34
|
+
|
|
35
|
+
def __init__(self, container: Any) -> None:
|
|
36
|
+
self._container = container
|
|
37
|
+
self._local = threading.local()
|
|
38
|
+
|
|
39
|
+
def _get_resolving(self) -> set[type[Any]]:
|
|
40
|
+
if not hasattr(self._local, "resolving"):
|
|
41
|
+
self._local.resolving = set()
|
|
42
|
+
return self._local.resolving
|
|
43
|
+
|
|
44
|
+
def _get_scope_context(self) -> ScopeContext | None:
|
|
45
|
+
return getattr(self._local, "scope_context", None)
|
|
46
|
+
|
|
47
|
+
def _set_scope_context(self, ctx: ScopeContext | None) -> None:
|
|
48
|
+
self._local.scope_context = ctx
|
|
49
|
+
|
|
50
|
+
def begin_scope(self) -> ScopeContext:
|
|
51
|
+
ctx = ScopeContext(on_exit=self.end_scope)
|
|
52
|
+
self._set_scope_context(ctx)
|
|
53
|
+
return ctx
|
|
54
|
+
|
|
55
|
+
def end_scope(self) -> None:
|
|
56
|
+
ctx = self._get_scope_context()
|
|
57
|
+
if ctx is not None:
|
|
58
|
+
ctx.clear()
|
|
59
|
+
self._set_scope_context(None)
|
|
60
|
+
|
|
61
|
+
def resolve(self, service_type: type[Any]) -> Any:
|
|
62
|
+
|
|
63
|
+
if service_type in self._container._bindings:
|
|
64
|
+
service_type = self._container._bindings[service_type]
|
|
65
|
+
|
|
66
|
+
resolving = self._get_resolving()
|
|
67
|
+
|
|
68
|
+
if service_type in resolving:
|
|
69
|
+
chain = " -> ".join(t.__name__ for t in resolving)
|
|
70
|
+
raise CircularDependencyError(
|
|
71
|
+
f"Circular dependency detected: {chain} -> {service_type.__name__}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
descriptor = self._container._services.get(service_type)
|
|
75
|
+
|
|
76
|
+
if descriptor is None:
|
|
77
|
+
raise ServiceNotFoundError(
|
|
78
|
+
f"Service '{service_type.__name__}' is not registered. "
|
|
79
|
+
f"Register it first using container.singleton(), "
|
|
80
|
+
f"container.transient(), or container.register()."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
descriptor.scope is ServiceScope.SINGLETON
|
|
85
|
+
and descriptor.instance is not None
|
|
86
|
+
):
|
|
87
|
+
return descriptor.instance
|
|
88
|
+
|
|
89
|
+
scope_context = self._get_scope_context()
|
|
90
|
+
|
|
91
|
+
if descriptor.scope is ServiceScope.SCOPED:
|
|
92
|
+
if scope_context is None:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
f"Service '{service_type.__name__}' is SCOPED "
|
|
95
|
+
f"but no active scope context. "
|
|
96
|
+
f"Use container.begin_scope() first."
|
|
97
|
+
)
|
|
98
|
+
if scope_context.has(service_type):
|
|
99
|
+
return scope_context.get(service_type)
|
|
100
|
+
|
|
101
|
+
if descriptor.factory is not None:
|
|
102
|
+
instance = descriptor.factory.create(self._container)
|
|
103
|
+
if descriptor.scope is ServiceScope.SINGLETON:
|
|
104
|
+
descriptor.instance = instance
|
|
105
|
+
elif descriptor.scope is ServiceScope.SCOPED and scope_context:
|
|
106
|
+
scope_context.set(service_type, instance)
|
|
107
|
+
return instance
|
|
108
|
+
|
|
109
|
+
resolving.add(service_type)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
module = sys.modules.get(descriptor.implementation.__module__, None)
|
|
113
|
+
globalns = getattr(module, "__dict__", {})
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
hints = get_type_hints(
|
|
117
|
+
descriptor.implementation.__init__,
|
|
118
|
+
globalns=globalns,
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
hints = {}
|
|
122
|
+
|
|
123
|
+
sig = inspect.signature(descriptor.implementation.__init__)
|
|
124
|
+
kwargs: dict[str, Any] = {}
|
|
125
|
+
|
|
126
|
+
for name, parameter in sig.parameters.items():
|
|
127
|
+
if name == "self":
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if parameter.kind in (
|
|
131
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
132
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
133
|
+
):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
dep_type = hints.get(name)
|
|
137
|
+
|
|
138
|
+
if dep_type is None:
|
|
139
|
+
raise TypeError(
|
|
140
|
+
f"Parameter '{name}' pada "
|
|
141
|
+
f"'{descriptor.implementation.__name__}' "
|
|
142
|
+
f"tidak memiliki type annotation."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
is_optional, inner_type = _is_optional(dep_type)
|
|
146
|
+
|
|
147
|
+
if is_optional:
|
|
148
|
+
if self._container.contains(inner_type):
|
|
149
|
+
kwargs[name] = self.resolve(inner_type)
|
|
150
|
+
else:
|
|
151
|
+
kwargs[name] = None
|
|
152
|
+
else:
|
|
153
|
+
kwargs[name] = self.resolve(dep_type)
|
|
154
|
+
|
|
155
|
+
instance = descriptor.implementation(**kwargs)
|
|
156
|
+
|
|
157
|
+
finally:
|
|
158
|
+
resolving.discard(service_type)
|
|
159
|
+
|
|
160
|
+
if descriptor.scope is ServiceScope.SINGLETON:
|
|
161
|
+
descriptor.instance = instance
|
|
162
|
+
elif descriptor.scope is ServiceScope.SCOPED and scope_context:
|
|
163
|
+
scope_context.set(service_type, instance)
|
|
164
|
+
|
|
165
|
+
return instance
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ScopeContext:
|
|
8
|
+
|
|
9
|
+
def __init__(self, on_exit: Callable[[], None] | None = None) -> None:
|
|
10
|
+
self._instances: dict[type[Any], Any] = {}
|
|
11
|
+
self._on_exit = on_exit
|
|
12
|
+
|
|
13
|
+
def has(self, service_type: type[Any]) -> bool:
|
|
14
|
+
return service_type in self._instances
|
|
15
|
+
|
|
16
|
+
def get(self, service_type: type[Any]) -> Any:
|
|
17
|
+
return self._instances[service_type]
|
|
18
|
+
|
|
19
|
+
def set(self, service_type: type[Any], instance: Any) -> None:
|
|
20
|
+
self._instances[service_type] = instance
|
|
21
|
+
|
|
22
|
+
def clear(self) -> None:
|
|
23
|
+
self._instances.clear()
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> ScopeContext:
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, *args: Any) -> None:
|
|
29
|
+
self.clear()
|
|
30
|
+
if self._on_exit is not None:
|
|
31
|
+
self._on_exit()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_string(value: object) -> bool:
|
|
5
|
+
return isinstance(value, str)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_integer(value: object) -> bool:
|
|
9
|
+
return isinstance(value, int)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_float(value: object) -> bool:
|
|
13
|
+
return isinstance(value, float)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_boolean(value: object) -> bool:
|
|
17
|
+
return isinstance(value, bool)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_non_empty_string(value: object) -> bool:
|
|
21
|
+
return isinstance(value, str) and bool(value.strip())
|
forgecore/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|