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 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))
@@ -0,0 +1,4 @@
1
+ from lucid.events.app_booting import AppBooting
2
+ from lucid.events.app_booted import AppBooted
3
+
4
+ __all__ = ["AppBooting", "AppBooted"]
@@ -0,0 +1,6 @@
1
+ from lucid_events import Event
2
+
3
+
4
+ class AppBooted(Event):
5
+ def __init__(self, app: "Application") -> None: # type: ignore[name-defined]
6
+ self.app = app
@@ -0,0 +1,6 @@
1
+ from lucid_events import Event
2
+
3
+
4
+ class AppBooting(Event):
5
+ def __init__(self, app: "Application") -> None: # type: ignore[name-defined]
6
+ self.app = app
@@ -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
+ [![PyPI version](https://badge.fury.io/py/lucid-framework.svg)](https://pypi.org/project/lucid-framework/)
48
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any