Skip to content

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 http extra. 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:

python -m pip install "xyberos[http]" uvicorn

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:

xyberos new myapp
cd myapp
python -m pip install -e ".[http,dev]" uvicorn
pytest

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. Greeter is registered by its own class, and a service that needs it declares Annotated[Greeter, Inject()]. There is no container to look things up in and no parameter-name magic.
  • The lifecycle is explicit. app.lifespan() runs validate, start, and stop. Nothing is constructed eagerly unless you ask for it, and everything the application owns is closed at the end of the block.
  • import xyberos imports 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())
uvicorn --app-dir . app:asgi_app
curl "http://127.0.0.1:8000/?name=reader"

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:

[app]
name = "hello_app"
environment = "development"

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:

app.provide(Greeter, instance=Greeter("hello_app"), eager=True)

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_app before you deploy: it validates the provider graph and compiles handler signatures without starting anything (see the command-line guide).
  • Set environment = "production" and debug = false in 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.