Open-source Django chassis for building microservices with cross-cutting concerns out of the box
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyms_django_chassis-1.0.0.tar.gz.
File metadata
- Download URL: pyms_django_chassis-1.0.0.tar.gz
- Upload date:
- Size: 134.2 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68d949e957a1c9fb4d463b5a42a3be405a9c69086cef01cc161f124625177651
|
|
| MD5 |
b3ab24547aeba56e41dca29a6346a6de
|
|
| BLAKE2b-256 |
bb3a022fc03cfceb2a43e4901200d7b6ebacaa222776af541319881bdf905703
|
File details
Details for the file pyms_django_chassis-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pyms_django_chassis-1.0.0-py3-none-any.whl
- Upload date:
- Size: 66.3 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1c8c9d70ad65d29001ed93a0c7f1546f14ec05533200c2992888c2a881c86c9
|
|
| MD5 |
acea68a674b4c328bf435ceb27085e0f
|
|
| BLAKE2b-256 |
f08c19e4940eec0353e6c1f9b21afe215b62250ad9e7156e56c51d6c438e4963
|