# 10 — Full app: integration

The integration story tying every component together. A real,
working application shape that uses lifecycle, exceptions,
config, settings, observability, vault, progress, and CLI
together.

> **Read first:** [`library_common.md`](library_common.md) and
> at least skim every other doc in this directory.

---

## Motivation

Each per-component doc shows one piece in isolation. Real
applications use most of them at once. This doc shows the
canonical shape for a typical service / CLI app — which
integration patterns work together, which ones don't compose,
where the seams are.

---

## Goal

Show how a 200-line consumer app uses the library. Not a
toy — the shape mirrors what
[`example_app/`](../../example_app/) ships as the reference
second-consumer.

---

## Layout

```
myapp/
├── pyproject.toml mgf-common pinned: >=0.4.0,<0.5
├── src/myapp/
│ ├── __init__.py __version__
│ ├── __main__.py python -m myapp dispatch
│ ├── settings.py AppSettings (composing MgfSettings)
│ ├── cli.py custom subcommands + main()
│ ├── domain/ your business logic (typed exceptions)
│ └── adapters/ translation seams (e.g., third-party SDKs)
└── tests/
    ├── unit/
    └── conftest.py         bootstrap_for_test fixture
```

---

## Settings (`src/myapp/settings.py`)

Compose `MgfSettings` as a sub-field — the canonical pattern
from [`04_settings.md`](04_settings.md):

```python
from typing import Annotated

from mgf.common.config import BaseAppSettings, Field, settings_config
from mgf.common.settings import MgfSettings

class AppSettings(BaseAppSettings):
    model_config = settings_config(
        env_prefix="MYAPP_",
        env_nested_delimiter="__",
    )

    # Domain fields:
    database_url: str = "sqlite:///./app.db"
    request_timeout_seconds: Annotated[int, Field(ge=1, le=300)] = 30

    # mgf-common's settings as a sub-field:
    mgf: MgfSettings = Field(default_factory=MgfSettings)
```

Operators set things via either:

```bash
# Domain fields
export MYAPP_DATABASE_URL=postgresql://prod-db/myapp

# mgf-common sub-fields
export MYAPP_MGF__LOGGING__LEVEL=INFO
```

OR `pyproject.toml`:

```toml
[tool.myapp]
database_url = "postgresql://prod-db/myapp"

[tool.myapp.mgf.logging]
level = "INFO"
format = "json"
```

---

## Domain code (`src/myapp/domain/...`)

Typed exceptions per [`02_exceptions.md`](02_exceptions.md):

```python
from mgf.common.exceptions import AppError, ResourceNotFoundError, ProviderError

class MyAppError(AppError):
    """Project-wide nominal alias."""

class DomainNotFoundError(ResourceNotFoundError):
    def __init__(self, name: str) -> None:
        super().__init__(name)
        self.args = (f"no domain {name!r}; run 'myapp domain list'",)

class DomainCreationError(ProviderError):
    """Provider boundary failure during domain creation."""
```

Consumer code raises typed errors; the CLI translator
([`09_cli.md`](09_cli.md)) maps them to stable exit codes.

Library-level code (NOT bootstrapped) uses the observability
helpers per [`05_observability.md`](05_observability.md):

```python
from mgf.common.observability import get_logger, operation_span

LOG = get_logger(__name__)

def fetch_domain(domain_id: str) -> Domain:
    with operation_span("fetch_domain", domain_id=domain_id):
        LOG.info("fetching", extra={"domain_id": domain_id})
        # ... real work ...
```

---

## CLI (`src/myapp/cli.py`)

```python
import argparse
import sys
from typing import TYPE_CHECKING

from mgf.common import bootstrap
from mgf.common.cli import (
    EXIT_SUCCESS,
    build_cli,
    dispatch,
    run_and_translate,
    subcommand,
)
from mgf.common.observability import get_logger

from myapp.settings import AppSettings

if TYPE_CHECKING:
    from collections.abc import Sequence

LOG = get_logger(__name__)

def _setup_create(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("name")
    parser.add_argument("--memory", type=int, default=2048)

@subcommand("create", help="Create a new domain", parser_setup=_setup_create)
def run_create(args: argparse.Namespace) -> int:
    settings = AppSettings()
    LOG.info("creating", extra={"name": args.name, "memory": args.memory})
    # ... domain creation logic — typed exceptions propagate ...
    return EXIT_SUCCESS

def main(argv: "Sequence[str] | None" = None) -> int:
    if argv is None:
        argv = sys.argv[1:]
    settings = AppSettings()
    parser = build_cli(prog="myapp", description="My App CLI.")
    args = parser.parse_args(list(argv))
    with bootstrap(
        app_name="myapp",
        app_version="1.0.0",
        settings=settings.mgf,
    ):
        return dispatch(args, parser=parser)

if __name__ == "__main__":
    sys.exit(run_and_translate(main, sys.argv[1:]))
```

