Adding a Service
This guide walks through creating a new Python microservice in the AEGIS monorepo, following the same patterns used by existing services.
Directory Structure
Every Python service follows this layout:
services/my-service/
├── pyproject.toml # Poetry config + pytest settings
├── poetry.lock # Locked dependencies
├── src/
│ └── my_service/ # Python package (underscore, not hyphen)
│ ├── __init__.py
│ ├── main.py # FastAPI app with lifespan
│ ├── config.py # Settings from environment variables
│ └── ... # Business logic modules
└── tests/
├── __init__.py
├── conftest.py # Shared test fixtures
└── test_*.py # Test filesThe directory name uses hyphens (my-service) but the Python package name uses underscores (my_service). This follows the convention of every existing AEGIS service.
Step 1: Create the pyproject.toml
[tool.poetry]
name = "my-service"
version = "0.1.0"
description = "Description of what this service does in AEGIS"
authors = ["AEGIS Team"]
packages = [{include = "my_service", from = "src"}]
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.115"
uvicorn = {version = "^0.34", extras = ["standard"]}
aegis-shared = {path = "../../shared", develop = true}
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
pytest-asyncio = "^0.23"
httpx = "^0.28"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
asyncio_mode = "auto"Key points:
- The shared library is always included as a path dependency:
{path = "../../shared", develop = true} asyncio_mode = "auto"is required for async tests- Add additional dependencies as needed (e.g.,
asyncpgfor PostgreSQL,redisfor Redis)
Step 2: Create the FastAPI Application
# src/my_service/main.py
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from aegis_shared.db.postgres import PostgresPool
from my_service.config import settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ── Globals set during lifespan ────────────────────────────────────────
pg: PostgresPool
@asynccontextmanager
async def lifespan(app: FastAPI):
global pg
pg = PostgresPool(dsn=settings.DATABASE_URL)
await pg.connect()
logger.info("My service started — PostgreSQL connected")
yield
await pg.disconnect()
logger.info("My service stopped")
app = FastAPI(
title="AEGIS My Service",
version="0.1.0",
description="Description of what this service does",
lifespan=lifespan,
)
# ── Health ─────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return {"status": "ok", "service": "my-service"}
# ── Endpoints ──────────────────────────────────────────────────────────
# Add your service-specific endpoints here
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=settings.HOST, port=settings.PORT)Step 3: Create the Config Module
# src/my_service/config.py
import os
class Settings:
HOST: str = os.getenv("HOST", "0.0.0.0")
PORT: int = int(os.getenv("PORT", "8010"))
DATABASE_URL: str = os.getenv(
"DATABASE_URL",
"postgresql://aegis:aegis_local@localhost:5432/aegis",
)
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
settings = Settings()Step 4: Create the init.py Files
mkdir -p services/my-service/src/my_service
touch services/my-service/src/my_service/__init__.py
mkdir -p services/my-service/tests
touch services/my-service/tests/__init__.pyStep 5: Set Up Tests
Create a conftest.py with appropriate fixtures. The pattern depends on your service’s infrastructure needs.
For a PostgreSQL-backed service:
# tests/conftest.py
from contextlib import asynccontextmanager
from typing import Any
import pytest
from fastapi.testclient import TestClient
class FakePostgresPool:
"""In-memory PostgreSQL mock."""
def __init__(self):
self.data: dict[str, list[dict[str, Any]]] = {}
async def execute(self, query: str, *args) -> str:
return "OK"
async def fetch(self, query: str, *args) -> list[dict[str, Any]]:
return []
async def fetchrow(self, query: str, *args) -> dict[str, Any] | None:
return None
async def fetchval(self, query: str, *args):
return None
async def connect(self):
pass
async def disconnect(self):
pass
@pytest.fixture
def fake_pg():
return FakePostgresPool()
@pytest.fixture
def app_client(fake_pg):
import my_service.main as main_module
main_module.pg = fake_pg
@asynccontextmanager
async def noop_lifespan(app):
yield
main_module.app.router.lifespan_context = noop_lifespan
return TestClient(main_module.app)Create a basic test file:
# tests/test_api.py
class TestHealth:
def test_health(self, app_client):
r = app_client.get("/health")
assert r.status_code == 200
assert r.json()["service"] == "my-service"Step 6: Install Dependencies and Verify
cd services/my-service
poetry install
poetry run pytest -v
poetry run uvicorn my_service.main:app --port 8010Step 7: Add to the API Gateway
The Go API gateway at services/api-gateway/cmd/gateway/main.go routes requests to backend services. To add your new service, add a route mapping.
The gateway forwards requests matching URL path prefixes to the appropriate backend. You need to add an entry that maps a path prefix to your service’s port:
/api/v1/my-feature -> http://localhost:8010The exact mechanism for adding routes depends on the gateway’s current implementation. Read services/api-gateway/cmd/gateway/main.go to understand the routing table format before making changes.
Step 8: Add to start-all.sh
Add your service to infrastructure/scripts/start-all.sh:
# Add after the existing service start commands
start_python_service "my-service" "my_service.main:app" 8010 "My Service"Also add the port to the stop function’s cleanup list:
for port in 8000 8001 8002 8003 8004 8005 8006 8007 8009 8010; do
lsof -ti:$port | xargs kill -9 2>/dev/null || true
doneStep 9: Add Shared Model Imports (if needed)
If your service needs to use or define shared models:
-
Using existing models — import from the shared library:
from aegis_shared.models.common import AegisBase, TenantEntity from aegis_shared.models.oilgas import Well, Lease -
Adding new models — create or extend files in
shared/src/aegis_shared/models/:See the Shared Models page for details on adding new models.
Step 10: Add Database Tables (if needed)
If your service needs new database tables:
-
Create a migration SQL file in
infrastructure/docker/postgres/:infrastructure/docker/postgres/0XX_my_service_tables.sql -
Add the volume mount in
docker-compose.yml:volumes: - ./infrastructure/docker/postgres/0XX_my_service_tables.sql:/docker-entrypoint-initdb.d/0XX_my_service_tables.sql -
For the init script to run, you need a fresh volume:
docker compose down rm -rf docker-volumes/postgres docker compose up -d postgres
For ongoing schema changes after initial setup, use Alembic migrations per-service rather than modifying init scripts.
Checklist Summary
| Step | Action | File/Location |
|---|---|---|
| 1 | Create directory structure | services/my-service/ |
| 2 | Create pyproject.toml | services/my-service/pyproject.toml |
| 3 | Create FastAPI app | services/my-service/src/my_service/main.py |
| 4 | Create config | services/my-service/src/my_service/config.py |
| 5 | Create __init__.py files | src/my_service/__init__.py, tests/__init__.py |
| 6 | Create test fixtures | services/my-service/tests/conftest.py |
| 7 | Create test files | services/my-service/tests/test_*.py |
| 8 | Install and verify | poetry install && poetry run pytest |
| 9 | Add gateway route | services/api-gateway/cmd/gateway/main.go |
| 10 | Add to startup script | infrastructure/scripts/start-all.sh |
| 11 | Add DB tables (if needed) | infrastructure/docker/postgres/0XX_*.sql |
| 12 | Export shared models (if needed) | shared/src/aegis_shared/models/ |