| """ |
| Django settings for pyrunner project. |
| |
| Generated by 'django-admin startproject' using Django 6.0.1. |
| |
| For more information on this file, see |
| https://docs.djangoproject.com/en/6.0/topics/settings/ |
| |
| For the full list of settings and their values, see |
| https://docs.djangoproject.com/en/6.0/ref/settings/ |
| """ |
|
|
| import os |
| import shutil |
| from pathlib import Path |
|
|
| from django.core.exceptions import ImproperlyConfigured |
| from dotenv import load_dotenv |
|
|
| |
| BASE_DIR = Path(__file__).resolve().parent.parent |
|
|
| |
| load_dotenv(BASE_DIR / ".env") |
|
|
|
|
| |
| |
|
|
| |
| |
| SECRET_KEY = os.environ["SECRET_KEY"] |
|
|
| |
| |
| DEBUG = os.environ.get("DEBUG", "False").lower() == "true" |
|
|
| ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") |
|
|
|
|
| |
|
|
| INSTALLED_APPS = [ |
| "django.contrib.admin", |
| "django.contrib.auth", |
| "django.contrib.contenttypes", |
| "django.contrib.sessions", |
| "django.contrib.messages", |
| "django.contrib.staticfiles", |
| |
| "django_q", |
| "tailwind", |
| "theme", |
| "axes", |
| |
| "core", |
| ] |
|
|
| |
| TAILWIND_APP_NAME = "theme" |
| INTERNAL_IPS = ["127.0.0.1"] |
| NPM_BIN_PATH = os.environ.get("NPM_BIN_PATH", shutil.which("npm") or "npm") |
|
|
| MIDDLEWARE = [ |
| "django.middleware.security.SecurityMiddleware", |
| "whitenoise.middleware.WhiteNoiseMiddleware", |
| "pyrunner.middleware.NoCacheMiddleware", |
| "django.contrib.sessions.middleware.SessionMiddleware", |
| "django.middleware.common.CommonMiddleware", |
| "django.middleware.csrf.CsrfViewMiddleware", |
| "django.contrib.auth.middleware.AuthenticationMiddleware", |
| "axes.middleware.AxesMiddleware", |
| "django.contrib.messages.middleware.MessageMiddleware", |
| "django.middleware.clickjacking.XFrameOptionsMiddleware", |
| "core.middleware.SetupWizardMiddleware", |
| ] |
|
|
| ROOT_URLCONF = "pyrunner.urls" |
|
|
| TEMPLATES = [ |
| { |
| "BACKEND": "django.template.backends.django.DjangoTemplates", |
| "DIRS": [BASE_DIR / "templates"], |
| "APP_DIRS": True, |
| "OPTIONS": { |
| "context_processors": [ |
| "django.template.context_processors.request", |
| "django.contrib.auth.context_processors.auth", |
| "django.contrib.messages.context_processors.messages", |
| "core.context_processors.pyrunner_version", |
| ], |
| }, |
| }, |
| ] |
|
|
| WSGI_APPLICATION = "pyrunner.wsgi.application" |
|
|
|
|
| |
| |
|
|
| DATABASES = { |
| "default": { |
| "ENGINE": "django.db.backends.sqlite3", |
| "NAME": BASE_DIR / "data" / "db.sqlite3", |
| "OPTIONS": { |
| "timeout": 30, |
| }, |
| } |
| } |
|
|
|
|
| |
| def _enable_sqlite_wal(sender, connection, **kwargs): |
| if connection.vendor == "sqlite": |
| cursor = connection.cursor() |
| cursor.execute("PRAGMA journal_mode=WAL;") |
| cursor.execute("PRAGMA busy_timeout=30000;") |
|
|
|
|
| from django.db.backends.signals import connection_created |
|
|
| connection_created.connect(_enable_sqlite_wal) |
|
|
|
|
| |
| AUTH_USER_MODEL = "core.User" |
|
|
| |
| LOGIN_URL = "auth:login" |
| LOGIN_REDIRECT_URL = "cpanel:dashboard" |
|
|
|
|
| |
| |
|
|
| AUTH_PASSWORD_VALIDATORS = [ |
| { |
| "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", |
| }, |
| { |
| "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", |
| }, |
| { |
| "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", |
| }, |
| { |
| "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", |
| }, |
| ] |
|
|
| |
| AUTHENTICATION_BACKENDS = [ |
| "axes.backends.AxesStandaloneBackend", |
| "django.contrib.auth.backends.ModelBackend", |
| ] |
|
|
| |
| AXES_FAILURE_LIMIT = int(os.environ.get("AXES_FAILURE_LIMIT", "5")) |
| AXES_COOLOFF_TIME = 0.25 |
| AXES_RESET_ON_SUCCESS = True |
| AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]] |
| AXES_ENABLE_ACCESS_FAILURE_LOG = True |
| AXES_VERBOSE = False |
|
|
|
|
| |
| |
|
|
| LANGUAGE_CODE = "en-us" |
|
|
| TIME_ZONE = "UTC" |
|
|
| USE_I18N = True |
|
|
| USE_TZ = True |
|
|
|
|
| |
| |
|
|
| STATIC_URL = "static/" |
| STATICFILES_DIRS = [BASE_DIR / "static"] |
| STATIC_ROOT = BASE_DIR / "staticfiles" |
|
|
| |
| STORAGES = { |
| "default": { |
| "BACKEND": "django.core.files.storage.FileSystemStorage", |
| }, |
| "staticfiles": { |
| "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", |
| }, |
| } |
|
|
|
|
| |
| |
|
|
| DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" |
|
|
|
|
| |
| |
| EMAIL_BACKEND = os.environ.get( |
| "EMAIL_BACKEND", |
| "django.core.mail.backends.console.EmailBackend", |
| ) |
| DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@pyrunner.local") |
|
|
| |
| RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "") |
| USE_RESEND = os.environ.get("USE_RESEND", "False").lower() == "true" |
|
|
|
|
| |
| def _get_q_cluster_config(): |
| """ |
| Get Q_CLUSTER configuration from database with environment fallbacks. |
| |
| This is called at module load time. Database values take precedence |
| over environment variables when available. |
| """ |
| |
| config = { |
| "name": "PyRunner", |
| "workers": int(os.environ.get("Q_WORKERS", 2)), |
| "timeout": 600 if os.name != "nt" else 0, |
| "retry": 660, |
| "queue_limit": 20, |
| "bulk": 5, |
| "orm": "default", |
| "catch_up": False, |
| } |
|
|
| |
| try: |
| from django.apps import apps |
|
|
| if not apps.ready: |
| return config |
|
|
| from django.db import connection |
| from django.db.utils import OperationalError, ProgrammingError |
|
|
| with connection.cursor() as cursor: |
| cursor.execute( |
| "SELECT q_workers, q_timeout, q_retry, q_queue_limit " |
| "FROM global_settings WHERE id = 1" |
| ) |
| row = cursor.fetchone() |
| if row: |
| config["workers"] = row[0] or config["workers"] |
| |
| if os.name == "nt": |
| config["timeout"] = 0 |
| else: |
| config["timeout"] = row[1] if row[1] is not None else config["timeout"] |
| config["retry"] = row[2] or config["retry"] |
| config["queue_limit"] = row[3] or config["queue_limit"] |
| except (OperationalError, ProgrammingError, Exception): |
| |
| pass |
|
|
| return config |
|
|
|
|
| Q_CLUSTER = _get_q_cluster_config() |
|
|
|
|
| |
| DATA_DIR = BASE_DIR / "data" |
| ENVIRONMENTS_ROOT = DATA_DIR / "environments" |
| SCRIPTS_WORKDIR = DATA_DIR / "workdir" |
|
|
| |
| DATA_DIR.mkdir(exist_ok=True) |
| ENVIRONMENTS_ROOT.mkdir(exist_ok=True) |
| SCRIPTS_WORKDIR.mkdir(exist_ok=True) |
|
|
|
|
| |
| |
| ENCRYPTION_KEY = os.environ.get("ENCRYPTION_KEY", "") |
|
|
| |
| if not ENCRYPTION_KEY: |
| if not DEBUG: |
| raise ImproperlyConfigured( |
| "ENCRYPTION_KEY is required in production. " |
| "Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"" |
| ) |
| else: |
| |
| try: |
| from cryptography.fernet import Fernet |
| Fernet(ENCRYPTION_KEY.encode() if isinstance(ENCRYPTION_KEY, str) else ENCRYPTION_KEY) |
| except Exception: |
| raise ImproperlyConfigured("ENCRYPTION_KEY is invalid. Must be a valid Fernet key.") |
|
|
|
|
| |
| LOGS_DIR = DATA_DIR / "logs" |
| LOGS_DIR.mkdir(exist_ok=True) |
|
|
| LOGGING = { |
| "version": 1, |
| "disable_existing_loggers": False, |
| "formatters": { |
| "verbose": { |
| "format": "[{asctime}] {levelname} {name} {message}", |
| "style": "{", |
| "datefmt": "%Y-%m-%d %H:%M:%S", |
| }, |
| "json": { |
| "()": "core.logging.JsonFormatter", |
| }, |
| }, |
| "handlers": { |
| "console": { |
| "class": "logging.StreamHandler", |
| "formatter": "verbose", |
| }, |
| "file": { |
| "class": "logging.handlers.RotatingFileHandler", |
| "filename": LOGS_DIR / "pyrunner.log", |
| "maxBytes": 10 * 1024 * 1024, |
| "backupCount": 5, |
| "formatter": "json", |
| "encoding": "utf-8", |
| }, |
| }, |
| "root": { |
| "handlers": ["console", "file"], |
| "level": "INFO", |
| }, |
| "loggers": { |
| "django": { |
| "handlers": ["console", "file"], |
| "level": "WARNING", |
| "propagate": False, |
| }, |
| "core": { |
| "handlers": ["console", "file"], |
| "level": "INFO", |
| "propagate": False, |
| }, |
| }, |
| } |
|
|
|
|
| |
| |
| if not DEBUG: |
| |
| SECURE_SSL_REDIRECT = os.environ.get("SECURE_SSL_REDIRECT", "True").lower() == "true" |
| SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") |
|
|
| |
| |
| SECURE_HSTS_SECONDS = int(os.environ.get("SECURE_HSTS_SECONDS", "2592000")) |
| SECURE_HSTS_INCLUDE_SUBDOMAINS = os.environ.get("SECURE_HSTS_INCLUDE_SUBDOMAINS", "True").lower() == "true" |
| SECURE_HSTS_PRELOAD = os.environ.get("SECURE_HSTS_PRELOAD", "False").lower() == "true" |
|
|
| |
| SESSION_COOKIE_SECURE = True |
| CSRF_COOKIE_SECURE = True |
|
|
| |
| SECURE_CONTENT_TYPE_NOSNIFF = True |
| X_FRAME_OPTIONS = "DENY" |
|
|
| |
| |
| |
| |
|
|
|
|
| |
| |
| |
|
|
| |
| API_RATE_LIMIT = int(os.environ.get("API_RATE_LIMIT", "60")) |
|
|
| |
| |
| API_CORS_ORIGINS = os.environ.get("API_CORS_ORIGINS", "*") |
|
|