This gives the user:
- `myapp create <name>` — your custom subcommand
- `myapp doctor` — install-state + wiring health checks (free, from `mgf-common`)
- `myapp explain` — active settings with provenance (free)
- `myapp validate <config.yaml>` — schema-validate (free)
- `myapp migrate <from> <to> <path>` — codemod (free)
- Stable exit codes for typed exceptions
- Crash report on disk if anything escapes the catch-all

---

---

## Service shape (FastAPI)

The CLI shape above covers one-shot tools and long-running batch
workers. The other canonical shape is an HTTP service. `mgf-common`
adds four optional extras that compose into a complete service:

- `[fastapi]` — lifespan + middleware + Depends helpers
- `[http]` — typed async HTTP client for upstream calls
- `[db]` — async SQLAlchemy engine + per-request session
- `[alembic]` — env.py helper for schema migrations

### Service layout

```
myservice/
├── pyproject.toml mgf-common[fastapi,http,db,alembic]
├── alembic.ini
├── alembic/
│ ├── env.py from mgf.common.alembic import configure_env
│ └── versions/
├── src/myservice/
│ ├── __init__.py
│ ├── settings.py AppSettings (composing MgfSettings)
│ ├── app.py FastAPI app + lifespan
│ ├── db/ SQLAlchemy models (Base.metadata)
│ ├── api/ routers; raise typed AppErrors
│ └── upstream/ AsyncHttpClient instances
└── tests/
    ├── unit/
    └── conftest.py
```

### `src/myservice/app.py`

```python
from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import AsyncSession

from mgf.common._lifecycle import bootstrap
from mgf.common.db import get_session, setup_db
from mgf.common.fastapi import (
    ExceptionTranslationMiddleware,
    RequestIdMiddleware,
)
from mgf.common.http import AsyncHttpClient

from myservice.settings import AppSettings

@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = AppSettings()
    with bootstrap(
        app_name="myservice",
        app_version="1.0.0",
        settings=settings.mgf,
    ):
        async with setup_db(app, database_url=settings.database_url):
            app.state.upstream = AsyncHttpClient(
                base_url=settings.upstream_url,
            )
            try:
                yield
            finally:
                await app.state.upstream.aclose()

app = FastAPI(lifespan=lifespan)
app.add_middleware(ExceptionTranslationMiddleware)
app.add_middleware(RequestIdMiddleware)

@app.get("/users/{user_id}")
async def get_user(
    user_id: str,
    session: Annotated[AsyncSession, Depends(get_session)],
) -> dict:
    # Typed errors bubble up; ExceptionTranslationMiddleware maps
    # them to JSON with the right status code.
    user = await session.get(User, user_id)
    if user is None:
        raise ResourceNotFoundError(name=user_id)
    return {"id": user.id, "name": user.name}
```

That's the entire app file. The composition is intentional:

- `bootstrap()` runs FIRST inside the lifespan — identity, logging,
  OTel, crash hooks wired before anything else opens.
- `setup_db()` runs INSIDE `bootstrap()` — engine pool created with
  identity already set; logs from SQLAlchemy carry the right
  app-name / version.
- `AsyncHttpClient` is constructed AFTER `setup_db()` — so its
  default User-Agent is auto-derived from the running identity.
- Both `setup_db()` and `bootstrap()` LIFO-unwind on shutdown:
  upstream client closes first, then DB pool disposes, then logging
  detaches.

What you get for free:

- Per-request `X-Request-Id` propagation (PEP 567 contextvar; survives
  async children).
- Typed `AppError` → JSON with the right HTTP status (no per-endpoint
  error handling needed).
