Skip to main content

Open-source Django chassis for building microservices with cross-cutting concerns out of the box

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyms_django_chassis-1.0.0.tar.gz (134.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyms_django_chassis-1.0.0-py3-none-any.whl (66.3 kB view details)

Uploaded Python 3

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

Hashes for pyms_django_chassis-1.0.0.tar.gz
Algorithm Hash digest
SHA256 68d949e957a1c9fb4d463b5a42a3be405a9c69086cef01cc161f124625177651
MD5 b3ab24547aeba56e41dca29a6346a6de
BLAKE2b-256 bb3a022fc03cfceb2a43e4901200d7b6ebacaa222776af541319881bdf905703

See more details on using hashes here.

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

Hashes for pyms_django_chassis-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f1c8c9d70ad65d29001ed93a0c7f1546f14ec05533200c2992888c2a881c86c9
MD5 acea68a674b4c328bf435ceb27085e0f
BLAKE2b-256 f08c19e4940eec0353e6c1f9b21afe215b62250ad9e7156e56c51d6c438e4963

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page