DjangoConfig Base Class
The DjangoConfig base class is the foundation of Django-CFG’s type-safe configuration system.
Overview
DjangoConfig extends Pydantic’s BaseModel to provide:
- ✅ Type-safe configuration with Pydantic v2 validation
- ✅ Environment detection (development, production, test)
- ✅ Smart defaults based on environment
- ✅ Django settings generation from configuration models
- ✅ Path resolution for project structure
- ✅ Security enforcement in production mode
Complete Class Definition
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
from typing import Dict, List, Any, Optional
from pathlib import Path
class DjangoConfig(BaseModel):
"""
Base configuration class for Django projects.
This is a pure data model - all business logic is in separate classes:
- Apps list generation → InstalledAppsBuilder
- Middleware generation → MiddlewareBuilder
- Security settings → SecurityBuilder
- Settings generation → SettingsGenerator
Key Features:
- 100% type safety through Pydantic v2
- Environment-aware smart defaults
- Comprehensive validation
- Zero raw dictionary usage
"""
model_config = {
"validate_assignment": True,
"extra": "forbid", # Forbid arbitrary fields for type safety
"env_prefix": "DJANGO_",
"populate_by_name": True,
"validate_default": True,
"str_strip_whitespace": True,
}
# ══════════════════════════════════════════════════════════════
# PROJECT INFORMATION
# ══════════════════════════════════════════════════════════════
project_name: str = Field(
...,
description="Human-readable project name",
min_length=1,
max_length=100,
)
project_version: str = Field(
default="1.0.0",
description="Project version (semver format)",
pattern=r"^\d+\.\d+\.\d+.*$",
)
project_description: str = Field(
default="",
description="Project description",
max_length=500,
)
project_logo: str = Field(
default="",
description="Project logo URL",
)
# ══════════════════════════════════════════════════════════════
# ENVIRONMENT & DEBUG
# ══════════════════════════════════════════════════════════════
env_mode: EnvironmentMode = Field(
default=EnvironmentMode.PRODUCTION,
description="Environment mode: development, production, or test",
)
debug: bool = Field(
default=False,
description="Django DEBUG setting",
)
debug_warnings: bool = Field(
default=False,
description="Enable detailed warnings traceback for debugging",
)
# ══════════════════════════════════════════════════════════════
# SECURITY
# ══════════════════════════════════════════════════════════════
secret_key: str = Field(
...,
description="Django SECRET_KEY (min 50 chars)",
min_length=50,
repr=False, # Don't show in repr for security
)
security_domains: List[str] = Field(
default_factory=lambda: ["localhost", "127.0.0.1"],
description="Domains for ALLOWED_HOSTS, CORS, CSRF (auto-configured)",
)
ssl_redirect: Optional[bool] = Field(
default=None,
description="Force SSL redirect. None = disabled (proxy handles SSL)",
)
cors_allow_headers: List[str] = Field(
default_factory=lambda: [
"accept", "accept-encoding", "authorization", "content-type",
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
"x-api-key", "x-api-token", "x-chunk-index", "x-chunk-checksum",
"x-is-last", "x-total-chunks",
],
description="CORS allowed headers",
)
axes: Optional[AxesConfig] = Field(
default=None,
description="Django-Axes brute-force protection (None = smart defaults)",
)
crypto_fields: Optional[CryptoFieldsConfig] = Field(
default=None,
description="Django Crypto Fields encryption for sensitive data",
)
encryption: Optional[EncryptionConfig] = Field(
default=None,
description="API response encryption to prevent data scraping",
)
# ══════════════════════════════════════════════════════════════
# URLS
# ══════════════════════════════════════════════════════════════
site_url: str = Field(
default="http://localhost:3000",
description="Frontend site URL",
)
api_url: str = Field(
default="http://localhost:8000",
description="Backend API URL",
)
media_url: str = Field(
default="/media/",
description="Media URL. Use '__auto__' to derive from api_url",
)
root_urlconf: Optional[str] = Field(
default=None,
description="Django ROOT_URLCONF (e.g., 'api.urls')",
)
wsgi_application: Optional[str] = Field(
default=None,
description="Django WSGI_APPLICATION (e.g., 'api.wsgi.application')",
)
# ══════════════════════════════════════════════════════════════
# DATABASE
# ══════════════════════════════════════════════════════════════
databases: Dict[str, DatabaseConfig] = Field(
default_factory=dict,
description="Database connections. 'default' is required",
)
enable_pool_cleanup: bool = Field(
default=False,
description="Enable explicit connection pool cleanup middleware",
)
# ══════════════════════════════════════════════════════════════
# CACHE
# ══════════════════════════════════════════════════════════════
redis_url: Optional[str] = Field(
default=None,
description="Redis URL (redis://host:port/db). Auto-creates cache backend",
)
cache_default: Optional[CacheConfig] = Field(
default=None,
description="Default cache backend (auto-created from redis_url if not set)",
)
cache_sessions: Optional[CacheConfig] = Field(
default=None,
description="Sessions cache backend",
)
# ══════════════════════════════════════════════════════════════
# APPLICATIONS
# ══════════════════════════════════════════════════════════════
project_apps: List[str] = Field(
default_factory=list,
description="Project-specific Django apps",
)
custom_middleware: List[str] = Field(
default_factory=list,
description="Custom middleware classes",
)
# ══════════════════════════════════════════════════════════════
# SERVICES
# ══════════════════════════════════════════════════════════════
email: Optional[EmailConfig] = Field(
default=None,
description="Email service (SMTP/console backend)",
)
telegram: Optional[TelegramConfig] = Field(
default=None,
description="Telegram bot notifications",
)
admin_emails: List[str] = Field(
default_factory=list,
description="Admin email addresses for notifications (email + telegram)",
)
# ══════════════════════════════════════════════════════════════
# AUTHENTICATION & OAUTH
# ══════════════════════════════════════════════════════════════
extra_authentication_classes: List[str] = Field(
default_factory=list,
description="Extra DRF authentication classes prepended before defaults",
)
github_oauth: Optional[GitHubOAuthConfig] = Field(
default=None,
description="GitHub OAuth for social authentication",
)
two_factor: Optional[TwoFactorConfig] = Field(
default=None,
description="Two-Factor Authentication (TOTP) configuration",
)
# ══════════════════════════════════════════════════════════════
# ADMIN INTERFACE
# ══════════════════════════════════════════════════════════════
unfold: Optional[UnfoldConfig] = Field(
default=None,
description="Unfold admin interface configuration",
)
admin_timezone: Optional[str] = Field(
default=None,
description="Admin timezone (e.g., 'Asia/Seoul'). None = auto-detect",
)
# ══════════════════════════════════════════════════════════════
# GEOGRAPHIC & TIMEZONE
# ══════════════════════════════════════════════════════════════
geo: Optional[GeoConfig] = Field(
default=None,
description="Geographic and timezone configuration",
)
# ══════════════════════════════════════════════════════════════
# API
# ══════════════════════════════════════════════════════════════
drf: Optional[DRFConfig] = Field(
default=None,
description="Django REST Framework configuration",
)
spectacular: Optional[SpectacularConfig] = Field(
default=None,
description="DRF Spectacular OpenAPI configuration",
)
grpc: Optional[GRPCConfig] = Field(
default=None,
description="gRPC server configuration",
)
grpc_module: Optional[DjangoGrpcModuleConfig] = Field(
default=None,
description="D1-backed gRPC module configuration",
)
api_keys: Optional[ApiKeys] = Field(
default=None,
description="External API keys (OpenAI, OpenRouter, etc.)",
)
limits: Optional[LimitsConfig] = Field(
default=None,
description="Application limits (file uploads, requests, etc.)",
)
# ══════════════════════════════════════════════════════════════
# BACKGROUND TASKS
# ══════════════════════════════════════════════════════════════
django_rq: Optional[DjangoRQConfig] = Field(
default=None,
description="Django-RQ task queue and scheduler",
)
# ══════════════════════════════════════════════════════════════
# CURRENCY & MONEY
# ══════════════════════════════════════════════════════════════
currency: Optional[CurrencyConfig] = Field(
default=None,
description="Currency conversion and exchange rates",
)
# ══════════════════════════════════════════════════════════════
# INTEGRATIONS
# ══════════════════════════════════════════════════════════════
centrifugo: Optional[DjangoCfgCentrifugoConfig] = Field(
default=None,
description="Centrifugo WebSocket pub/sub",
)
ngrok: Optional[NgrokConfig] = Field(
default=None,
description="Ngrok tunneling for development/webhooks",
)
streamlit_admin: Optional[StreamlitAdminConfig] = Field(
default=None,
description="Streamlit admin panel (auto-starts with Django)",
)
frontend_monitor: Optional[FrontendMonitorConfig] = Field(
default=None,
description="Frontend error monitoring configuration",
)
cloudflare: Optional[CloudflareConfig] = Field(
default=None,
description="Cloudflare D1 integration configuration",
)
# ══════════════════════════════════════════════════════════════
# STORAGE & FILES
# ══════════════════════════════════════════════════════════════
storage: Optional[StorageConfig] = Field(
default_factory=StorageConfig,
description="Automatic file cleanup for FileField/ImageField",
)
# ══════════════════════════════════════════════════════════════
# FRONTEND / UI
# ══════════════════════════════════════════════════════════════
tailwind_app_name: str = Field(
default="theme",
description="Tailwind theme app name",
min_length=1,
max_length=50,
)
tailwind_version: int = Field(
default=4,
description="Tailwind CSS version (3 or 4)",
ge=3,
le=4,
)
enable_drf_tailwind: bool = Field(
default=True,
description="Enable Tailwind CSS theme for DRF Browsable API",
)
# ══════════════════════════════════════════════════════════════
# DJANGO-CFG SETTINGS
# ══════════════════════════════════════════════════════════════
startup_info_mode: StartupInfoMode = Field(
default=StartupInfoMode.FULL,
description="Startup info: NONE, SHORT, or FULL",
)
show_ai_hints: bool = Field(
default=True,
description="Show AI development hints in startup output",
)
# ══════════════════════════════════════════════════════════════
# INTERNAL STATE
# ══════════════════════════════════════════════════════════════
_base_dir: Optional[Path] = PrivateAttr(default=None)
_django_settings: Optional[Dict[str, Any]] = PrivateAttr(default=None)
_service: Optional[Any] = PrivateAttr(default=None)Validators
DjangoConfig includes comprehensive validators to ensure configuration correctness:
Field Validators
@field_validator("project_name")
@classmethod
def validate_project_name(cls, v: str) -> str:
"""Validate project name format."""
if not v.replace(" ", "").replace("-", "").replace("_", "").isalnum():
raise ValueError(
"Project name must contain only alphanumeric characters, "
"spaces, hyphens, and underscores"
)
return v
@field_validator("secret_key")
@classmethod
def validate_secret_key(cls, v: str) -> str:
"""Validate Django SECRET_KEY - minimum 50 characters."""
if len(v) < 50:
raise ValueError("SECRET_KEY must be at least 50 characters long")
return v
@field_validator("project_apps")
@classmethod
def validate_project_apps(cls, v: List[str]) -> List[str]:
"""Validate project apps list format."""
for app in v:
if not app or not app.replace(".", "").replace("_", "").isalnum():
raise ValueError(f"Invalid app name '{app}'")
return v
@field_validator("media_url", mode="before")
@classmethod
def validate_media_url(cls, v: str, info) -> str:
"""Handle '__auto__' directive for media_url."""
if v == "__auto__":
api_url = info.data.get("api_url", "http://localhost:8000")
return f"{api_url.rstrip('/')}/media/"
return v if v.endswith("/") else f"{v}/"Model Validators
@model_validator(mode="after")
def validate_configuration_consistency(self) -> "DjangoConfig":
"""Validate overall configuration consistency."""
# In development mode, force media_url to use api_url
if self.is_development:
dev_media_url = f"{self.api_url.rstrip('/')}/media/"
if self.media_url != dev_media_url:
object.__setattr__(self, 'media_url', dev_media_url)
# Ensure 'default' database exists
if not self.databases:
raise ConfigurationError("At least one database must be configured")
if "default" not in self.databases:
raise ConfigurationError("'default' database is required")
# Validate database routing consistency
for alias, db_config in self.databases.items():
if db_config.migrate_to and db_config.migrate_to not in self.databases:
raise ConfigurationError(
f"Database '{alias}' references non-existent database '{db_config.migrate_to}'"
)
return self
@model_validator(mode="after")
def enforce_production_security(self) -> "DjangoConfig":
"""
SECURITY: Production mode ALWAYS disables debug.
If DEBUG=True in production, a warning is issued and DEBUG
is forcefully set to False to prevent:
- Sensitive data exposure in tracebacks
- Memory leaks from SQL query logging
- Performance degradation
"""
if self.is_production and self.debug:
import warnings
warnings.warn(
"SECURITY: DEBUG=True is not allowed in production! "
"Forcing DEBUG=False.",
UserWarning
)
object.__setattr__(self, 'debug', False)
return selfEnvironment Detection
Environment is auto-detected from environment variables during initialization:
def model_post_init(self, _context: Any) -> None:
"""Initialize configuration after Pydantic validation."""
# Auto-detect environment from env variables
env_vars = ['DJANGO_ENV', 'ENVIRONMENT', 'ENV']
for env_var in env_vars:
env_value = os.environ.get(env_var)
if env_value:
env_normalized = env_value.lower().strip()
if env_normalized in ('dev', 'devel', 'develop', 'development', 'local'):
self.env_mode = EnvironmentMode.DEVELOPMENT
elif env_normalized in ('prod', 'production'):
self.env_mode = EnvironmentMode.PRODUCTION
elif env_normalized in ('test', 'testing'):
self.env_mode = EnvironmentMode.TEST
breakRecognized environment values:
- Development:
dev,devel,develop,development,local - Production:
prod,production - Test:
test,testing
Properties
DjangoConfig provides convenient properties for checking the current environment:
@property
def is_development(self) -> bool:
"""Check if running in development mode."""
return self.env_mode == EnvironmentMode.DEVELOPMENT
@property
def is_production(self) -> bool:
"""Check if running in production mode."""
return self.env_mode == EnvironmentMode.PRODUCTION
@property
def is_test(self) -> bool:
"""Check if running in test mode."""
return self.env_mode == EnvironmentMode.TEST
@property
def base_dir(self) -> Path:
"""
Get the base directory of the project.
Auto-detected by searching for manage.py in parent directories.
"""
if self._base_dir is None:
current_path = Path.cwd().resolve()
for path in [current_path] + list(current_path.parents):
if (path / "manage.py").exists():
self._base_dir = path
break
if self._base_dir is None:
self._base_dir = current_path
return self._base_dirSettings Generation Methods
get_all_settings
def get_all_settings(self) -> Dict[str, Any]:
"""Generate complete Django settings dictionary."""
from django_cfg.core.state.registry import set_current_config
set_current_config(self)
if self._django_settings is None:
from django_cfg.core.generation import SettingsGenerator
self._django_settings = SettingsGenerator.generate(self)
return self._django_settingsExample usage:
# config.py
from django_cfg import DjangoConfig, DatabaseConfig
class MyConfig(DjangoConfig):
project_name = "My Project"
secret_key = "your-50-character-minimum-secret-key-here-for-security"
databases = {
"default": DatabaseConfig(
engine="django.db.backends.postgresql",
name="mydb",
)
}
config = MyConfig()
# settings.py
from config import config
globals().update(config.get_all_settings())get_installed_apps
def get_installed_apps(self) -> List[str]:
"""Get complete INSTALLED_APPS list."""
return self.service.get_installed_apps()The method delegates to ConfigService which builds the apps list from:
- Django core apps
- WhiteNoise, Unfold Admin
- REST Framework, JWT, CORS
- Security (Axes), Import/Export
- Django-CFG core modules
- Your
project_apps
get_middleware
def get_middleware(self) -> List[str]:
"""Get complete MIDDLEWARE list."""
return self.service.get_middleware()The method delegates to ConfigService which includes:
- Security, WhiteNoise, CORS
- Sessions, Authentication, CSRF
- Messages, ClickJacking protection
- Django-Axes (brute-force protection)
- Your
custom_middleware
get_allowed_hosts
def get_allowed_hosts(self) -> List[str]:
"""Get ALLOWED_HOSTS."""
return self.service.get_allowed_hosts()Auto-generated from security_domains, including localhost variants for development.
build_media_url
def build_media_url(
self,
path: Optional[str],
fallback: Optional[str] = None,
) -> Optional[str]:Builds an absolute URL for a media file from its storage path, handling both
relative and CDN-absolute media_url values automatically.
| Argument | Type | Description |
|---|---|---|
path | str | None | Relative path as stored in a model field, e.g. "avatars/user_42.jpg". Returns fallback if empty/None. |
fallback | str | None | Value returned when path is empty (default None). |
# media_url = "/media/" (relative — prepends api_url)
config.build_media_url("avatars/user_42.jpg")
# → "https://api.example.com/media/avatars/user_42.jpg"
# media_url = "https://cdn.example.com/media/" (absolute CDN)
config.build_media_url("avatars/user_42.jpg")
# → "https://cdn.example.com/media/avatars/user_42.jpg"
# Empty path → fallback
config.build_media_url(None, fallback="/static/no-avatar.png")
# → "/static/no-avatar.png"Standalone helpers (when the config isn’t directly accessible):
from django_cfg import get_media_url, build_media_url
# Auto-reads current config — use anywhere after Django is set up:
avatar_url = get_media_url(user.avatar)
logo_url = get_media_url(org.logo, fallback="/static/default-logo.png")
# Explicit — useful in tests or code running before Django starts:
url = build_media_url(
"logos/company.png",
api_url="https://api.example.com",
media_url="/media/",
)build_api_url
def build_api_url(self, path: str) -> str:Builds an absolute URL relative to api_url. Use this for API proxy URLs — photo endpoints, download routes, webhooks — anything that goes through the backend API rather than the media storage.
| Argument | Type | Description |
|---|---|---|
path | str | URL path segment, e.g. "apix/catalog_photos/photo/abc.jpg". Leading slash is stripped automatically. |
config.build_api_url("apix/catalog_photos/photo/abc.jpg")
# → "https://api.example.com/apix/catalog_photos/photo/abc.jpg"
# Typical usage in a service:
prefix = config.openapi_client.api_prefix # e.g. "apix"
url = config.build_api_url(f"{prefix}/catalog_photos/photo/{photo_id}.jpg")
# → "https://api.example.com/apix/catalog_photos/photo/<uuid>.jpg"Standalone helpers:
from django_cfg import get_api_url, build_api_url
# Auto-reads current config:
url = get_api_url("apix/catalog_photos/photo/abc.jpg")
# → "https://api.example.com/apix/catalog_photos/photo/abc.jpg"
# Explicit:
url = build_api_url("apix/photo/abc.jpg", api_url="https://api.example.com")build_api_url vs build_media_url — use build_media_url for files served from Django’s MEDIA_ROOT (user uploads, avatars). Use build_api_url for URLs that go through an API view or proxy (photo watermarking endpoint, download routes, etc.).
Other Methods
def invalidate_cache(self) -> None:
"""Invalidate cached Django settings."""
self._django_settings = None
def should_enable_rq(self) -> bool:
"""Determine if Django-RQ should be enabled."""
return hasattr(self, 'django_rq') and self.django_rq and self.django_rq.enabledUsage Examples
Basic Configuration
# config.py
from django_cfg import DjangoConfig, DatabaseConfig
from django_cfg.core.types.enums import EnvironmentMode
class MyConfig(DjangoConfig):
project_name: str = "My Project"
secret_key: str = "your-secret-key-here-minimum-50-characters-long-string"
env_mode = EnvironmentMode.DEVELOPMENT
debug: bool = True
project_apps: list = ["apps.blog", "apps.shop"]
databases = {
"default": DatabaseConfig(
engine="django.db.backends.postgresql",
name="mydb",
user="postgres",
password="password",
host="localhost",
port="5432",
)
}With Environment Properties
from django_cfg import DjangoConfig, DatabaseConfig
class MyConfig(DjangoConfig):
project_name: str = "My Project"
secret_key: str = "your-secret-key-here-minimum-50-characters-long-string"
databases = {
"default": DatabaseConfig(
engine="django.db.backends.postgresql",
name="mydb",
)
}
# Usage - environment is auto-detected from DJANGO_ENV, ENVIRONMENT, or ENV
config = MyConfig()
if config.is_development:
print("Running in development mode")
elif config.is_production:
print("Running in production mode")
elif config.is_test:
print("Running in test mode")With Services
from django_cfg import DjangoConfig, DatabaseConfig, EmailConfig, TelegramConfig
class MyConfig(DjangoConfig):
project_name: str = "My Project"
secret_key: str = "your-secret-key-here-minimum-50-characters-long-string"
databases = {
"default": DatabaseConfig(
engine="django.db.backends.postgresql",
name="mydb",
)
}
# Email service
email = EmailConfig(
backend="smtp",
host="smtp.gmail.com",
port=587,
username="[email protected]",
password="your-app-password",
)
# Telegram notifications
telegram = TelegramConfig(
bot_token="your-bot-token",
chat_id="your-chat-id",
)
# Admin notifications
admin_emails = ["[email protected]"]
# Redis cache (auto-creates cache backend)
redis_url = "redis://localhost:6379/0"With Integrations
from django_cfg import DjangoConfig, DatabaseConfig, DRFConfig, UnfoldConfig
class MyConfig(DjangoConfig):
project_name: str = "My Project"
secret_key: str = "your-secret-key-here-minimum-50-characters-long-string"
databases = {
"default": DatabaseConfig(
engine="django.db.backends.postgresql",
name="mydb",
)
}
# Django REST Framework
drf = DRFConfig(
page_size=20,
max_page_size=100,
)
# Unfold admin interface
unfold = UnfoldConfig(
site_title="My Admin",
)Model Configuration
The model_config dict controls Pydantic behavior:
model_config = {
"validate_assignment": True, # Validate on attribute assignment
"extra": "forbid", # Forbid extra fields for type safety
"env_prefix": "DJANGO_", # Environment variable prefix
"populate_by_name": True, # Allow field name or alias
"validate_default": True, # Validate default values
"str_strip_whitespace": True, # Strip whitespace from strings
}Environment Variables
With env_prefix="DJANGO_", you can override fields via environment variables:
export DJANGO_SECRET_KEY="your-production-secret-key-at-least-50-chars"
export DJANGO_DEBUG=false
export DJANGO_PROJECT_NAME="My App"Type Safety
With extra="forbid", unknown fields raise validation errors:
# ❌ This will raise a validation error
config = MyConfig(
project_name="My Project",
secret_key="...",
unknown_field="value" # ValidationError: extra fields not permitted
)
# ✅ Only defined fields are allowed
config = MyConfig(
project_name="My Project",
secret_key="your-50-character-minimum-secret-key-here-for-security",
databases={"default": DatabaseConfig(engine="...", name="mydb")},
)See Also
Core Configuration
- Configuration Overview - Configuration system overview
- Security Settings - Security configuration
- Database Models - Database configuration
- Cache Models - Cache configuration
- Email Models - Email configuration
Modules
- File Cleanup - Automatic file cleanup for FileField/ImageField
- Currency - Currency conversion and exchange rates
- Telegram - Telegram bot notifications
- Unfold Admin - Modern admin interface
Integrations
- Django-RQ - Background task queue
- Centrifugo - WebSocket pub/sub
- gRPC - gRPC server configuration
- Ngrok - Development tunneling
- OAuth - Social authentication