- Async DB session per request, disposed when the handler returns.
- Engine pool disposed cleanly on app shutdown (no FD leaks).
- Production-leaning defaults: `pool_pre_ping=True`,
  `pool_recycle=3600`, `expire_on_commit=False`, retry on transient
  upstream failure.

### `alembic/env.py`

```python
from mgf.common.alembic import configure_env

from myservice.db.base import Base
from myservice.settings import AppSettings

configure_env(
    database_url=AppSettings().database_url,
    target_metadata=Base.metadata,
    naming_convention={
        "ix": "ix_%(column_0_label)s",
        "uq": "uq_%(table_name)s_%(column_0_name)s",
        "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
        "pk": "pk_%(table_name)s",
    },
)
```

That's the whole `env.py`. `alembic upgrade head` /
`alembic revision --autogenerate -m "..."` work as before — async
drivers (asyncpg / aiomysql / aiosqlite / psycopg async / asyncmy)
auto-detected by URL prefix.

### Settings shape (additions for the service variant)

```python
from pydantic import Field
from mgf.common.config import BaseAppSettings, settings_config
from mgf.common.settings import MgfSettings

class AppSettings(BaseAppSettings):
    model_config = settings_config(
        env_prefix="MYSERVICE_",
        env_nested_delimiter="__",
    )

    database_url: str
    upstream_url: str = "https://api.upstream.com"

    mgf: MgfSettings = Field(default_factory=MgfSettings)
```

Operators set:

```bash
export MYSERVICE_DATABASE_URL=postgresql+asyncpg://user:pass@db/myservice
export MYSERVICE_UPSTREAM_URL=https://api.prod.example.com
export MYSERVICE_MGF__LOGGING__LEVEL=INFO
```

### Per-recipe deep dives

The recipes in `docs/public/recipes/` cover each piece in detail:

- [`fastapi.md`](recipes/fastapi.md) — middleware order, Depends
  helpers, custom error mappings (RFC 7807 / JSON:API).
- [`http.md`](recipes/http.md) — retry policy, vault-resolved auth,
  testing with respx.
- [`db.md`](recipes/db.md) — Postgres RLS multi-tenancy via
  `tenant_session`, manual session patterns.
- [`alembic.md`](recipes/alembic.md) — multi-database (`-x db=...`),
  naming-convention recommendation.

---

## Tests (`tests/conftest.py`)

```python
import pytest
from mgf.common import bootstrap_for_test, AppContext

@pytest.fixture
def app_ctx() -> AppContext:
    with bootstrap_for_test() as ctx:
        yield ctx

@pytest.fixture(autouse=True)
def _disable_pyproject(monkeypatch: pytest.MonkeyPatch) -> None:
    """Hermetic — host shell pyproject doesn't leak in."""
    monkeypatch.setenv("MGF_DISABLE_PYPROJECT", "1")
```

Test bodies use `app_ctx` for any code that needs identity /
observability. Per-component test patterns are in
[`01_lifecycle.md`](01_lifecycle.md) §"Test bootstrap with
capture".

---

## Reference consumer

[`example_app/`](../../example_app/) is a real, packaged
implementation of this shape:

- [`src/example_app/settings.py`](../../example_app/src/example_app/settings.py)
  — composition pattern
- [`src/example_app/cli.py`](../../example_app/src/example_app/cli.py)
  — custom subcommand + `bootstrap` + `dispatch`
- [`tests/test_smoke.py`](../../example_app/tests/test_smoke.py)
  — 10 end-to-end tests using `bootstrap_for_test`

Its CI step (the Woodpecker `compat-matrix-example-app`) runs
its test suite against the in-repo `mgf-common` on every push
— if a `mgf-common` public-API regression breaks the consumer,
CI catches it before release.

---

## Links

- **Per-component docs:** every other file in this directory
  ([`01_lifecycle.md`](01_lifecycle.md) through
  [`09_cli.md`](09_cli.md))
- **Reference consumer:** [`example_app/`](../../example_app/)
- **Layered example:**
  [`examples/06_full_app/`](../../examples/06_full_app/) — three
  layers (simple → practical → advanced)
- **Engineering standards:**
  [`docs/standards/`](../standards/) — what every consuming
  project commits to
- **Public-API contract:** [`PUBLIC_API.md`](../../PUBLIC_API.md)
