Skip to Content
ConfigurationDjango Settings

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 self

Environment 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 break

Recognized 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_dir

Settings 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_settings

Example 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.

ArgumentTypeDescription
pathstr | NoneRelative path as stored in a model field, e.g. "avatars/user_42.jpg". Returns fallback if empty/None.
fallbackstr | NoneValue 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.

ArgumentTypeDescription
pathstrURL 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.enabled

Usage 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

Modules

Integrations

Last updated on