Architecture¶
EndoCore is a thin, legible pipeline. This page maps the whole system so you always know where a request is and which module owns it.
The two halves¶
endocore/ ← the framework (installed package, the `endo` CLI)
your_app/ ← your application (Api/, Services/, Models/, ...)
You import endocore; you write your_app. The framework never generates
your business logic — it discovers and serves it.
Package layout¶
endocore/
core/
discovery.py # scan Api/ tree -> RouteSpec list (the one tree-walk)
router.py # path parsing + matching rules (version, [id], method)
registry.py # route trie + resolver (built once at boot, cached)
loader.py # importlib dynamic import of handlers (error-resilient)
request.py # Request over the ASGI scope
response.py # Response / StreamingResponse -> ASGI send
websocket.py # WebSocket over the ASGI websocket scope
middleware.py # onion / call_next chain
di.py # Depends() + provider resolution
application.py # async def app(scope, receive, send) — ties it together
cache.py config.py signing.py pubsub.py openapi.py exceptions.py
middleware/ # shipped middleware: logging, cors, csrf, gzip, ...
orm/ # the ORM (models, fields, query, compiler, backends, ...)
cli/ # the `endo` CLI (create, dev, routes, migrate, ...)
extensions/ # Redis, Celery, Email, Cache integrations
Boot sequence¶
When the app starts, Application.boot():
- Puts the app directory on
sys.path(sofrom Services... import ...works). - Scans
Api/once withpathlib.rglob("*.py")→ a list ofRouteSpec. - Imports each handler with
importlib, wrapped intry/except. One broken file is collected as aBootError, never fatal. - Registers handlers into a route trie (by version → segment).
- Loads user middleware (
Middleware/__init__.py), hooks (hooks.py), DI providers (providers.py), and extensions (extensions.py). - Builds the middleware pipeline once and logs a boot summary
(
loaded N routes, M files with errors).
The route tree is cached. In dev, a watchfiles watcher rebuilds it
in-process on change — no restart.
Request flow¶
┌─────────────────── middleware chain (onion) ───────────────────┐
scope/receive → │ logging → [ your middleware... ] → dispatch → handler → Response│ → send
└────────────────────────────────────────────────────────────────┘
Application.__call__receives the raw ASGI(scope, receive, send).- For
http, it builds aRequestand runs it through the pipeline. - The innermost layer,
_dispatch, resolves the route in the trie, injects dependencies (di.solve), calls yourhandler, and coerces the return value into aResponse. - The
Responsewrites itself tosend.
websocket scopes are handled by _handle_websocket; lifespan runs
startup/shutdown hooks and the dev watcher.
The resolver (the heart)¶
Given POST /v2/user/42/role, the resolver:
- splits the path into segments;
- checks the first segment against
^v\d+$→ the version; - walks the trie, preferring static children over the dynamic (
[id]) child, capturing path params; - looks up the method (
POST) at the matched node.
Outcomes: 200 (matched), 404 (no such path/version), 405 (path exists,
wrong method). No version prefix → 404 by default (opt-in "latest" alias).
Layers in your app¶
| Layer | Role |
|---|---|
API (Api/) |
endpoint files — thin: parse → call service → respond |
| Services | all business logic; heavy code lives here |
| Models | data description (ORM models) |
| Middleware | request wrapping (auth, logging, CORS, ...) |
| Utils | pure functions, no side effects |
This separation is what makes versioning cheap: a new version copies thin
endpoints and local services, while global /Services/ stay shared.