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.
Files changed (49) hide show
  1. forge_core_di-0.2.0.dist-info/METADATA +58 -0
  2. forge_core_di-0.2.0.dist-info/RECORD +49 -0
  3. forge_core_di-0.2.0.dist-info/WHEEL +4 -0
  4. forge_core_di-0.2.0.dist-info/licenses/LICENSE +9 -0
  5. forgecore/__init__.py +88 -0
  6. forgecore/application/__init__.py +7 -0
  7. forgecore/application/app.py +94 -0
  8. forgecore/application/metadata.py +11 -0
  9. forgecore/config/__init__.py +5 -0
  10. forgecore/config/config.py +23 -0
  11. forgecore/context/__init__.py +5 -0
  12. forgecore/context/context.py +23 -0
  13. forgecore/core/__init__.py +31 -0
  14. forgecore/core/constants.py +7 -0
  15. forgecore/core/exceptions.py +18 -0
  16. forgecore/core/types.py +4 -0
  17. forgecore/core/version.py +3 -0
  18. forgecore/lifecycle/__init__.py +7 -0
  19. forgecore/lifecycle/manager.py +40 -0
  20. forgecore/lifecycle/state.py +11 -0
  21. forgecore/logging/__init__.py +5 -0
  22. forgecore/logging/logger.py +23 -0
  23. forgecore/plugins/__init__.py +7 -0
  24. forgecore/plugins/manager.py +31 -0
  25. forgecore/plugins/plugin.py +21 -0
  26. forgecore/providers/__init__.py +5 -0
  27. forgecore/providers/provider.py +14 -0
  28. forgecore/registry/__init__.py +7 -0
  29. forgecore/registry/exceptions.py +2 -0
  30. forgecore/registry/registry.py +31 -0
  31. forgecore/runtime/__init__.py +9 -0
  32. forgecore/runtime/bus.py +50 -0
  33. forgecore/runtime/event.py +10 -0
  34. forgecore/runtime/exceptions.py +2 -0
  35. forgecore/services/__init__.py +20 -0
  36. forgecore/services/autowire.py +27 -0
  37. forgecore/services/container.py +103 -0
  38. forgecore/services/descriptor.py +16 -0
  39. forgecore/services/exceptions.py +6 -0
  40. forgecore/services/factory.py +22 -0
  41. forgecore/services/resolver.py +165 -0
  42. forgecore/services/scope.py +7 -0
  43. forgecore/services/scope_context.py +31 -0
  44. forgecore/utils/__init__.py +9 -0
  45. forgecore/utils/inspect.py +7 -0
  46. forgecore/utils/system.py +15 -0
  47. forgecore/validation/__init__.py +15 -0
  48. forgecore/validation/validators.py +21 -0
  49. 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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,7 @@
1
+ from .app import ForgeApp
2
+ from .metadata import ApplicationMetadata
3
+
4
+ __all__ = [
5
+ "ForgeApp",
6
+ "ApplicationMetadata",
7
+ ]
@@ -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,11 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class ApplicationMetadata:
8
+ name: str
9
+ version: str
10
+ description: str = ""
11
+ author: str = ""
@@ -0,0 +1,5 @@
1
+ from .config import Config
2
+
3
+ __all__ = [
4
+ "Config",
5
+ ]
@@ -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,5 @@
1
+ from .context import ApplicationContext
2
+
3
+ __all__ = [
4
+ "ApplicationContext",
5
+ ]
@@ -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,7 @@
1
+ PACKAGE_NAME = "forgecore"
2
+
3
+ DEFAULT_ENCODING = "utf-8"
4
+
5
+ DEFAULT_TIMEOUT = 30
6
+
7
+ MAX_RETRIES = 3
@@ -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
@@ -0,0 +1,4 @@
1
+ from typing import Any, TypeAlias
2
+
3
+ JSON: TypeAlias = dict[str, Any]
4
+ JSONList: TypeAlias = list[JSON]
@@ -0,0 +1,3 @@
1
+ VERSION = (0, 2, 0)
2
+
3
+ __version__ = ".".join(map(str, VERSION))
@@ -0,0 +1,7 @@
1
+ from .manager import LifecycleManager
2
+ from .state import LifecycleState
3
+
4
+ __all__ = [
5
+ "LifecycleManager",
6
+ "LifecycleState",
7
+ ]
@@ -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,11 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class LifecycleState(str, Enum):
7
+ INITIALIZED = "initialized"
8
+ STARTING = "starting"
9
+ RUNNING = "running"
10
+ STOPPING = "stopping"
11
+ STOPPED = "stopped"
@@ -0,0 +1,5 @@
1
+ from .logger import get_logger
2
+
3
+ __all__ = [
4
+ "get_logger",
5
+ ]
@@ -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,7 @@
1
+ from .manager import PluginManager
2
+ from .plugin import Plugin
3
+
4
+ __all__ = [
5
+ "Plugin",
6
+ "PluginManager",
7
+ ]
@@ -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,5 @@
1
+ from .provider import ServiceProvider
2
+
3
+ __all__ = [
4
+ "ServiceProvider",
5
+ ]
@@ -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,7 @@
1
+ from .exceptions import RegistryKeyError
2
+ from .registry import Registry
3
+
4
+ __all__ = [
5
+ "Registry",
6
+ "RegistryKeyError",
7
+ ]
@@ -0,0 +1,2 @@
1
+ class RegistryKeyError(KeyError):
2
+ """Raised when a registry key cannot be found."""
@@ -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)
@@ -0,0 +1,9 @@
1
+ from .bus import EventBus
2
+ from .event import Event
3
+ from .exceptions import EventError
4
+
5
+ __all__ = [
6
+ "Event",
7
+ "EventBus",
8
+ "EventError",
9
+ ]
@@ -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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(slots=True, frozen=True)
8
+ class Event:
9
+ name: str
10
+ payload: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,2 @@
1
+ class EventError(Exception):
2
+ pass
@@ -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,6 @@
1
+ class ServiceNotFoundError(LookupError):
2
+ pass
3
+
4
+
5
+ class CircularDependencyError(Exception):
6
+ pass
@@ -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,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ServiceScope(str, Enum):
5
+ SINGLETON = "singleton"
6
+ TRANSIENT = "transient"
7
+ SCOPED = "scoped"
@@ -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,9 @@
1
+ from .inspect import package_version
2
+ from .system import machine, operating_system, python_version
3
+
4
+ __all__ = [
5
+ "package_version",
6
+ "python_version",
7
+ "operating_system",
8
+ "machine",
9
+ ]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import version
4
+
5
+
6
+ def package_version(package: str) -> str:
7
+ return version(package)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+
5
+
6
+ def python_version() -> str:
7
+ return platform.python_version()
8
+
9
+
10
+ def operating_system() -> str:
11
+ return platform.system()
12
+
13
+
14
+ def machine() -> str:
15
+ return platform.machine()
@@ -0,0 +1,15 @@
1
+ from .validators import (
2
+ is_boolean,
3
+ is_float,
4
+ is_integer,
5
+ is_non_empty_string,
6
+ is_string,
7
+ )
8
+
9
+ __all__ = [
10
+ "is_string",
11
+ "is_integer",
12
+ "is_float",
13
+ "is_boolean",
14
+ "is_non_empty_string",
15
+ ]
@@ -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"