lucid-framework 0.1.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.
- lucid/__init__.py +35 -0
- lucid/application.py +82 -0
- lucid/events/__init__.py +4 -0
- lucid/events/app_booted.py +6 -0
- lucid/events/app_booting.py +6 -0
- lucid/service_provider.py +19 -0
- lucid_framework-0.1.0.dist-info/METADATA +926 -0
- lucid_framework-0.1.0.dist-info/RECORD +9 -0
- lucid_framework-0.1.0.dist-info/WHEEL +4 -0
lucid/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from lucid.application import Application
|
|
2
|
+
from lucid.service_provider import ServiceProvider
|
|
3
|
+
from lucid.events.app_booting import AppBooting
|
|
4
|
+
from lucid.events.app_booted import AppBooted
|
|
5
|
+
|
|
6
|
+
# Re-export from sub-packages for convenience
|
|
7
|
+
from lucid_container import Container
|
|
8
|
+
from lucid_config import Config, ConfigContract
|
|
9
|
+
from lucid_events import Event, Dispatcher, DispatcherContract, Listener, AsyncListener, Subscriber
|
|
10
|
+
from lucid_pipeline import Pipeline, AsyncPipeline, Pipe, AsyncPipe
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# Framework
|
|
14
|
+
"Application",
|
|
15
|
+
"ServiceProvider",
|
|
16
|
+
"AppBooting",
|
|
17
|
+
"AppBooted",
|
|
18
|
+
# Container
|
|
19
|
+
"Container",
|
|
20
|
+
# Config
|
|
21
|
+
"Config",
|
|
22
|
+
"ConfigContract",
|
|
23
|
+
# Events
|
|
24
|
+
"Event",
|
|
25
|
+
"Dispatcher",
|
|
26
|
+
"DispatcherContract",
|
|
27
|
+
"Listener",
|
|
28
|
+
"AsyncListener",
|
|
29
|
+
"Subscriber",
|
|
30
|
+
# Pipeline
|
|
31
|
+
"Pipeline",
|
|
32
|
+
"AsyncPipeline",
|
|
33
|
+
"Pipe",
|
|
34
|
+
"AsyncPipe",
|
|
35
|
+
]
|
lucid/application.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lucid_container import Container
|
|
4
|
+
from lucid_config import Config, ConfigContract
|
|
5
|
+
from lucid_events import Dispatcher, DispatcherContract
|
|
6
|
+
|
|
7
|
+
from lucid.service_provider import ServiceProvider
|
|
8
|
+
from lucid.events.app_booting import AppBooting
|
|
9
|
+
from lucid.events.app_booted import AppBooted
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Application:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self._container = Container()
|
|
15
|
+
self._config = Config()
|
|
16
|
+
self._dispatcher = Dispatcher(container=self._container)
|
|
17
|
+
self._providers: list[ServiceProvider] = []
|
|
18
|
+
self._booted = False
|
|
19
|
+
|
|
20
|
+
# Bind core services
|
|
21
|
+
self._container.instance("app", self)
|
|
22
|
+
self._container.instance(ConfigContract, self._config)
|
|
23
|
+
self._container.alias("config", ConfigContract)
|
|
24
|
+
self._container.instance(DispatcherContract, self._dispatcher)
|
|
25
|
+
self._container.alias("events", DispatcherContract)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def container(self) -> Container:
|
|
29
|
+
return self._container
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def config(self) -> Config:
|
|
33
|
+
return self._config
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def events(self) -> Dispatcher:
|
|
37
|
+
return self._dispatcher
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_booted(self) -> bool:
|
|
41
|
+
return self._booted
|
|
42
|
+
|
|
43
|
+
def configure(self, defaults: dict, env_path: str = ".env") -> None:
|
|
44
|
+
"""Load config from defaults, .env, .env.local, and environment variables."""
|
|
45
|
+
self._config.load_dict(defaults)
|
|
46
|
+
self._load_env_silent(env_path)
|
|
47
|
+
self._load_env_silent(f"{env_path}.local")
|
|
48
|
+
self._config.load_env_vars()
|
|
49
|
+
|
|
50
|
+
def _load_env_silent(self, path: str) -> None:
|
|
51
|
+
"""Load a .env file, silently skipping if it doesn't exist."""
|
|
52
|
+
try:
|
|
53
|
+
self._config.load_env(path)
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def register(self, provider_class: type) -> None:
|
|
58
|
+
"""Instantiate a service provider and call its register() immediately."""
|
|
59
|
+
if self._booted:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
f"Cannot register {provider_class.__name__} after the application has booted."
|
|
62
|
+
)
|
|
63
|
+
provider = provider_class(self)
|
|
64
|
+
self._providers.append(provider)
|
|
65
|
+
provider.register()
|
|
66
|
+
|
|
67
|
+
def make(self, abstract: object) -> object:
|
|
68
|
+
"""Resolve any binding from the container."""
|
|
69
|
+
return self._container.make(abstract)
|
|
70
|
+
|
|
71
|
+
def boot(self) -> None:
|
|
72
|
+
"""Boot the application: run providers, fire lifecycle events."""
|
|
73
|
+
if self._booted:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self._dispatcher.dispatch(AppBooting(self))
|
|
77
|
+
|
|
78
|
+
for provider in self._providers:
|
|
79
|
+
provider.boot()
|
|
80
|
+
|
|
81
|
+
self._booted = True
|
|
82
|
+
self._dispatcher.dispatch(AppBooted(self))
|
lucid/events/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from lucid.application import Application
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ServiceProvider:
|
|
10
|
+
def __init__(self, app: "Application") -> None:
|
|
11
|
+
self.app = app
|
|
12
|
+
|
|
13
|
+
def register(self) -> None:
|
|
14
|
+
"""Bind things into the container. Called immediately on app.register()."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def boot(self) -> None:
|
|
18
|
+
"""Called after all providers have registered. Safe to resolve dependencies."""
|
|
19
|
+
pass
|
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lucid-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python framework that tells you what to do next.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sharik709/lucid-framework
|
|
6
|
+
Project-URL: Documentation, https://github.com/sharik709/lucid-framework#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/sharik709/lucid-framework
|
|
8
|
+
Project-URL: Issues, https://github.com/sharik709/lucid-framework/issues
|
|
9
|
+
Author-email: Sharik Shaikh <shaikhsharik709@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: config,convention,dependency-injection,events,framework,ioc,pipeline,service-provider
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: lucid-config>=0.1.0
|
|
25
|
+
Requires-Dist: lucid-container>=0.1.0
|
|
26
|
+
Requires-Dist: lucid-events>=0.1.0
|
|
27
|
+
Requires-Dist: lucid-pipeline>=0.1.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Lucid
|
|
36
|
+
|
|
37
|
+
**A Python framework that tells you what to do next.**
|
|
38
|
+
|
|
39
|
+
Django gives you an ORM and leaves you to figure out the rest. Flask gives you a route decorator and wishes you luck. FastAPI gives you type hints and a prayer. Every Python project ends up as a custom framework anyway — you just spend the first two weeks building it instead of building your product.
|
|
40
|
+
|
|
41
|
+
Lucid is the missing opinion layer. One install, one boot sequence, and you get dependency injection, configuration, event-driven architecture, and clean pipelines — all wired together and ready to go. You always know the next step because there's a preferred way to do everything.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install lucid-framework
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
[](https://pypi.org/project/lucid-framework/)
|
|
48
|
+
[](https://www.python.org/downloads/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 30 Seconds to Running
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from lucid import Application
|
|
57
|
+
|
|
58
|
+
app = Application()
|
|
59
|
+
|
|
60
|
+
# Load config from .env and defaults
|
|
61
|
+
app.configure({
|
|
62
|
+
"app": {"name": "my-project", "debug": True},
|
|
63
|
+
"cache": {"driver": "memory"},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
# Register your services
|
|
67
|
+
app.container.singleton(UserRepository, PostgresUserRepository)
|
|
68
|
+
app.container.singleton(PaymentGateway, StripeGateway)
|
|
69
|
+
|
|
70
|
+
# Register event listeners
|
|
71
|
+
app.events.listen(OrderCompleted, SendConfirmationEmail)
|
|
72
|
+
app.events.listen(OrderCompleted, UpdateInventory)
|
|
73
|
+
|
|
74
|
+
# Boot — everything wires itself
|
|
75
|
+
app.boot()
|
|
76
|
+
|
|
77
|
+
# Build any service — the entire dependency tree resolves automatically
|
|
78
|
+
service = app.make(OrderService)
|
|
79
|
+
service.process(order)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No setup scripts. No configuration classes that inherit from other configuration classes. No `settings.py` with 200 lines of `os.environ.get()`. You describe what you want, Lucid builds it.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## What's Inside
|
|
87
|
+
|
|
88
|
+
One install gives you the full core:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
lucid-framework
|
|
92
|
+
├── lucid-container → Dependency injection with autowiring
|
|
93
|
+
├── lucid-config → Cascading config with .env support and type casting
|
|
94
|
+
├── lucid-events → Event dispatcher with typed events and prioritized listeners
|
|
95
|
+
└── lucid-pipeline → Multi-step data processing chains
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each package works standalone. The framework ties them into a single coherent application lifecycle.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## The Application
|
|
103
|
+
|
|
104
|
+
The `Application` class is the entry point. It creates the container, loads config, registers the event dispatcher, and boots your service providers — in the right order, every time.
|
|
105
|
+
|
|
106
|
+
### Creating an application
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from lucid import Application
|
|
110
|
+
|
|
111
|
+
app = Application()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This sets up:
|
|
115
|
+
|
|
116
|
+
- A `Container` instance with the application itself bound as `"app"`
|
|
117
|
+
- A `Config` instance bound as `ConfigContract` and aliased to `"config"`
|
|
118
|
+
- A `Dispatcher` instance bound as `DispatcherContract` and aliased to `"events"`
|
|
119
|
+
|
|
120
|
+
### `app.configure(defaults, env_path=".env")`
|
|
121
|
+
|
|
122
|
+
Loads configuration from defaults, `.env` files, and environment variables — in the right priority order.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
app.configure(
|
|
126
|
+
defaults={
|
|
127
|
+
"app": {
|
|
128
|
+
"name": "my-project",
|
|
129
|
+
"debug": False,
|
|
130
|
+
"env": "production",
|
|
131
|
+
"secret_key": None,
|
|
132
|
+
},
|
|
133
|
+
"database": {
|
|
134
|
+
"host": "localhost",
|
|
135
|
+
"port": 5432,
|
|
136
|
+
"name": "mydb",
|
|
137
|
+
},
|
|
138
|
+
"cache": {"driver": "memory"},
|
|
139
|
+
"mail": {"driver": "log"},
|
|
140
|
+
},
|
|
141
|
+
env_path=".env",
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This does, in order:
|
|
146
|
+
|
|
147
|
+
1. Loads `defaults` as the base config.
|
|
148
|
+
2. Loads `.env` if it exists.
|
|
149
|
+
3. Loads `.env.local` if it exists (personal overrides, gitignored).
|
|
150
|
+
4. Loads real environment variables (highest file-based priority).
|
|
151
|
+
|
|
152
|
+
After this, `app.config` is ready.
|
|
153
|
+
|
|
154
|
+
### `app.config`
|
|
155
|
+
|
|
156
|
+
Shorthand for the config instance. Dot-notation access, type casting, the works.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
app.config.get("app.name") # "my-project"
|
|
160
|
+
app.config.boolean("app.debug") # False
|
|
161
|
+
app.config.integer("database.port") # 5432
|
|
162
|
+
app.config.get("app.env") # "production" (or APP_ENV from environment)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `app.container`
|
|
166
|
+
|
|
167
|
+
Direct access to the container for binding services.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
app.container.singleton(CacheContract, RedisCache)
|
|
171
|
+
app.container.bind(ReportGenerator, PDFReportGenerator)
|
|
172
|
+
app.container.instance("stripe_key", "sk_live_...")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `app.events`
|
|
176
|
+
|
|
177
|
+
Direct access to the event dispatcher.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
app.events.listen(UserRegistered, SendWelcomeEmail)
|
|
181
|
+
app.events.listen(UserRegistered, CreateDefaultSettings, priority=10)
|
|
182
|
+
|
|
183
|
+
@app.events.listen(OrderCompleted)
|
|
184
|
+
def log_order(event):
|
|
185
|
+
print(f"Order {event.order_id} completed")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `app.make(abstract)`
|
|
189
|
+
|
|
190
|
+
Shorthand for `app.container.make()`. Resolves any class or binding from the container.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
service = app.make(OrderService)
|
|
194
|
+
config = app.make("config")
|
|
195
|
+
events = app.make("events")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `app.boot()`
|
|
199
|
+
|
|
200
|
+
Finalizes the application. Calls `boot()` on all registered service providers, freezes config (optional), and dispatches `AppBooted` event.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
app.boot()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
After boot:
|
|
207
|
+
|
|
208
|
+
- All service providers have registered and booted.
|
|
209
|
+
- The container is fully wired.
|
|
210
|
+
- `AppBooted` event has fired.
|
|
211
|
+
- The application is ready to handle requests/tasks.
|
|
212
|
+
|
|
213
|
+
### `app.is_booted`
|
|
214
|
+
|
|
215
|
+
Property that returns `True` after `boot()` has been called.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Service Providers
|
|
220
|
+
|
|
221
|
+
Service providers are how you organize your application's bindings and boot logic. Each provider is responsible for one subsystem.
|
|
222
|
+
|
|
223
|
+
### Writing a provider
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from lucid import ServiceProvider
|
|
227
|
+
|
|
228
|
+
class DatabaseServiceProvider(ServiceProvider):
|
|
229
|
+
|
|
230
|
+
def register(self):
|
|
231
|
+
"""Bind things into the container. No resolving here."""
|
|
232
|
+
self.app.container.singleton(DatabaseContract, lambda c: PostgresDatabase(
|
|
233
|
+
host=c.make("config").get("database.host"),
|
|
234
|
+
port=c.make("config").integer("database.port"),
|
|
235
|
+
name=c.make("config").get("database.name"),
|
|
236
|
+
))
|
|
237
|
+
|
|
238
|
+
def boot(self):
|
|
239
|
+
"""All providers have registered. Safe to resolve and interact."""
|
|
240
|
+
db = self.app.make(DatabaseContract)
|
|
241
|
+
db.connect()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Registering providers
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
app = Application()
|
|
248
|
+
app.configure(defaults)
|
|
249
|
+
|
|
250
|
+
# Register providers — register() called immediately on each
|
|
251
|
+
app.register(DatabaseServiceProvider)
|
|
252
|
+
app.register(CacheServiceProvider)
|
|
253
|
+
app.register(MailServiceProvider)
|
|
254
|
+
|
|
255
|
+
# Boot — boot() called on all providers in order
|
|
256
|
+
app.boot()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Provider lifecycle
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
app.register(P) → P(app) instantiated → P.register() called
|
|
263
|
+
app.register(Q) → Q(app) instantiated → Q.register() called
|
|
264
|
+
app.register(R) → R(app) instantiated → R.register() called
|
|
265
|
+
app.boot() → P.boot() → Q.boot() → R.boot() → AppBooted dispatched
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`register()` runs immediately when you call `app.register()`. All bindings are available by the time `boot()` runs. This means providers can depend on each other's bindings during `boot()`.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Application Events
|
|
273
|
+
|
|
274
|
+
The framework fires lifecycle events you can hook into.
|
|
275
|
+
|
|
276
|
+
| Event | When | Payload |
|
|
277
|
+
|-------------------|------------------------------------|------------------|
|
|
278
|
+
| `AppBooting` | Just before `boot()` runs providers| `app` |
|
|
279
|
+
| `AppBooted` | After all providers have booted | `app` |
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from lucid.events import AppBooted
|
|
283
|
+
|
|
284
|
+
@app.events.listen(AppBooted)
|
|
285
|
+
def on_ready(event):
|
|
286
|
+
print(f"{event.app.config.get('app.name')} is ready!")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Using Pipelines
|
|
292
|
+
|
|
293
|
+
Pipelines are available standalone — no special integration needed. But they shine when combined with the container.
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from lucid import Pipeline, Pipe
|
|
297
|
+
|
|
298
|
+
class ValidateRequest(Pipe):
|
|
299
|
+
def __init__(self, config: ConfigContract):
|
|
300
|
+
self.config = config
|
|
301
|
+
|
|
302
|
+
def handle(self, data, next_pipe):
|
|
303
|
+
if not data.get("api_key"):
|
|
304
|
+
return {"error": "Missing API key"}
|
|
305
|
+
if data["api_key"] != self.config.get("app.api_key"):
|
|
306
|
+
return {"error": "Invalid API key"}
|
|
307
|
+
return next_pipe(data)
|
|
308
|
+
|
|
309
|
+
class NormalizeData(Pipe):
|
|
310
|
+
def handle(self, data, next_pipe):
|
|
311
|
+
data["email"] = data.get("email", "").lower().strip()
|
|
312
|
+
return next_pipe(data)
|
|
313
|
+
|
|
314
|
+
# Resolve pipe instances through the container — dependencies autowired
|
|
315
|
+
result = (
|
|
316
|
+
Pipeline(request_data)
|
|
317
|
+
.through([
|
|
318
|
+
app.make(ValidateRequest),
|
|
319
|
+
NormalizeData(),
|
|
320
|
+
lambda data: {**data, "processed": True},
|
|
321
|
+
])
|
|
322
|
+
.then(save_to_database)
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Real-World Examples
|
|
329
|
+
|
|
330
|
+
### API service
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from lucid import Application, ServiceProvider
|
|
334
|
+
|
|
335
|
+
# ── Config ──
|
|
336
|
+
defaults = {
|
|
337
|
+
"app": {"name": "order-api", "debug": False, "secret_key": None},
|
|
338
|
+
"database": {"host": "localhost", "port": 5432, "name": "orders"},
|
|
339
|
+
"cache": {"driver": "memory"},
|
|
340
|
+
"mail": {"driver": "log"},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# ── Providers ──
|
|
344
|
+
class RepositoryProvider(ServiceProvider):
|
|
345
|
+
def register(self):
|
|
346
|
+
self.app.container.singleton(UserRepository, PostgresUserRepository)
|
|
347
|
+
self.app.container.singleton(OrderRepository, PostgresOrderRepository)
|
|
348
|
+
|
|
349
|
+
class PaymentProvider(ServiceProvider):
|
|
350
|
+
def register(self):
|
|
351
|
+
self.app.container.singleton(PaymentGateway, lambda c: StripeGateway(
|
|
352
|
+
api_key=c.make("config").get("stripe.secret_key"),
|
|
353
|
+
))
|
|
354
|
+
|
|
355
|
+
class EventListenerProvider(ServiceProvider):
|
|
356
|
+
def boot(self):
|
|
357
|
+
events = self.app.events
|
|
358
|
+
events.listen(OrderCompleted, SendConfirmationEmail)
|
|
359
|
+
events.listen(OrderCompleted, UpdateInventory, priority=20)
|
|
360
|
+
events.listen(PaymentFailed, NotifySupport)
|
|
361
|
+
events.listen(UserRegistered, SendWelcomeEmail)
|
|
362
|
+
events.listen(UserRegistered, CreateDefaultSettings, priority=10)
|
|
363
|
+
|
|
364
|
+
# ── Bootstrap ──
|
|
365
|
+
app = Application()
|
|
366
|
+
app.configure(defaults)
|
|
367
|
+
|
|
368
|
+
app.register(RepositoryProvider)
|
|
369
|
+
app.register(PaymentProvider)
|
|
370
|
+
app.register(EventListenerProvider)
|
|
371
|
+
|
|
372
|
+
app.boot()
|
|
373
|
+
|
|
374
|
+
# ── Use ──
|
|
375
|
+
order_service = app.make(OrderService)
|
|
376
|
+
order_service.process(incoming_order)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### CLI tool
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from lucid import Application
|
|
383
|
+
|
|
384
|
+
app = Application()
|
|
385
|
+
app.configure({
|
|
386
|
+
"app": {"name": "data-migrator"},
|
|
387
|
+
"source_db": {"host": "old-db.internal", "port": 5432},
|
|
388
|
+
"target_db": {"host": "new-db.internal", "port": 5432},
|
|
389
|
+
})
|
|
390
|
+
app.boot()
|
|
391
|
+
|
|
392
|
+
migrator = app.make(DataMigrator)
|
|
393
|
+
migrator.run()
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Testing
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
def create_test_app(**config_overrides):
|
|
400
|
+
"""Create a fresh application with test doubles."""
|
|
401
|
+
app = Application()
|
|
402
|
+
|
|
403
|
+
defaults = {
|
|
404
|
+
"app": {"name": "test", "debug": True, "secret_key": "test-secret"},
|
|
405
|
+
"database": {"host": "localhost", "name": "test_db"},
|
|
406
|
+
"mail": {"driver": "log"},
|
|
407
|
+
}
|
|
408
|
+
defaults.update(config_overrides)
|
|
409
|
+
app.configure(defaults)
|
|
410
|
+
|
|
411
|
+
# Swap real implementations for test doubles
|
|
412
|
+
app.container.instance(MailerContract, FakeMailer())
|
|
413
|
+
app.container.instance(PaymentGateway, FakePaymentGateway(always_succeeds=True))
|
|
414
|
+
app.container.instance(CacheContract, InMemoryCache())
|
|
415
|
+
|
|
416
|
+
app.boot()
|
|
417
|
+
return app
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_order_completion():
|
|
421
|
+
app = create_test_app()
|
|
422
|
+
service = app.make(OrderService)
|
|
423
|
+
|
|
424
|
+
result = service.process(test_order)
|
|
425
|
+
|
|
426
|
+
assert result.status == "completed"
|
|
427
|
+
assert app.make(MailerContract).last_sent.subject == "Order Confirmed"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_order_with_failed_payment():
|
|
431
|
+
app = create_test_app()
|
|
432
|
+
app.container.instance(PaymentGateway, FakePaymentGateway(always_fails=True))
|
|
433
|
+
|
|
434
|
+
service = app.make(OrderService)
|
|
435
|
+
result = service.process(test_order)
|
|
436
|
+
|
|
437
|
+
assert result.status == "payment_failed"
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Every test gets a clean application with isolated state. No global singletons, no monkeypatching, no test ordering issues.
|
|
441
|
+
|
|
442
|
+
### Integration with web frameworks
|
|
443
|
+
|
|
444
|
+
Lucid doesn't replace your web framework — it runs alongside it.
|
|
445
|
+
|
|
446
|
+
**With FastAPI:**
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
from fastapi import FastAPI, Depends
|
|
450
|
+
from lucid import Application
|
|
451
|
+
|
|
452
|
+
# Bootstrap Lucid
|
|
453
|
+
lucid = Application()
|
|
454
|
+
lucid.configure(defaults)
|
|
455
|
+
lucid.register(DatabaseProvider)
|
|
456
|
+
lucid.register(CacheProvider)
|
|
457
|
+
lucid.boot()
|
|
458
|
+
|
|
459
|
+
# FastAPI app
|
|
460
|
+
api = FastAPI()
|
|
461
|
+
|
|
462
|
+
def get_lucid():
|
|
463
|
+
return lucid
|
|
464
|
+
|
|
465
|
+
@api.post("/orders")
|
|
466
|
+
def create_order(data: OrderRequest, app: Application = Depends(get_lucid)):
|
|
467
|
+
service = app.make(OrderService)
|
|
468
|
+
return service.process(data)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**With Flask:**
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
from flask import Flask
|
|
475
|
+
|
|
476
|
+
flask_app = Flask(__name__)
|
|
477
|
+
|
|
478
|
+
lucid = Application()
|
|
479
|
+
lucid.configure(defaults)
|
|
480
|
+
lucid.boot()
|
|
481
|
+
|
|
482
|
+
@flask_app.route("/orders", methods=["POST"])
|
|
483
|
+
def create_order():
|
|
484
|
+
service = lucid.make(OrderService)
|
|
485
|
+
return service.process(request.json)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Lucid manages your services, config, and events. The web framework manages HTTP. They compose cleanly without either one taking over.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Directory Conventions
|
|
493
|
+
|
|
494
|
+
Lucid recommends (but doesn't enforce) this project structure:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
my-project/
|
|
498
|
+
├── app/
|
|
499
|
+
│ ├── __init__.py
|
|
500
|
+
│ ├── services/ # Business logic
|
|
501
|
+
│ │ ├── order_service.py
|
|
502
|
+
│ │ ├── user_service.py
|
|
503
|
+
│ │ └── payment_service.py
|
|
504
|
+
│ ├── repositories/ # Data access
|
|
505
|
+
│ │ ├── user_repository.py
|
|
506
|
+
│ │ └── order_repository.py
|
|
507
|
+
│ ├── events/ # Event classes
|
|
508
|
+
│ │ ├── order_events.py
|
|
509
|
+
│ │ └── user_events.py
|
|
510
|
+
│ ├── listeners/ # Event listeners
|
|
511
|
+
│ │ ├── send_welcome_email.py
|
|
512
|
+
│ │ └── update_inventory.py
|
|
513
|
+
│ ├── contracts/ # ABCs / interfaces
|
|
514
|
+
│ │ ├── cache_contract.py
|
|
515
|
+
│ │ ├── mailer_contract.py
|
|
516
|
+
│ │ └── payment_contract.py
|
|
517
|
+
│ └── providers/ # Service providers
|
|
518
|
+
│ ├── database_provider.py
|
|
519
|
+
│ ├── cache_provider.py
|
|
520
|
+
│ └── mail_provider.py
|
|
521
|
+
├── config/
|
|
522
|
+
│ └── defaults.py # Default configuration dict
|
|
523
|
+
├── .env # Environment defaults
|
|
524
|
+
├── .env.local # Personal overrides (gitignored)
|
|
525
|
+
├── bootstrap.py # Application setup
|
|
526
|
+
├── main.py # Entry point
|
|
527
|
+
└── tests/
|
|
528
|
+
├── conftest.py # Test app factory
|
|
529
|
+
└── ...
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**bootstrap.py:**
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
from lucid import Application
|
|
536
|
+
from config.defaults import defaults
|
|
537
|
+
from app.providers.database_provider import DatabaseProvider
|
|
538
|
+
from app.providers.cache_provider import CacheProvider
|
|
539
|
+
from app.providers.mail_provider import MailProvider
|
|
540
|
+
|
|
541
|
+
def create_app() -> Application:
|
|
542
|
+
app = Application()
|
|
543
|
+
app.configure(defaults)
|
|
544
|
+
|
|
545
|
+
app.register(DatabaseProvider)
|
|
546
|
+
app.register(CacheProvider)
|
|
547
|
+
app.register(MailProvider)
|
|
548
|
+
|
|
549
|
+
app.boot()
|
|
550
|
+
return app
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**main.py:**
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
from bootstrap import create_app
|
|
557
|
+
|
|
558
|
+
app = create_app()
|
|
559
|
+
|
|
560
|
+
# Your application logic starts here
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
This is a convention, not a requirement. Lucid works with any project structure — it's your container, your config, your events. Organize them however you want.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Architecture
|
|
568
|
+
|
|
569
|
+
### Project Structure
|
|
570
|
+
|
|
571
|
+
```
|
|
572
|
+
lucid-framework/
|
|
573
|
+
├── src/
|
|
574
|
+
│ └── lucid/
|
|
575
|
+
│ ├── __init__.py # Public API — re-exports everything
|
|
576
|
+
│ ├── application.py # Application class
|
|
577
|
+
│ ├── events/
|
|
578
|
+
│ │ ├── __init__.py
|
|
579
|
+
│ │ ├── app_booting.py # AppBooting event
|
|
580
|
+
│ │ └── app_booted.py # AppBooted event
|
|
581
|
+
│ └── service_provider.py # ServiceProvider base (re-exported or extended)
|
|
582
|
+
├── tests/
|
|
583
|
+
│ ├── __init__.py
|
|
584
|
+
│ ├── test_application.py # Application lifecycle
|
|
585
|
+
│ ├── test_configure.py # Config loading through Application
|
|
586
|
+
│ ├── test_providers.py # Provider registration and boot
|
|
587
|
+
│ ├── test_events.py # Lifecycle events
|
|
588
|
+
│ ├── test_make.py # Container resolution through app
|
|
589
|
+
│ └── test_integration.py # Full stack integration tests
|
|
590
|
+
├── pyproject.toml
|
|
591
|
+
├── README.md
|
|
592
|
+
├── LICENSE
|
|
593
|
+
└── CHANGELOG.md
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Implementation Notes
|
|
597
|
+
|
|
598
|
+
**The Application class internals:**
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
from lucid_container import Container
|
|
602
|
+
from lucid_config import Config, ConfigContract
|
|
603
|
+
from lucid_events import Dispatcher, DispatcherContract
|
|
604
|
+
|
|
605
|
+
class Application:
|
|
606
|
+
def __init__(self):
|
|
607
|
+
self._container = Container()
|
|
608
|
+
self._config = Config()
|
|
609
|
+
self._dispatcher = Dispatcher(container=self._container)
|
|
610
|
+
self._providers: list[ServiceProvider] = []
|
|
611
|
+
self._booted = False
|
|
612
|
+
|
|
613
|
+
# Bind core services
|
|
614
|
+
self._container.instance("app", self)
|
|
615
|
+
self._container.instance(ConfigContract, self._config)
|
|
616
|
+
self._container.alias("config", ConfigContract)
|
|
617
|
+
self._container.instance(DispatcherContract, self._dispatcher)
|
|
618
|
+
self._container.alias("events", DispatcherContract)
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def container(self) -> Container:
|
|
622
|
+
return self._container
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def config(self) -> Config:
|
|
626
|
+
return self._config
|
|
627
|
+
|
|
628
|
+
@property
|
|
629
|
+
def events(self) -> Dispatcher:
|
|
630
|
+
return self._dispatcher
|
|
631
|
+
|
|
632
|
+
@property
|
|
633
|
+
def is_booted(self) -> bool:
|
|
634
|
+
return self._booted
|
|
635
|
+
|
|
636
|
+
def configure(self, defaults: dict, env_path: str = ".env"):
|
|
637
|
+
self._config.load_dict(defaults)
|
|
638
|
+
self._config.load_env(env_path)
|
|
639
|
+
self._config.load_env(f"{env_path}.local")
|
|
640
|
+
self._config.load_env_vars()
|
|
641
|
+
|
|
642
|
+
def register(self, provider_class: type):
|
|
643
|
+
provider = provider_class(self)
|
|
644
|
+
self._providers.append(provider)
|
|
645
|
+
provider.register()
|
|
646
|
+
|
|
647
|
+
def make(self, abstract):
|
|
648
|
+
return self._container.make(abstract)
|
|
649
|
+
|
|
650
|
+
def boot(self):
|
|
651
|
+
if self._booted:
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
self._dispatcher.dispatch(AppBooting(self))
|
|
655
|
+
|
|
656
|
+
for provider in self._providers:
|
|
657
|
+
provider.boot()
|
|
658
|
+
|
|
659
|
+
self._booted = True
|
|
660
|
+
self._dispatcher.dispatch(AppBooted(self))
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**What `configure()` does with missing files:**
|
|
664
|
+
|
|
665
|
+
If `.env` or `.env.local` doesn't exist, `load_env()` silently skips it (no `FileNotFoundError`). This means `configure()` always works — in development with a `.env` file and in production with only real environment variables.
|
|
666
|
+
|
|
667
|
+
**Re-exports in `__init__.py`:**
|
|
668
|
+
|
|
669
|
+
The framework re-exports the most common classes so users only need one import:
|
|
670
|
+
|
|
671
|
+
```python
|
|
672
|
+
# lucid/__init__.py
|
|
673
|
+
from lucid.application import Application
|
|
674
|
+
from lucid.service_provider import ServiceProvider
|
|
675
|
+
from lucid.events.app_booting import AppBooting
|
|
676
|
+
from lucid.events.app_booted import AppBooted
|
|
677
|
+
|
|
678
|
+
# Re-export from sub-packages for convenience
|
|
679
|
+
from lucid_container import Container
|
|
680
|
+
from lucid_config import Config, ConfigContract
|
|
681
|
+
from lucid_events import Event, Dispatcher, DispatcherContract, Listener, AsyncListener, Subscriber
|
|
682
|
+
from lucid_pipeline import Pipeline, AsyncPipeline, Pipe, AsyncPipe
|
|
683
|
+
|
|
684
|
+
__all__ = [
|
|
685
|
+
# Framework
|
|
686
|
+
"Application",
|
|
687
|
+
"ServiceProvider",
|
|
688
|
+
"AppBooting",
|
|
689
|
+
"AppBooted",
|
|
690
|
+
# Container
|
|
691
|
+
"Container",
|
|
692
|
+
# Config
|
|
693
|
+
"Config",
|
|
694
|
+
"ConfigContract",
|
|
695
|
+
# Events
|
|
696
|
+
"Event",
|
|
697
|
+
"Dispatcher",
|
|
698
|
+
"DispatcherContract",
|
|
699
|
+
"Listener",
|
|
700
|
+
"AsyncListener",
|
|
701
|
+
"Subscriber",
|
|
702
|
+
# Pipeline
|
|
703
|
+
"Pipeline",
|
|
704
|
+
"AsyncPipeline",
|
|
705
|
+
"Pipe",
|
|
706
|
+
"AsyncPipe",
|
|
707
|
+
]
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
This means users can write `from lucid import Application, Event, Listener, Pipeline` — one import line for everything.
|
|
711
|
+
|
|
712
|
+
**ServiceProvider base class:**
|
|
713
|
+
|
|
714
|
+
The framework either re-exports `ServiceProvider` from `lucid_container` or provides its own thin wrapper that adds the `self.app` property:
|
|
715
|
+
|
|
716
|
+
```python
|
|
717
|
+
class ServiceProvider:
|
|
718
|
+
def __init__(self, app: "Application"):
|
|
719
|
+
self.app = app
|
|
720
|
+
|
|
721
|
+
def register(self) -> None:
|
|
722
|
+
"""Bind things into the container."""
|
|
723
|
+
pass
|
|
724
|
+
|
|
725
|
+
def boot(self) -> None:
|
|
726
|
+
"""Called after all providers have registered."""
|
|
727
|
+
pass
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Public API
|
|
731
|
+
|
|
732
|
+
```python
|
|
733
|
+
from lucid import Application # The app
|
|
734
|
+
from lucid import ServiceProvider # Base for providers
|
|
735
|
+
from lucid import AppBooting, AppBooted # Lifecycle events
|
|
736
|
+
|
|
737
|
+
# Everything from sub-packages, re-exported:
|
|
738
|
+
from lucid import Container # DI container
|
|
739
|
+
from lucid import Config, ConfigContract # Configuration
|
|
740
|
+
from lucid import Event, Dispatcher, DispatcherContract, Listener, AsyncListener, Subscriber # Events
|
|
741
|
+
from lucid import Pipeline, AsyncPipeline, Pipe, AsyncPipe # Pipelines
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## pyproject.toml Specification
|
|
747
|
+
|
|
748
|
+
```toml
|
|
749
|
+
[build-system]
|
|
750
|
+
requires = ["hatchling"]
|
|
751
|
+
build-backend = "hatchling.build"
|
|
752
|
+
|
|
753
|
+
[project]
|
|
754
|
+
name = "lucid-framework"
|
|
755
|
+
version = "0.1.0"
|
|
756
|
+
description = "A Python framework that tells you what to do next."
|
|
757
|
+
readme = "README.md"
|
|
758
|
+
license = "MIT"
|
|
759
|
+
requires-python = ">=3.10"
|
|
760
|
+
authors = [
|
|
761
|
+
{ name = "Sharik Shaikh", email = "shaikhsharik709@gmail.com" },
|
|
762
|
+
]
|
|
763
|
+
keywords = [
|
|
764
|
+
"framework", "dependency-injection", "config", "events",
|
|
765
|
+
"pipeline", "service-provider", "ioc", "convention",
|
|
766
|
+
]
|
|
767
|
+
classifiers = [
|
|
768
|
+
"Development Status :: 4 - Beta",
|
|
769
|
+
"Intended Audience :: Developers",
|
|
770
|
+
"License :: OSI Approved :: MIT License",
|
|
771
|
+
"Programming Language :: Python :: 3",
|
|
772
|
+
"Programming Language :: Python :: 3.10",
|
|
773
|
+
"Programming Language :: Python :: 3.11",
|
|
774
|
+
"Programming Language :: Python :: 3.12",
|
|
775
|
+
"Programming Language :: Python :: 3.13",
|
|
776
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
777
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
778
|
+
"Typing :: Typed",
|
|
779
|
+
]
|
|
780
|
+
dependencies = [
|
|
781
|
+
"lucid-pipeline>=0.1.0",
|
|
782
|
+
"lucid-container>=0.1.0",
|
|
783
|
+
"lucid-config>=0.1.0",
|
|
784
|
+
"lucid-events>=0.1.0",
|
|
785
|
+
]
|
|
786
|
+
|
|
787
|
+
[project.urls]
|
|
788
|
+
Homepage = "https://github.com/sharik709/lucid-framework"
|
|
789
|
+
Documentation = "https://github.com/sharik709/lucid-framework#readme"
|
|
790
|
+
Repository = "https://github.com/sharik709/lucid-framework"
|
|
791
|
+
Issues = "https://github.com/sharik709/lucid-framework/issues"
|
|
792
|
+
|
|
793
|
+
[tool.pytest.ini_options]
|
|
794
|
+
testpaths = ["tests"]
|
|
795
|
+
asyncio_mode = "auto"
|
|
796
|
+
|
|
797
|
+
[tool.mypy]
|
|
798
|
+
strict = true
|
|
799
|
+
|
|
800
|
+
[project.optional-dependencies]
|
|
801
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "mypy>=1.0", "ruff>=0.1"]
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## Test Cases to Implement
|
|
807
|
+
|
|
808
|
+
### Application Lifecycle
|
|
809
|
+
|
|
810
|
+
- `Application()` creates container, config, and dispatcher
|
|
811
|
+
- `app.container` returns a Container instance
|
|
812
|
+
- `app.config` returns a Config instance
|
|
813
|
+
- `app.events` returns a Dispatcher instance
|
|
814
|
+
- `app.is_booted` is `False` before boot, `True` after
|
|
815
|
+
- `app.boot()` called twice does nothing the second time
|
|
816
|
+
|
|
817
|
+
### Configure
|
|
818
|
+
|
|
819
|
+
- `app.configure(defaults)` loads defaults into config
|
|
820
|
+
- Config values accessible via `app.config.get()`
|
|
821
|
+
- Environment variables override defaults
|
|
822
|
+
- Missing `.env` file doesn't raise
|
|
823
|
+
- Missing `.env.local` file doesn't raise
|
|
824
|
+
- Type casting works after configure (`boolean`, `integer`, etc.)
|
|
825
|
+
|
|
826
|
+
### Container Access
|
|
827
|
+
|
|
828
|
+
- `app.make(SomeClass)` resolves from container
|
|
829
|
+
- `app.make("config")` returns the config (alias works)
|
|
830
|
+
- `app.make("events")` returns the dispatcher (alias works)
|
|
831
|
+
- `app.make("app")` returns the application itself
|
|
832
|
+
- `app.container.singleton()` bindings work through `app.make()`
|
|
833
|
+
- `app.container.bind()` bindings work through `app.make()`
|
|
834
|
+
|
|
835
|
+
### Service Providers
|
|
836
|
+
|
|
837
|
+
- `app.register(P)` instantiates P with the app
|
|
838
|
+
- `app.register(P)` calls `P.register()` immediately
|
|
839
|
+
- Provider's `self.app` is the application instance
|
|
840
|
+
- Provider can bind into `self.app.container` during `register()`
|
|
841
|
+
- Provider can read from `self.app.config` during `register()`
|
|
842
|
+
- `app.boot()` calls `boot()` on all registered providers
|
|
843
|
+
- `boot()` is called in registration order
|
|
844
|
+
- Provider boot can resolve bindings from other providers
|
|
845
|
+
- Provider boot can access the event dispatcher
|
|
846
|
+
- Multiple providers register without conflicts
|
|
847
|
+
|
|
848
|
+
### Lifecycle Events
|
|
849
|
+
|
|
850
|
+
- `AppBooting` is dispatched at the start of `boot()`
|
|
851
|
+
- `AppBooted` is dispatched after all providers have booted
|
|
852
|
+
- `AppBooting` event carries the app reference
|
|
853
|
+
- `AppBooted` event carries the app reference
|
|
854
|
+
- Listeners registered before `boot()` receive the events
|
|
855
|
+
- `AppBooting` fires before any provider `boot()` is called
|
|
856
|
+
- `AppBooted` fires after all provider `boot()` calls complete
|
|
857
|
+
|
|
858
|
+
### Re-exports
|
|
859
|
+
|
|
860
|
+
- `from lucid import Application` works
|
|
861
|
+
- `from lucid import ServiceProvider` works
|
|
862
|
+
- `from lucid import Container` works
|
|
863
|
+
- `from lucid import Config, ConfigContract` works
|
|
864
|
+
- `from lucid import Event, Dispatcher, Listener, Subscriber` works
|
|
865
|
+
- `from lucid import Pipeline, Pipe` works
|
|
866
|
+
- `from lucid import AppBooting, AppBooted` works
|
|
867
|
+
|
|
868
|
+
### Integration Tests
|
|
869
|
+
|
|
870
|
+
- Full stack: configure → register providers → boot → make service → use service
|
|
871
|
+
- Provider registers binding, another provider resolves it during boot
|
|
872
|
+
- Event listener registered in a provider fires when event dispatched
|
|
873
|
+
- Config loaded in configure, read by provider during register
|
|
874
|
+
- Container autowires a service whose dependencies were bound by providers
|
|
875
|
+
- Pipeline used inside a service that was resolved from the container
|
|
876
|
+
- Test double swap: instance() overrides a singleton for testing
|
|
877
|
+
|
|
878
|
+
### Edge Cases
|
|
879
|
+
|
|
880
|
+
- Application with no providers — boot succeeds
|
|
881
|
+
- Application with no config — boot succeeds (empty config)
|
|
882
|
+
- Registering a provider after boot raises error (or is silently ignored)
|
|
883
|
+
- `make()` before `boot()` works for bindings registered during `register()`
|
|
884
|
+
- Provider `register()` raising an exception surfaces clearly
|
|
885
|
+
- Provider `boot()` raising an exception surfaces clearly
|
|
886
|
+
- Two providers binding the same abstract — last one wins
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## The Lucid Ecosystem
|
|
891
|
+
|
|
892
|
+
```
|
|
893
|
+
┌──────────────────────────────────────────────────────────┐
|
|
894
|
+
│ lucid-framework │
|
|
895
|
+
│ │
|
|
896
|
+
│ Application · ServiceProvider · Lifecycle Events │
|
|
897
|
+
│ │
|
|
898
|
+
├──────────┬──────────┬──────────────┬─────────────────────┤
|
|
899
|
+
│ lucid- │ lucid- │ lucid- │ lucid- │
|
|
900
|
+
│ container│ config │ events │ pipeline │
|
|
901
|
+
│ │ │ │ │
|
|
902
|
+
│ DI + │ .env + │ Typed events │ Multi-step │
|
|
903
|
+
│ autowire │ dot │ + listeners │ data chains │
|
|
904
|
+
│ │ notation │ + subscribers│ │
|
|
905
|
+
└──────────┴──────────┴──────────────┴─────────────────────┘
|
|
906
|
+
▼
|
|
907
|
+
Feature packages (coming soon)
|
|
908
|
+
lucid-cache · lucid-mail · lucid-queue
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Each box is an independent PyPI package. Install the framework to get everything, or install only what you need.
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
## Coming Soon
|
|
916
|
+
|
|
917
|
+
- `lucid-cache` — Multi-driver cache (memory, file, Redis) with a unified API.
|
|
918
|
+
- `lucid-mail` — Multi-driver mail (SMTP, Mailgun, SES) with templates and queued sending.
|
|
919
|
+
- `lucid-queue` — Background job processing with swappable backends.
|
|
920
|
+
- `lucid-cli` — Artisan-style CLI for code generation, migrations, and task scheduling.
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## License
|
|
925
|
+
|
|
926
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
lucid/__init__.py,sha256=Libs5MW_lW-ePqUDcd8dRptwcS6MyrNCfdubiCXF9aI,884
|
|
2
|
+
lucid/application.py,sha256=9vrJDsz898jN4BWsI-dQlYkc4WGlRzzy_JLRb9G9tfg,2704
|
|
3
|
+
lucid/service_provider.py,sha256=-k-LFFkqYNIxUo768RJzLwdtqC00sHOd8bdhpTwlMOw,494
|
|
4
|
+
lucid/events/__init__.py,sha256=hOhxy_BcSn1Ojj2k_C0Ce-473LZG3PqX2z8Bg4E1gp4,133
|
|
5
|
+
lucid/events/app_booted.py,sha256=NXpQXv09y0IPPqCCtxu_WA0QKOLzB5pBbtOAcznNs2c,162
|
|
6
|
+
lucid/events/app_booting.py,sha256=CANv_hsFw7NhkD3EYIJQoYIZJBF80YPq1gX3oYsLEOE,163
|
|
7
|
+
lucid_framework-0.1.0.dist-info/METADATA,sha256=RlzLz_SM8FKY0gejIQQ71U80FHw8XTkwP68FV-JJOIs,28935
|
|
8
|
+
lucid_framework-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
lucid_framework-0.1.0.dist-info/RECORD,,
|