A Python framework that tells you what to do next.
Project description
Lucid
A Python framework that tells you what to do next.
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.
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.
pip install lucid-framework
30 Seconds to Running
from lucid import Application
app = Application()
# Load config from .env and defaults
app.configure({
"app": {"name": "my-project", "debug": True},
"cache": {"driver": "memory"},
})
# Register your services
app.container.singleton(UserRepository, PostgresUserRepository)
app.container.singleton(PaymentGateway, StripeGateway)
# Register event listeners
app.events.listen(OrderCompleted, SendConfirmationEmail)
app.events.listen(OrderCompleted, UpdateInventory)
# Boot — everything wires itself
app.boot()
# Build any service — the entire dependency tree resolves automatically
service = app.make(OrderService)
service.process(order)
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.
What's Inside
One install gives you the full core:
lucid-framework
├── lucid-container → Dependency injection with autowiring
├── lucid-config → Cascading config with .env support and type casting
├── lucid-events → Event dispatcher with typed events and prioritized listeners
└── lucid-pipeline → Multi-step data processing chains
Each package works standalone. The framework ties them into a single coherent application lifecycle.
The Application
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.
Creating an application
from lucid import Application
app = Application()
This sets up:
- A
Containerinstance with the application itself bound as"app" - A
Configinstance bound asConfigContractand aliased to"config" - A
Dispatcherinstance bound asDispatcherContractand aliased to"events"
app.configure(defaults, env_path=".env")
Loads configuration from defaults, .env files, and environment variables — in the right priority order.
app.configure(
defaults={
"app": {
"name": "my-project",
"debug": False,
"env": "production",
"secret_key": None,
},
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb",
},
"cache": {"driver": "memory"},
"mail": {"driver": "log"},
},
env_path=".env",
)
This does, in order:
- Loads
defaultsas the base config. - Loads
.envif it exists. - Loads
.env.localif it exists (personal overrides, gitignored). - Loads real environment variables (highest file-based priority).
After this, app.config is ready.
app.config
Shorthand for the config instance. Dot-notation access, type casting, the works.
app.config.get("app.name") # "my-project"
app.config.boolean("app.debug") # False
app.config.integer("database.port") # 5432
app.config.get("app.env") # "production" (or APP_ENV from environment)
app.container
Direct access to the container for binding services.
app.container.singleton(CacheContract, RedisCache)
app.container.bind(ReportGenerator, PDFReportGenerator)
app.container.instance("stripe_key", "sk_live_...")
app.events
Direct access to the event dispatcher.
app.events.listen(UserRegistered, SendWelcomeEmail)
app.events.listen(UserRegistered, CreateDefaultSettings, priority=10)
@app.events.listen(OrderCompleted)
def log_order(event):
print(f"Order {event.order_id} completed")
app.make(abstract)
Shorthand for app.container.make(). Resolves any class or binding from the container.
service = app.make(OrderService)
config = app.make("config")
events = app.make("events")
app.boot()
Finalizes the application. Calls boot() on all registered service providers, freezes config (optional), and dispatches AppBooted event.
app.boot()
After boot:
- All service providers have registered and booted.
- The container is fully wired.
AppBootedevent has fired.- The application is ready to handle requests/tasks.
app.is_booted
Property that returns True after boot() has been called.
Service Providers
Service providers are how you organize your application's bindings and boot logic. Each provider is responsible for one subsystem.
Writing a provider
from lucid import ServiceProvider
class DatabaseServiceProvider(ServiceProvider):
def register(self):
"""Bind things into the container. No resolving here."""
self.app.container.singleton(DatabaseContract, lambda c: PostgresDatabase(
host=c.make("config").get("database.host"),
port=c.make("config").integer("database.port"),
name=c.make("config").get("database.name"),
))
def boot(self):
"""All providers have registered. Safe to resolve and interact."""
db = self.app.make(DatabaseContract)
db.connect()
Registering providers
app = Application()
app.configure(defaults)
# Register providers — register() called immediately on each
app.register(DatabaseServiceProvider)
app.register(CacheServiceProvider)
app.register(MailServiceProvider)
# Boot — boot() called on all providers in order
app.boot()
Provider lifecycle
app.register(P) → P(app) instantiated → P.register() called
app.register(Q) → Q(app) instantiated → Q.register() called
app.register(R) → R(app) instantiated → R.register() called
app.boot() → P.boot() → Q.boot() → R.boot() → AppBooted dispatched
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().
Application Events
The framework fires lifecycle events you can hook into.
| Event | When | Payload |
|---|---|---|
AppBooting |
Just before boot() runs providers |
app |
AppBooted |
After all providers have booted | app |
from lucid.events import AppBooted
@app.events.listen(AppBooted)
def on_ready(event):
print(f"{event.app.config.get('app.name')} is ready!")
Using Pipelines
Pipelines are available standalone — no special integration needed. But they shine when combined with the container.
from lucid import Pipeline, Pipe
class ValidateRequest(Pipe):
def __init__(self, config: ConfigContract):
self.config = config
def handle(self, data, next_pipe):
if not data.get("api_key"):
return {"error": "Missing API key"}
if data["api_key"] != self.config.get("app.api_key"):
return {"error": "Invalid API key"}
return next_pipe(data)
class NormalizeData(Pipe):
def handle(self, data, next_pipe):
data["email"] = data.get("email", "").lower().strip()
return next_pipe(data)
# Resolve pipe instances through the container — dependencies autowired
result = (
Pipeline(request_data)
.through([
app.make(ValidateRequest),
NormalizeData(),
lambda data: {**data, "processed": True},
])
.then(save_to_database)
)
Real-World Examples
API service
from lucid import Application, ServiceProvider
# ── Config ──
defaults = {
"app": {"name": "order-api", "debug": False, "secret_key": None},
"database": {"host": "localhost", "port": 5432, "name": "orders"},
"cache": {"driver": "memory"},
"mail": {"driver": "log"},
}
# ── Providers ──
class RepositoryProvider(ServiceProvider):
def register(self):
self.app.container.singleton(UserRepository, PostgresUserRepository)
self.app.container.singleton(OrderRepository, PostgresOrderRepository)
class PaymentProvider(ServiceProvider):
def register(self):
self.app.container.singleton(PaymentGateway, lambda c: StripeGateway(
api_key=c.make("config").get("stripe.secret_key"),
))
class EventListenerProvider(ServiceProvider):
def boot(self):
events = self.app.events
events.listen(OrderCompleted, SendConfirmationEmail)
events.listen(OrderCompleted, UpdateInventory, priority=20)
events.listen(PaymentFailed, NotifySupport)
events.listen(UserRegistered, SendWelcomeEmail)
events.listen(UserRegistered, CreateDefaultSettings, priority=10)
# ── Bootstrap ──
app = Application()
app.configure(defaults)
app.register(RepositoryProvider)
app.register(PaymentProvider)
app.register(EventListenerProvider)
app.boot()
# ── Use ──
order_service = app.make(OrderService)
order_service.process(incoming_order)
CLI tool
from lucid import Application
app = Application()
app.configure({
"app": {"name": "data-migrator"},
"source_db": {"host": "old-db.internal", "port": 5432},
"target_db": {"host": "new-db.internal", "port": 5432},
})
app.boot()
migrator = app.make(DataMigrator)
migrator.run()
Testing
def create_test_app(**config_overrides):
"""Create a fresh application with test doubles."""
app = Application()
defaults = {
"app": {"name": "test", "debug": True, "secret_key": "test-secret"},
"database": {"host": "localhost", "name": "test_db"},
"mail": {"driver": "log"},
}
defaults.update(config_overrides)
app.configure(defaults)
# Swap real implementations for test doubles
app.container.instance(MailerContract, FakeMailer())
app.container.instance(PaymentGateway, FakePaymentGateway(always_succeeds=True))
app.container.instance(CacheContract, InMemoryCache())
app.boot()
return app
def test_order_completion():
app = create_test_app()
service = app.make(OrderService)
result = service.process(test_order)
assert result.status == "completed"
assert app.make(MailerContract).last_sent.subject == "Order Confirmed"
def test_order_with_failed_payment():
app = create_test_app()
app.container.instance(PaymentGateway, FakePaymentGateway(always_fails=True))
service = app.make(OrderService)
result = service.process(test_order)
assert result.status == "payment_failed"
Every test gets a clean application with isolated state. No global singletons, no monkeypatching, no test ordering issues.
Integration with web frameworks
Lucid doesn't replace your web framework — it runs alongside it.
With FastAPI:
from fastapi import FastAPI, Depends
from lucid import Application
# Bootstrap Lucid
lucid = Application()
lucid.configure(defaults)
lucid.register(DatabaseProvider)
lucid.register(CacheProvider)
lucid.boot()
# FastAPI app
api = FastAPI()
def get_lucid():
return lucid
@api.post("/orders")
def create_order(data: OrderRequest, app: Application = Depends(get_lucid)):
service = app.make(OrderService)
return service.process(data)
With Flask:
from flask import Flask
flask_app = Flask(__name__)
lucid = Application()
lucid.configure(defaults)
lucid.boot()
@flask_app.route("/orders", methods=["POST"])
def create_order():
service = lucid.make(OrderService)
return service.process(request.json)
Lucid manages your services, config, and events. The web framework manages HTTP. They compose cleanly without either one taking over.
Directory Conventions
Lucid recommends (but doesn't enforce) this project structure:
my-project/
├── app/
│ ├── __init__.py
│ ├── services/ # Business logic
│ │ ├── order_service.py
│ │ ├── user_service.py
│ │ └── payment_service.py
│ ├── repositories/ # Data access
│ │ ├── user_repository.py
│ │ └── order_repository.py
│ ├── events/ # Event classes
│ │ ├── order_events.py
│ │ └── user_events.py
│ ├── listeners/ # Event listeners
│ │ ├── send_welcome_email.py
│ │ └── update_inventory.py
│ ├── contracts/ # ABCs / interfaces
│ │ ├── cache_contract.py
│ │ ├── mailer_contract.py
│ │ └── payment_contract.py
│ └── providers/ # Service providers
│ ├── database_provider.py
│ ├── cache_provider.py
│ └── mail_provider.py
├── config/
│ └── defaults.py # Default configuration dict
├── .env # Environment defaults
├── .env.local # Personal overrides (gitignored)
├── bootstrap.py # Application setup
├── main.py # Entry point
└── tests/
├── conftest.py # Test app factory
└── ...
bootstrap.py:
from lucid import Application
from config.defaults import defaults
from app.providers.database_provider import DatabaseProvider
from app.providers.cache_provider import CacheProvider
from app.providers.mail_provider import MailProvider
def create_app() -> Application:
app = Application()
app.configure(defaults)
app.register(DatabaseProvider)
app.register(CacheProvider)
app.register(MailProvider)
app.boot()
return app
main.py:
from bootstrap import create_app
app = create_app()
# Your application logic starts here
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.
Architecture
Project Structure
lucid-framework/
├── src/
│ └── lucid/
│ ├── __init__.py # Public API — re-exports everything
│ ├── application.py # Application class
│ ├── events/
│ │ ├── __init__.py
│ │ ├── app_booting.py # AppBooting event
│ │ └── app_booted.py # AppBooted event
│ └── service_provider.py # ServiceProvider base (re-exported or extended)
├── tests/
│ ├── __init__.py
│ ├── test_application.py # Application lifecycle
│ ├── test_configure.py # Config loading through Application
│ ├── test_providers.py # Provider registration and boot
│ ├── test_events.py # Lifecycle events
│ ├── test_make.py # Container resolution through app
│ └── test_integration.py # Full stack integration tests
├── pyproject.toml
├── README.md
├── LICENSE
└── CHANGELOG.md
Implementation Notes
The Application class internals:
from lucid_container import Container
from lucid_config import Config, ConfigContract
from lucid_events import Dispatcher, DispatcherContract
class Application:
def __init__(self):
self._container = Container()
self._config = Config()
self._dispatcher = Dispatcher(container=self._container)
self._providers: list[ServiceProvider] = []
self._booted = False
# Bind core services
self._container.instance("app", self)
self._container.instance(ConfigContract, self._config)
self._container.alias("config", ConfigContract)
self._container.instance(DispatcherContract, self._dispatcher)
self._container.alias("events", DispatcherContract)
@property
def container(self) -> Container:
return self._container
@property
def config(self) -> Config:
return self._config
@property
def events(self) -> Dispatcher:
return self._dispatcher
@property
def is_booted(self) -> bool:
return self._booted
def configure(self, defaults: dict, env_path: str = ".env"):
self._config.load_dict(defaults)
self._config.load_env(env_path)
self._config.load_env(f"{env_path}.local")
self._config.load_env_vars()
def register(self, provider_class: type):
provider = provider_class(self)
self._providers.append(provider)
provider.register()
def make(self, abstract):
return self._container.make(abstract)
def boot(self):
if self._booted:
return
self._dispatcher.dispatch(AppBooting(self))
for provider in self._providers:
provider.boot()
self._booted = True
self._dispatcher.dispatch(AppBooted(self))
What configure() does with missing files:
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.
Re-exports in __init__.py:
The framework re-exports the most common classes so users only need one import:
# lucid/__init__.py
from lucid.application import Application
from lucid.service_provider import ServiceProvider
from lucid.events.app_booting import AppBooting
from lucid.events.app_booted import AppBooted
# Re-export from sub-packages for convenience
from lucid_container import Container
from lucid_config import Config, ConfigContract
from lucid_events import Event, Dispatcher, DispatcherContract, Listener, AsyncListener, Subscriber
from lucid_pipeline import Pipeline, AsyncPipeline, Pipe, AsyncPipe
__all__ = [
# Framework
"Application",
"ServiceProvider",
"AppBooting",
"AppBooted",
# Container
"Container",
# Config
"Config",
"ConfigContract",
# Events
"Event",
"Dispatcher",
"DispatcherContract",
"Listener",
"AsyncListener",
"Subscriber",
# Pipeline
"Pipeline",
"AsyncPipeline",
"Pipe",
"AsyncPipe",
]
This means users can write from lucid import Application, Event, Listener, Pipeline — one import line for everything.
ServiceProvider base class:
The framework either re-exports ServiceProvider from lucid_container or provides its own thin wrapper that adds the self.app property:
class ServiceProvider:
def __init__(self, app: "Application"):
self.app = app
def register(self) -> None:
"""Bind things into the container."""
pass
def boot(self) -> None:
"""Called after all providers have registered."""
pass
Public API
from lucid import Application # The app
from lucid import ServiceProvider # Base for providers
from lucid import AppBooting, AppBooted # Lifecycle events
# Everything from sub-packages, re-exported:
from lucid import Container # DI container
from lucid import Config, ConfigContract # Configuration
from lucid import Event, Dispatcher, DispatcherContract, Listener, AsyncListener, Subscriber # Events
from lucid import Pipeline, AsyncPipeline, Pipe, AsyncPipe # Pipelines
pyproject.toml Specification
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "lucid-framework"
version = "0.1.0"
description = "A Python framework that tells you what to do next."
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Sharik Shaikh", email = "shaikhsharik709@gmail.com" },
]
keywords = [
"framework", "dependency-injection", "config", "events",
"pipeline", "service-provider", "ioc", "convention",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
dependencies = [
"lucid-pipeline>=0.1.0",
"lucid-container>=0.1.0",
"lucid-config>=0.1.0",
"lucid-events>=0.1.0",
]
[project.urls]
Homepage = "https://github.com/sharik709/lucid-framework"
Documentation = "https://github.com/sharik709/lucid-framework#readme"
Repository = "https://github.com/sharik709/lucid-framework"
Issues = "https://github.com/sharik709/lucid-framework/issues"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.mypy]
strict = true
[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "mypy>=1.0", "ruff>=0.1"]
Test Cases to Implement
Application Lifecycle
Application()creates container, config, and dispatcherapp.containerreturns a Container instanceapp.configreturns a Config instanceapp.eventsreturns a Dispatcher instanceapp.is_bootedisFalsebefore boot,Trueafterapp.boot()called twice does nothing the second time
Configure
app.configure(defaults)loads defaults into config- Config values accessible via
app.config.get() - Environment variables override defaults
- Missing
.envfile doesn't raise - Missing
.env.localfile doesn't raise - Type casting works after configure (
boolean,integer, etc.)
Container Access
app.make(SomeClass)resolves from containerapp.make("config")returns the config (alias works)app.make("events")returns the dispatcher (alias works)app.make("app")returns the application itselfapp.container.singleton()bindings work throughapp.make()app.container.bind()bindings work throughapp.make()
Service Providers
app.register(P)instantiates P with the appapp.register(P)callsP.register()immediately- Provider's
self.appis the application instance - Provider can bind into
self.app.containerduringregister() - Provider can read from
self.app.configduringregister() app.boot()callsboot()on all registered providersboot()is called in registration order- Provider boot can resolve bindings from other providers
- Provider boot can access the event dispatcher
- Multiple providers register without conflicts
Lifecycle Events
AppBootingis dispatched at the start ofboot()AppBootedis dispatched after all providers have bootedAppBootingevent carries the app referenceAppBootedevent carries the app reference- Listeners registered before
boot()receive the events AppBootingfires before any providerboot()is calledAppBootedfires after all providerboot()calls complete
Re-exports
from lucid import Applicationworksfrom lucid import ServiceProviderworksfrom lucid import Containerworksfrom lucid import Config, ConfigContractworksfrom lucid import Event, Dispatcher, Listener, Subscriberworksfrom lucid import Pipeline, Pipeworksfrom lucid import AppBooting, AppBootedworks
Integration Tests
- Full stack: configure → register providers → boot → make service → use service
- Provider registers binding, another provider resolves it during boot
- Event listener registered in a provider fires when event dispatched
- Config loaded in configure, read by provider during register
- Container autowires a service whose dependencies were bound by providers
- Pipeline used inside a service that was resolved from the container
- Test double swap: instance() overrides a singleton for testing
Edge Cases
- Application with no providers — boot succeeds
- Application with no config — boot succeeds (empty config)
- Registering a provider after boot raises error (or is silently ignored)
make()beforeboot()works for bindings registered duringregister()- Provider
register()raising an exception surfaces clearly - Provider
boot()raising an exception surfaces clearly - Two providers binding the same abstract — last one wins
The Lucid Ecosystem
┌──────────────────────────────────────────────────────────┐
│ lucid-framework │
│ │
│ Application · ServiceProvider · Lifecycle Events │
│ │
├──────────┬──────────┬──────────────┬─────────────────────┤
│ lucid- │ lucid- │ lucid- │ lucid- │
│ container│ config │ events │ pipeline │
│ │ │ │ │
│ DI + │ .env + │ Typed events │ Multi-step │
│ autowire │ dot │ + listeners │ data chains │
│ │ notation │ + subscribers│ │
└──────────┴──────────┴──────────────┴─────────────────────┘
▼
Feature packages (coming soon)
lucid-cache · lucid-mail · lucid-queue
Each box is an independent PyPI package. Install the framework to get everything, or install only what you need.
Coming Soon
lucid-cache— Multi-driver cache (memory, file, Redis) with a unified API.lucid-mail— Multi-driver mail (SMTP, Mailgun, SES) with templates and queued sending.lucid-queue— Background job processing with swappable backends.lucid-cli— Artisan-style CLI for code generation, migrations, and task scheduling.
License
MIT License. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lucid_framework-0.1.0.tar.gz.
File metadata
- Download URL: lucid_framework-0.1.0.tar.gz
- Upload date:
- Size: 15.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b9b4d8eb9c3608c113e7f151df698811689db677de7470440494ec322a75348
|
|
| MD5 |
9ba3ca29515c4a1cbde21554dc9ef2b7
|
|
| BLAKE2b-256 |
e26ca71d7acd77343cd94c728a844e04855e18c04d2e66b4c75e7827e026536f
|
File details
Details for the file lucid_framework-0.1.0-py3-none-any.whl.
File metadata
- Download URL: lucid_framework-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e767fc5845cd96e6c5bbeec3a461bb1c004673eed0efb1cecc16d87b87f48587
|
|
| MD5 |
ddbed8df302eac92f177ce00b3fd5467
|
|
| BLAKE2b-256 |
445130bd4fff123790904f8d91c52c1e51abb27e0364a2e97f83e39f0b794512
|