Open-source Django chassis for building production-ready microservices.
Cross-cutting concerns out of the box — trace, log, observe, scale.
🛠️ Tech Stack
✨ Features
|
🏛️ BaseModel
UUID PK · timestamps · soft-delete · |
🏗️ DDD Scaffold CLI
Layers by actor: |
|
📡 Observability Structured JSON logging · OpenTelemetry tracing · B3 propagation · OTLP export · trace context vars |
🏢 Multi-tenancy
Schema-based tenant isolation via |
|
🔐 Secret Management AWS Secrets Manager integration with automatic config loading |
📚 API Docs
Auto-generated Swagger / ReDoc via |
|
🔀 Read Replicas Database router for read/write separation |
🧙 Interactive CLI TUI wizard (Textual) or automatic fallback to plain prompts |
📦 Installation
# Core only
pip install pyms-django-chassis
# Recommended profile for most microservices
pip install "pyms-django-chassis[baas]"
# Add the TUI wizard on top of any profile
pip install "pyms-django-chassis[baas,tui]"
# Everything
pip install "pyms-django-chassis[all]"
Optional extras
| Extra | Packages | Description |
|---|---|---|
monitoring |
opentelemetry-api · sdk · propagator-b3 · exporter-otlp-proto-http |
Distributed tracing + OTLP export |
aws |
boto3 |
AWS Secrets Manager |
tenant |
django-tenants · psycopg2-binary |
Schema-based multi-tenancy (PostgreSQL) |
docs |
drf-spectacular |
OpenAPI · Swagger UI · ReDoc |
restql |
django-restql |
Dynamic field filtering via query params |
import-export |
django-import-export |
CSV / XLSX import and export |
dev-tools |
django-debug-toolbar · django-extensions |
Development utilities |
tui |
textual |
Interactive terminal wizard (see CLI) |
baas |
tenant + docs + restql + monitoring + aws |
Backend-as-a-Service profile |
daas |
tenant + docs + restql + import-export + monitoring + aws |
Data-as-a-Service profile |
all |
all of the above except tui |
Full feature set |
[!NOTE]
tuiis not included inall. Install it explicitly if you want the interactive wizard.
🚀 Quick Start
1. Generate a new microservice
pip install "pyms-django-chassis[tui]"
pyms-django startproject my-service
The wizard covers 3 steps + confirmation:
| Step | Fields |
|---|---|
| 1 · Project Setup | Package manager (uv / poetry) · SERVICE_NAME · BASE_PATH · Python version (3.11–3.14) · Django version (4.2 LTS – 6.0) |
| 2 · Features | Multi-tenancy toggle · Extras with inline descriptions · live counter N/7 · synced all checkbox |
| 3 · DDD Structure | Module name · Actor (optional) |
| Confirmation | Full summary · Generate / Cancel · Escape goes back |
Generated layout:
my-service/
├── manage.py
├── pyproject.toml # uv (PEP 621) or poetry
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Includes PostgreSQL when multitenant=True
├── .env.example
├── .gitignore
├── .pre-commit-config.yaml # Ruff hooks
├── ruff.toml
├── README.md
├── config/
│ ├── settings/
│ │ ├── base.py # Production — all settings
│ │ └── dev.py # Local — inherits base, DEBUG=True
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
└── apps/
└── <module>/
├── apps.py
├── migrations/
└── ... # DDD structure (see folderddd)
2. Inherit chassis settings
# config/settings/base.py
from pyms_django.settings.main import * # noqa: F401,F403
SERVICE_NAME = "ms-orders"
BASE_PATH = "/orders"
MULTITENANT = False
INSTALLED_APPS = [*INSTALLED_APPS, "apps.orders"] # noqa: F405
LOCAL_APPS: list[tuple[str, str]] = [
("apps.orders.usuario.api.v1.urls", BASE_PATH),
]
# config/settings/dev.py
from config.settings.base import * # noqa: F401,F403
DEBUG = True
ALLOWED_HOSTS = ["*"]
🖥️ CLI
pyms-django <command> [args]
Commands:
startproject <name> Generate a complete microservice
folderddd <module> [--actor] Add DDD structure to an existing project
folderddd
Generates (or extends) the DDD structure of a module. Can be called multiple times to add new actors without touching existing ones.
# No actor — all layers directly under apps/{module}/
pyms-django folderddd orders
# With actor — each actor gets its own full stack
pyms-django folderddd orders --actor user
pyms-django folderddd orders --actor manager
pyms-django folderddd orders --actor internal
# Special actor — no api/ layer
pyms-django folderddd orders --actor shared
Generated structure:
apps/
└── orders/ # Django app (name="apps.orders")
├── __init__.py
├── apps.py # OrdersConfig, label="orders"
├── migrations/
│
├── user/ # actor with full DDD stack
│ ├── api/v1/
│ │ ├── serializers.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── application/
│ │ ├── services/
│ │ │ ├── dtos.py
│ │ │ └── orders_service.py
│ │ └── use_cases/
│ │ ├── dtos.py
│ │ └── orders_use_case.py
│ ├── domain/
│ │ ├── aggregates.py
│ │ ├── entities.py
│ │ ├── value_objects.py
│ │ └── repositories.py
│ └── infrastructure/
│ ├── models.py # extends BaseModel
│ ├── services/
│ └── repositories/
│
└── shared/ # no api/ — code shared between actors
├── application/
├── domain/
└── infrastructure/
└── models.py
[!TIP]
sharedis a reserved actor name. You can callfolderdddas many times as needed to add actors without touching existing ones.
Register the app in settings:
# config/settings/base.py
INSTALLED_APPS = [*INSTALLED_APPS, "apps.orders"] # noqa: F405
LOCAL_APPS: list[tuple[str, str]] = [
("apps.orders.user.api.v1.urls", BASE_PATH),
("apps.orders.manager.api.v1.urls", f"{BASE_PATH}/manager"),
]
🧱 BaseModel
All generated models extend BaseModel:
from pyms_django.models import BaseModel
from django.db import models
class Order(BaseModel):
# ── Inherited fields ─────────────────────────────────
# id → UUIDField auto-generated, non-editable
# created_at → DateTimeField auto_now_add
# updated_at → DateTimeField auto_now
# deleted_at → DateTimeField null (soft-delete marker)
# active → BooleanField True by default
#
# objects → active records only
# all_objects → no filter applied
name = models.CharField(max_length=255)
class Meta:
active_signals_bulk_operations = True # emit post_save on bulk ops
| Method | Behaviour |
|---|---|
instance.delete() |
Soft-delete: active=False + deleted_at=now() |
instance.restore() |
Undo soft-delete |
instance.hard_delete() |
Permanently remove from the database |
qs.hard_delete() |
Bulk permanent delete via queryset |
Model.bulk_create(objs) |
Mass insert, emits post_save if active_signals_bulk_operations=True |
Model.bulk_update(objs, fields) |
Mass update, emits post_save if active_signals_bulk_operations=True |
💥 Domain Exceptions
from pyms_django.exceptions import DomainException, TypeException, LogLevel, ErrorDetail
class UserNotFoundError(DomainException):
code = "user_not_found"
description = "The requested user does not exist"
type = TypeException.BUSINESS # → HTTP 400
log_level = LogLevel.WARNING
# Raise with field-level details
raise UserNotFoundError(
field="user_id",
details=[ErrorDetail(code="invalid_uuid", description="Not a valid UUID")],
)
TypeException |
HTTP status |
|---|---|
VALIDATION |
400 |
BUSINESS |
400 |
PERMISSION |
403 |
TECHNICAL |
500 (default) |
Standardised error response:
{
"messages": [
{
"type": "ERROR",
"code": "user_not_found",
"description": "",
"field": "user_id",
"details": [
{"code": "invalid_uuid", "description": "Not a valid UUID"}
]
}
],
"trace_id": "a1b2c3d4e5f6..."
}
The
descriptionfield is intentionally empty in HTTP responses for domain exceptions — details are only logged server-side.
📡 Observability
Structured JSON logging
Every log line is enriched with service metadata and the current trace context:
{
"message": "REQUEST",
"timestamp": "2026-03-04T10:30:45.123456+00:00",
"severity": "INFO",
"service": "ms-orders",
"version": "1.2.0",
"trace": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"span": "x1y2z3a4b5c6d7e8",
"url": "http://localhost:8000/orders/",
"method": "POST"
}
Fields trace and span are populated from the active OpenTelemetry span when the monitoring extra is installed, or from B3 headers extracted by TracingMiddleware otherwise — so trace IDs always appear in logs even without an OTel SDK.
Masking sensitive payloads
# config/settings/base.py
DISABLED_PAYLOAD_LOGGING = {
"/api/login/": ["password", "token"],
"/api/users/": ["email"],
}
OpenTelemetry tracing
Install the monitoring extra and point the collector URL:
pip install "pyms-django-chassis[monitoring]"
# .env
METRICS_COLLECTOR_URL=http://otel-collector:4318
The TracingMiddleware creates a SERVER span for every request and propagates B3 headers to downstream calls automatically.
⚡ Built-in Endpoints
All microservices expose these routes without any additional configuration:
| Route | Description | Requires extra |
|---|---|---|
GET /{BASE_PATH}/health-check/ |
Liveness probe | — |
GET /{BASE_PATH}/version/ |
Artifact version (read from pyproject.toml) |
— |
GET /{BASE_PATH}/dependencies/ |
Dependency tree | — |
GET /{BASE_PATH}/schema/ |
OpenAPI schema | docs |
GET /{BASE_PATH}/ |
Swagger UI | docs |
GET /{BASE_PATH}/redoc/ |
ReDoc | docs |
🏢 Multi-tenancy
Enable schema-based multi-tenancy (PostgreSQL only):
# config/settings/base.py
from pyms_django.settings.main import * # noqa: F401,F403
MULTITENANT = True
TENANT_APPS: list[str] = [
"apps.orders",
]
When MULTITENANT = True the chassis automatically:
- Sets
DATABASES["default"]["ENGINE"]todjango_tenants.postgresql_backend - Prepends
TenantMainMiddlewareas the first middleware - Rebuilds
INSTALLED_APPSwith the requireddjango_tenantsordering - Adds
TenantSyncRoutertoDATABASE_ROUTERS
[!IMPORTANT] The
tenantextra must be installed:pip install "pyms-django-chassis[tenant]"
🔀 Read Replicas
# config/settings/base.py
ACTIVE_DATABASE_READ = True
DATABASES = {
"default": { # write
"ENGINE": "django.db.backends.postgresql",
"HOST": "primary-db.example.com",
...
},
"read_db": { # read replica
"ENGINE": "django.db.backends.postgresql",
"HOST": "replica-db.example.com",
...
},
}
from pyms_django.db.utils import get_read_db_alias
queryset = Order.objects.using(get_read_db_alias()).filter(active=True)
📄 OpenAPI Components
Reusable OAS definitions to keep schema annotations DRY:
from drf_spectacular.utils import extend_schema, OpenApiParameter
from pyms_django.oas.parameters import HEADER_USER_ID_PARAM
from pyms_django.oas.responses import BAD_REQUEST_RESPONSE, INTERNAL_SERVER_ERROR_RESPONSE
@extend_schema(
parameters=[OpenApiParameter(**HEADER_USER_ID_PARAM)],
responses={
400: BAD_REQUEST_RESPONSE,
500: INTERNAL_SERVER_ERROR_RESPONSE,
},
)
def my_view(request):
...
⚙️ Settings Reference
All variables below can be overridden in your config/settings/base.py after the star import.
| Variable | Default | Description |
|---|---|---|
SERVICE_NAME |
"to-be-defined" |
Service identifier — used in logs |
BASE_PATH |
"" |
URL prefix for all routes |
MULTITENANT |
False |
Enable schema-based multi-tenancy |
ADMIN_ENABLED |
False |
Mount Django admin at {BASE_PATH}/admin/ |
LOCAL_APPS |
[] |
List of (urls_module, prefix) tuples to register |
TENANT_APPS |
[] |
Apps isolated per tenant (multitenant only) |
HEADER_USER_ID |
"User-Id" |
Header name for the authenticated user UUID |
HEADER_APP_ID |
"App-Id" |
Header name for the calling application ID |
ACTIVE_DATABASE_READ |
False |
Route read queries to read_db |
DISABLED_PAYLOAD_LOGGING |
{} |
Map of path → fields to mask in request logs |
API_VERSION |
"v1" |
Default API version prefix for the chassis router |
Environment variables consumed directly:
| Variable | Used for |
|---|---|
DJANGO_SECRET_KEY |
SECRET_KEY |
DJANGO_DEBUG |
DEBUG (true/false) |
DJANGO_ALLOWED_HOSTS |
ALLOWED_HOSTS (comma-separated) |
DATABASE_ENGINE |
DB engine (default sqlite3) |
DATABASE_NAME |
DB name |
DATABASE_USER |
DB user |
DATABASE_PASSWORD |
DB password |
DATABASE_HOST |
DB host |
DATABASE_PORT |
DB port |
LOG_LEVEL |
Root log level (default INFO) |
METRICS_COLLECTOR_URL |
OTLP collector endpoint |
🐍 Django Compatibility
| Django | Support |
|---|---|
| 4.2 LTS | ✓ |
| 5.0 | ✓ |
| 5.1 | ✓ |
| 5.2 LTS | ✓ |
| 6.0 | ✓ |
The chassis uses STORAGES (available since Django 4.2) and avoids deprecated APIs, making it forward-compatible across all supported versions. The CLI lets you choose the exact Django version when generating a new microservice.
📋 License
MIT © PyMS Contributors
Metadata
Release files for pyms-django-chassis 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyms_django_chassis-1.0.0.tar.gz | 134.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyms_django_chassis-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 200.5 kB
Release files / pyms_django_chassis-1.0.0.tar.gz
| Download URL | pyms_django_chassis-1.0.0.tar.gz |
|---|---|
| Size | 134.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
68d949e957a1c9fb4d463b5a42a3be405a9c69086cef01cc161f124625177651
|
|
BLAKE2b-256 checksum How to use checksums |
bb3a022fc03cfceb2a43e4901200d7b6ebacaa222776af541319881bdf905703
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pyms_django_chassis-1.0.0-py3-none-any.whl
| Download URL | pyms_django_chassis-1.0.0-py3-none-any.whl |
|---|---|
| Size | 66.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f1c8c9d70ad65d29001ed93a0c7f1546f14ec05533200c2992888c2a881c86c9
|
|
BLAKE2b-256 checksum How to use checksums |
f08c19e4940eec0353e6c1f9b21afe215b62250ad9e7156e56c51d6c438e4963
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|