Getting started¶
Status: Phase 7. This guide takes you from an empty directory to a running application. It uses only stable core APIs and the optional
httpextra. For what "stable" means here, see the stability guide.
Xyberos requires Python 3.10 or newer. Install it, with the http extra, alongside an ASGI server:
uvicorn is not a Xyberos dependency. Xyberos does not implement an ASGI server, so any ASGI server
can run the application you build here.
To start a new project instead of reading the rest of this page first, scaffold one:
It writes the same shape this guide builds by hand, plus a settings file and a test. See the command-line guide.
The smallest application¶
An application is a composition root: you name the services it has and the routes it answers, and the runtime owns their lifetimes.
import asyncio
from typing import Annotated
from xyberos import App, Inject
class Greeter:
def greet(self, name: str) -> str:
return f"hello, {name}"
async def main() -> None:
app = App()
app.provide(Greeter)
async with app.lifespan():
greeter = await app.resolve(Greeter)
print(greeter.greet("world"))
asyncio.run(main())
Three things are worth noticing, because everything else follows from them:
- Nothing was configured by name.
Greeteris registered by its own class, and a service that needs it declaresAnnotated[Greeter, Inject()]. There is no container to look things up in and no parameter-name magic. - The lifecycle is explicit.
app.lifespan()runsvalidate,start, andstop. Nothing is constructed eagerly unless you ask for it, and everything the application owns is closed at the end of the block. import xyberosimports no optional dependency. The HTTP, database, AI-adapter, and durable packages are separate, so a program that does not use them cannot break because of them.
Add HTTP¶
from typing import Annotated
from starlette.applications import Starlette
from starlette.requests import Request
from xyberos import App, Inject, Settings
from xyberos.http import HttpAdapter
class Greeter:
def greet(self, name: str) -> str:
return f"hello, {name}"
def create_app() -> App:
app = App(settings=Settings.load())
app.provide(Greeter)
@app.get("/")
async def greet(request: Request, greeter: Annotated[Greeter, Inject()]) -> dict[str, str]:
return {"greeting": greeter.greet(request.query_params.get("name", "world"))}
return app
def build_asgi_app(app: App) -> Starlette:
return HttpAdapter(app).asgi_app
asgi_app = build_asgi_app(create_app())
The handler's parameters are the whole interface: request: Request comes from Starlette, and
anything marked Inject() comes from the graph, resolved inside the request scope. Both are
validated when HttpAdapter(...) is constructed, so a handler with a typo or a missing provider
fails at startup rather than on the first request. See the HTTP guide.
A complete, runnable version of this is the hello app example, including a settings file and an integration test that exercises it.
Configure it¶
Settings come from xyberos.toml:
Precedence, highest first: explicit arguments, XYBEROS_ environment variables, the configuration
file, then defaults. Nesting uses a double underscore, so XYBEROS_APP__NAME=other overrides the
file, and XYBEROS_CONFIG=/etc/xyberos.toml replaces the file that is searched for.
Read settings through the application, not by importing a global: app.settings.app.name,
app.settings.app.environment, app.settings.app.debug, and
app.settings.app.shutdown_timeout. An unknown key, an incompatible type, or an unsupported
schema_version is rejected with a field-level ConfigurationError that names the field — settings
are validated before the application starts, never during a request.
Optional capabilities own their own sections, [plugins.<name>] and [ai.<name>], with the same
precedence. Core keeps their contents opaque; the capability validates its own keys. See the
plugin guide and the AI guide.
Choose lifetimes¶
| Scope | One instance per | Use it for |
|---|---|---|
Scope.APP (default) |
application | connection pools, clients, configuration-backed services |
Scope.REQUEST |
request | per-request state, a unit of work, anything holding a request value |
Scope.TRANSIENT |
resolution | stateless helpers; may not own a managed resource |
app.provide(Greeter) registers an app-scoped service and lets the runtime construct it. Pass
factory= or instance= when construction needs arguments, and eager=True when the service must
be proven to start before the application accepts work:
A longer-lived service may not depend on a shorter-lived one, and validation says so with the resolution path. The runtime guide covers the rules in full.
Grow the application¶
| You want | Read | Install |
|---|---|---|
| A project scaffold, a config check, a dev server, migrations | command-line guide | core |
| HTTP routes, middleware, error responses | HTTP guide | .[http] |
| SQLite with migrations and transaction retries | database operations guide | .[database] |
| Contribution points, typed plugin configuration | plugin guide | core |
| Retries, timeouts, branching multi-step work | workflow guide | core |
| A provider-neutral model facade and tools | AI guide | core, .[ollama] for Ollama |
| Surviving a process death mid-workflow | durable execution guide | .[durable] |
| Writing tests without live infrastructure | testing guide | .[http] for the test client |
| What broke and why | troubleshooting guide | — |
A conventional layout keeps the optional extras at the edges:
myapp/
├── xyberos.toml # configuration
├── migrations/ # Alembic revisions, run at deploy time
└── src/myapp/
├── app.py # create_app(): composition and routes only
├── service.py # business rules; no HTTP types
└── tables.py # SQLAlchemy Core tables
Routes and composition belong in one module, business rules in another, and neither needs the other to be testable: a service is a plain class, which is why the test suites in this repository can check behavior without a server. The SQLite CRUD example is this layout, filled in.
Next steps¶
- Read the runtime guide for providers, scopes, lifecycle, and configuration.
- Read the testing guide before you write your first test; the deterministic doubles are what make a clock-dependent or model-dependent test fast.
- Run
xyberos check --app myapp.app:create_appbefore you deploy: it validates the provider graph and compiles handler signatures without starting anything (see the command-line guide). - Set
environment = "production"anddebug = falsein your deployment configuration, and read the security guide before you expose the application to a network.
If you would rather be walked through all of this one step at a time, read the tutorial instead. If you are reviewing the design rather than using it, read the architecture review.