REST API used to manage virtual labs and their projects
The service is an async FastAPI application backed by PostgreSQL (async SQLAlchemy + Alembic), Redis, Keycloak (OIDC), Stripe (billing & subscriptions), Mailpit (local SMTP). It is packaged and run with uv and orchestrated locally via Docker Compose.
- Stack & requirements
- Repository layout
- Quick start
- Environment configuration
- Common Make targets
- Authentication & test users
- Keycloak admin UI
- Database & migrations
- Testing
- Billing / Stripe testing
- Operational scripts
- Code quality
- IDE setup
- Contributing
- Funding & acknowledgment
| Tool | Version | Notes |
|---|---|---|
| Python | >=3.12,<4 |
Project targets 3.12 (also tested against 3.13/3.14) |
uv |
latest | Package + virtualenv manager (replaces Poetry) |
| Docker + Docker Compose | latest | Brings up Postgres, Redis, Keycloak (+ its DB), Mailpit, and a Stripe CLI listener |
jq |
any | Used by helper scripts |
| Stripe CLI | optional | For local webhook / Setup Intent testing |
Key runtime dependencies (see pyproject.toml for the full list): fastapi[all], uvicorn, sqlalchemy[asyncio], asyncpg, alembic, pydantic v2, pydantic-settings, python-keycloak, pyjwt, stripe, fastapi-mail, redis, loguru, sentry-sdk[fastapi], obp-accounting-sdk.
Make sure your user is in the
dockergroup so Docker commands don't requiresudo:sudo usermod -aG docker "$USER"
virtual_labs/
├── api.py # FastAPI app factory, middleware, exception handlers, router wiring
├── routes/ # HTTP layer — one module per domain (labs, projects, billing, …)
├── usecases/ # Application use-cases orchestrating services + repositories
├── services/ # Domain services (Stripe, Keycloak, email, accounting, …)
├── repositories/ # Async SQLAlchemy data access
├── domain/ # Pydantic schemas / domain models (request/response DTOs)
├── infrastructure/ # DB session pool, Redis client, settings, transport, kc, email
├── core/ # Cross-cutting primitives: exceptions, response schemas, auth
├── external/ # Adapters for third-party systems
├── shared/ # Shared utilities
├── static/ # Static assets (email templates, etc.)
├── utils/ # Generic helpers
└── tests/ # Pytest suite (async, integration markers)
alembic/ # Alembic env + versioned migrations
scripts/ # One-off and operational scripts (subscription tiers, bulk invite, …)
env-prep/ # Keycloak realm export + seed data used by dev-init.sh
The router wiring lives in virtual_labs/api.py. All routers are mounted under settings.BASE_PATH, and OpenAPI docs are exposed at {BASE_PATH}/docs.
# 1. Install uv (one-time): https://docs.astral.sh/uv/getting-started/installation/
# 2. Install Python deps into a managed virtualenv
make install # equivalent to: uv sync
# 3. Create your local env file (see Environment configuration below)
# Use the committed `.env.development` as a starting point:
cp .env.development .env.local
# 4. Bring up infra + app
make init
# 5. Initialize / migrate the database
make init-db
# 6. Run the API in reload mode against the running stack (optional)
make devThe API will be available at http://127.0.0.1:8000 and the interactive docs at http://127.0.0.1:8000/docs.
The service reads configuration from .env.local (loaded by dev-init.sh and make init). The repo ships .env.development with the non-secret defaults that match docker-compose.yml — use it as a starting template and fill in your own Stripe / Sentry / Keycloak secrets locally.
The authoritative list of supported settings (with defaults and types) lives in virtual_labs/infrastructure/settings.py. Common groups:
- App:
APP_NAME,APP_DEBUG,DEPLOYMENT_ENV,BASE_PATH,CORS_ORIGINS,CORS_ORIGIN_REGEX - Database: async SQLAlchemy URI (
postgresql+asyncpg://…) - Keycloak:
KC_SERVER_URI,KC_REALM_NAME,KC_CLIENT_ID,KC_CLIENT_SECRET - Redis: host / port / credentials
- Stripe:
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_DEVICE_NAME,STRIPE_API_VERSION, tax-billing flags (BILLING_TAX_ENABLED,BILLING_TAX_ENABLED_COUNTRIES,BILLING_TAX_BEHAVIOR,BILLING_TAX_MISSING_COUNTRY_MODE) - Sentry:
SENTRY_DSN,SENTRY_TRACES_SAMPLE_RATE,SENTRY_PROFILES_SAMPLE_RATE
Run make help (or simply make) to print this list in your terminal — every target's description below comes straight from the inline ## … annotations in the Makefile, so it always reflects the truth.
$ make help
help Show this help
install Install all dependencies
upgrade-deps Upgrade all dependencies to latest compatible versions
check-deps Check lock file is up to date
audit Run package auditing
dev Run development api server
init Run project with .env.local file (for local development)
init-ci Run project without env file (for CI/CD environments)
destroy Destroy project containers (with .env.local)
destroy-ci Destroy project containers (without env file)
build Build the Docker image
format Run formatters and auto-fix linting issues
lint Run linters (check only, no modifications)
style-check Run pre-commit style checks
type-check Run static type checks
check-all Run format, lint, style-check and type-check
test Run tests
init-db Create & seed db tables
check-db-schema Check if db schema change requires a migration
migration Create or update the alembic migration
tiers Populate subscription tiersA few of the most common targets in context:
- First-time setup:
make install && make init && make init-db - Day-to-day:
make dev(reload server),make test,make check-allbefore pushing - Migrations:
make migration MESSAGE="…"to autogenerate,make init-dbto apply - Reset the stack:
make destroy(drops containers + volumes)
The dev stack ships with a Keycloak realm (obp-realm) populated by env-prep/realm-export.json.
make init automatically copies the token for user test (the user allowed to create virtual labs) to your clipboard.
You can also fetch a token at any time:
./get_user_token.sh
# Prompts for username — valid values: test, test-1, test-2The token is both printed to stdout and copied to your clipboard.
The Keycloak container is exposed on host port 9090 with --hostname-admin=http://localhost:9090, so the admin console is reachable out of the box at:
- URL:
http://localhost:9090 - Username / password:
admin/admin(see docker-compose.yml) - Realm used by the API:
obp-realm(seeded from env-prep/realm-export.json)
Migrations live in alembic/versions. Alembic config is in alembic.ini.
Generate a new migration (autogenerated from model changes):
make migration MESSAGE="add foo column to virtual_labs"
# or directly:
uv run alembic revision --autogenerate -m "add foo column"Always review autogenerated migrations. Alembic cannot detect every schema change — see the autogenerate caveats.
Apply migrations:
make init-db
# or:
uv run alembic upgrade headCheck whether a migration is required:
make check-db-schemaRun the full suite (also populates test subscription tiers first):
make testRun a subset directly:
uv run pytest virtual_labs/tests/path/to/test_file.py -k some_test -xIntegration tests are marked with @pytest.mark.integration (see pyproject.toml).
The STRIPE_SECRET_KEY test key and STRIPE_DEVICE_NAME=dev must be present in .env.local. Swiss VAT / tax-billing knobs are documented in SUBSCRIPTION.md and the BILLING_TAX_* settings.
Attaching a payment method is normally a frontend flow (stripe.confirmSetup()). To test from the backend manually:
- Create a Setup Intent via
POST /virtual-labs/{virtual_lab_id}/billing/setup-intent. - Confirm it through the Stripe CLI:
stripe setup_intents confirm seti_1PFtBwFjhkSGAqrAUHCvTAAA \
--payment-method=pm_card_visaReferences: Stripe Setup Intents · Stripe CLI.
Installed as uv run entrypoints (defined in pyproject.toml):
| Command | Purpose |
|---|---|
uv run populate-tiers |
Seed/refresh subscription tiers (--test for test mode) — scripts/populate_subscription_tiers.py |
uv run upgrade-subscription |
Upgrade an existing subscription — scripts/upgrade_subscription.py |
uv run send_emails |
Send templated emails — scripts/send_emails.py |
uv run manage-coupons |
Manage Stripe coupons — scripts/manage_stripe_coupons.py |
uv run bulk-invite |
Bulk-invite users to a project — scripts/bulk_invite_to_project.py |
uv run migrate-tax-billing |
One-off migration to the tax-billing model — scripts/migrate_to_tax_billing.py |
- Formatting & linting: Ruff —
make format/make lint - Static typing:
ty—make type-check(config inpyproject.toml) - Pre-commit hooks: ruff format + ruff lint on push
Install hooks once after cloning:
uv run pre-commit installCI runs style-check against .pre-commit-config-ci.yaml.
- Point the Python interpreter at the uv-managed venv:
.venv/bin/pythonin the project root (created byuv sync). - Recommended extensions:
- Ruff (
charliermarsh.ruff) - Python (
ms-python.python)
- Ruff (
The project uses ty (configured in pyproject.toml) as the type checker — invoke it via make type-check.
See CONTRIBUTING.md. In short:
- Branch off
main. uv run pre-commit install(first time only).- Keep PRs focused; include tests where reasonable.
- Run
make check-allandmake testbefore opening a PR.
The development of this software was supported by funding to the Blue Brain Project, a research center of the École polytechnique fédérale de Lausanne (EPFL), from the Swiss government's ETH Board of the Swiss Federal Institutes of Technology.
Copyright © 2024 Blue Brain Project/EPFL Copyright © 2025 Open Brain Institute