Skip to main content
typing animation

Python Django DRF License PyPI

Open-source Django chassis for building production-ready microservices.
Cross-cutting concerns out of the box — trace, log, observe, scale.


🛠️ Tech Stack

Tech Stack

OpenTelemetry Pydantic Textual uv Poetry Ruff


✨ Features

🏛️ BaseModel UUID PK · timestamps · soft-delete · restore() · hard_delete() · bulk ops with optional signals

🏗️ DDD Scaffold CLI Layers by actor: api/v1/ · application/ · domain/ · infrastructure/

📡 Observability Structured JSON logging · OpenTelemetry tracing · B3 propagation · OTLP export · trace context vars

🏢 Multi-tenancy Schema-based tenant isolation via django-tenants (PostgreSQL)

🔐 Secret Management AWS Secrets Manager integration with automatic config loading

📚 API Docs Auto-generated Swagger / ReDoc via drf-spectacular

🔀 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] tui is not included in all. 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] shared is a reserved actor name. You can call folderddd as 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 description field 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"] to django_tenants.postgresql_backend
  • Prepends TenantMainMiddleware as the first middleware
  • Rebuilds INSTALLED_APPS with the required django_tenants ordering
  • Adds TenantSyncRouter to DATABASE_ROUTERS

[!IMPORTANT] The tenant extra 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)

Source distribution for pyms-django-chassis 1.0.0
File Size Uploaded
pyms_django_chassis-1.0.0.tar.gz 134.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyms-django-chassis 1.0.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page