From 988d131c49f7173d6d30bee5fb4bf6f1bd449564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Sun, 22 Mar 2026 20:02:16 +0100 Subject: [PATCH] Add initial project structure and configuration files --- .env.example | 31 ++++++ .gitignore | 88 ++++++++++++---- .pre-commit-config.yaml | 51 +++++++++ .vscode/launch.json | 16 +++ README.md | 94 ++++++++++++++++- config/__init__.py | 0 config/settings.py | 205 ++++++++++++++++++++++++++++++++++++ config/urls.py | 6 ++ config/wsgi.py | 14 +++ manage.py | 24 +++++ pyproject.toml | 134 +++++++++++++++++++++++ tools/systemd/panso.service | 32 ++++++ tools/systemd/panso.socket | 11 ++ 13 files changed, 687 insertions(+), 19 deletions(-) create mode 100644 .env.example create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/launch.json create mode 100644 config/__init__.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100755 manage.py create mode 100644 pyproject.toml create mode 100644 tools/systemd/panso.service create mode 100644 tools/systemd/panso.socket diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5feb39b --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Django Configuration +# Set to False in production +DEBUG=True + +# Django Secret Key +# Generate a new secret key for production: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' +DJANGO_SECRET_KEY=your-secret-key-here + +# Email Configuration +# SMTP Host (examples below) +EMAIL_HOST=smtp.gmail.com + +# SMTP Port (common ports: 587 for TLS, 465 for SSL, 25 for unencrypted) +EMAIL_PORT=587 + +# Email credentials +EMAIL_HOST_USER=your-email@gmail.com +EMAIL_HOST_PASSWORD=your-app-password-here + +# Connection security +# Use TLS (True for most providers like Gmail, Outlook) +EMAIL_USE_TLS=True +# Use SSL (False for most providers, True for some older configurations) +EMAIL_USE_SSL=False + +# Connection timeout in seconds +EMAIL_TIMEOUT=10 + +# Redis Configuration +REDIS_URL_CACHE=unix:///var/run/redis/redis.sock?db=0 +REDIS_URL_CELERY=unix:///var/run/redis/redis.sock?db=1 diff --git a/.gitignore b/.gitignore index ab3e8ce..e2a8cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -28,8 +27,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -47,7 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ cover/ @@ -86,32 +85,45 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +poetry.lock +poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -119,11 +131,25 @@ __pypackages__/ celerybeat-schedule celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -156,9 +182,35 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..75d713e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,51 @@ +repos: + - repo: https://github.com/asottile/add-trailing-comma + rev: v4.0.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-toml + - id: check-vcs-permalinks + - id: end-of-file-fixer + - id: mixed-line-ending + - id: name-tests-test + args: [--pytest-test-first] + - id: trailing-whitespace + + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.30.0 + hooks: + - id: django-upgrade + args: [--target-version, "6.0"] + + - repo: https://github.com/Riverside-Healthcare/djLint + rev: v1.36.4 + hooks: + - id: djlint-reformat-django + - id: djlint-django + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.7 + hooks: + - id: ruff-check + args: ["--fix", "--exit-non-zero-on-fix"] + - id: ruff-format + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py311-plus"] + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.11 + hooks: + - id: actionlint diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a5e1158 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Django", + "type": "debugpy", + "request": "launch", + "args": [ + "runserver" + ], + "django": true, + "autoStartBrowser": false, + "program": "${workspaceFolder}/manage.py" + } + ] +} diff --git a/README.md b/README.md index 05ec850..6c8865c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,95 @@ # panso.se -Price tracker for Swedish stores \ No newline at end of file +Price tracker for Swedish stores + +# panso + +Get notified when a new drop is available on Twitch + +## TL;DR (Arch Linux + PostgreSQL) + +Install and initialize Postgres, then start the service: + +```bash +sudo pacman -S postgresql +sudo -u postgres initdb -D /var/lib/postgres/data +sudo systemctl enable --now postgresql +``` + +Create a local role and database: + +```bash +sudo -u postgres createuser -P panso +sudo -u postgres createdb -O panso panso +``` + +Point Django at the unix socket used by Arch (`/run/postgresql`): + +```bash +POSTGRES_USER=panso +POSTGRES_PASSWORD=your_password +POSTGRES_DB=panso +POSTGRES_HOST=/run/postgresql +POSTGRES_PORT=5432 +``` + +### Linux (Systemd) + +```bash +sudo useradd --create-home --home-dir /home/panso --shell /bin/fish panso +sudo passwd panso +# sudo usermod -aG wheel panso +su - panso +git clone https://git.lovinator.space/TheLovinator/panso.se.git +cd panso +uv sync --no-dev + +# Modify .env with the correct database credentials and other settings, then run migrations: +uv run python manage.py migrate +``` + +Install the systemd service from the repo: + +```bash +sudo install -m 0644 tools/systemd/panso.socket /etc/systemd/system/panso.socket +sudo install -m 0644 tools/systemd/panso.service /etc/systemd/system/panso.service +``` + +Enable and start the service: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now panso.socket +sudo systemctl enable --now panso.service + +curl --unix-socket /run/panso/panso.sock https://panso.se +``` + +Install and enable timers: + +```bash +sudo install -m 0644 tools/systemd/panso-backup.{service,timer} /etc/systemd/system/ +sudo install -m 0644 tools/systemd/panso-import-drops.{service,timer} /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now panso-backup.timer panso-import-drops.timer +``` + +## Development + +```bash +uv run python manage.py createsuperuser +uv run python manage.py makemigrations +uv run python manage.py migrate +uv run python manage.py collectstatic +uv run python manage.py runserver +uv run pytest +``` + +## Celery + +Start a worker: + +```bash +uv run celery -A config worker --loglevel=info +uv run celery -A config beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler +``` diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..e1b31a0 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,205 @@ +import logging +import os +import sys +from pathlib import Path +from typing import Any + +import sentry_sdk +from dotenv import load_dotenv +from platformdirs import user_data_dir + +logger: logging.Logger = logging.getLogger("panso.settings") + +load_dotenv(verbose=True) + +DEBUG: bool = ( + os.getenv("DEBUG", "False").lower() in {"true", "1", "t"} or "runserver" in sys.argv +) +TESTING: bool = ( + os.getenv("TESTING", "False").lower() in {"true", "1", "t"} + or "test" in sys.argv + or "PYTEST_VERSION" in os.environ +) + + +def get_data_dir() -> Path: + r"""Get the directory where the application data will be stored. + + This directory is created if it does not exist. + + Returns: + Path: The directory where the application data will be stored. + + For example, on Windows, it might be: + `C:\Users\lovinator\AppData\Roaming\TheLovinator\Panso` + + In this directory, application data such as media and static files will be stored. + """ + data_dir: str = user_data_dir( + appname="Panso", + appauthor="TheLovinator", + roaming=True, + ensure_exists=True, + ) + return Path(data_dir) + + +DATA_DIR: Path = get_data_dir() + +ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "tlovinator@gmail.com")] +BASE_DIR: Path = Path(__file__).resolve().parent.parent +ROOT_URLCONF = "config.urls" +SECRET_KEY: str = os.getenv("DJANGO_SECRET_KEY", default="") +if not SECRET_KEY: + logger.error("DJANGO_SECRET_KEY environment variable is not set.") + sys.exit(1) + + +DEFAULT_FROM_EMAIL: str | None = os.getenv(key="EMAIL_HOST_USER", default=None) +EMAIL_HOST: str = os.getenv(key="EMAIL_HOST", default="smtp.gmail.com") +EMAIL_HOST_PASSWORD: str | None = os.getenv(key="EMAIL_HOST_PASSWORD", default=None) +EMAIL_HOST_USER: str | None = os.getenv(key="EMAIL_HOST_USER", default=None) +EMAIL_PORT: int = int(os.getenv(key="EMAIL_PORT", default=str(587))) +EMAIL_SUBJECT_PREFIX = "[Panso.se] " +EMAIL_TIMEOUT: int = int(os.getenv(key="EMAIL_TIMEOUT", default=str(10))) +EMAIL_USE_LOCALTIME = True +EMAIL_USE_TLS: bool = os.getenv(key="EMAIL_USE_TLS", default="True") == "True" +EMAIL_USE_SSL: bool = os.getenv(key="EMAIL_USE_SSL", default="False") == "True" +SERVER_EMAIL: str | None = os.getenv(key="EMAIL_HOST_USER", default=None) + +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/accounts/login/" +LOGOUT_REDIRECT_URL = "/" + +ACCOUNT_EMAIL_VERIFICATION = "none" +ACCOUNT_AUTHENTICATION_METHOD = "username" +ACCOUNT_EMAIL_REQUIRED = False + +MEDIA_ROOT: Path = DATA_DIR / "media" +MEDIA_ROOT.mkdir(exist_ok=True) +MEDIA_URL = "/media/" + +STATIC_ROOT: Path = DATA_DIR / "staticfiles" +STATIC_ROOT.mkdir(exist_ok=True) +STATIC_URL = "/static/" +STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"] + +TIME_ZONE = "UTC" +WSGI_APPLICATION = "config.wsgi.application" + +INTERNAL_IPS: list[str] = [] +if DEBUG: + INTERNAL_IPS = ["127.0.0.1", "localhost"] # pyright: ignore[reportConstantRedefinition] + +ALLOWED_HOSTS: list[str] = [".localhost", "127.0.0.1", "[::1]", "testserver"] +if not DEBUG: + ALLOWED_HOSTS = ["panso.se"] # pyright: ignore[reportConstantRedefinition] + + +LOGGING: dict[str, Any] = { + "version": 1, + "disable_existing_loggers": False, + "handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}}, + "loggers": { + "": {"handlers": ["console"], "level": "INFO", "propagate": True}, + "panso": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "django": {"handlers": ["console"], "level": "INFO", "propagate": False}, + "django.utils.autoreload": { + "handlers": ["console"], + "level": "INFO", + "propagate": True, + }, + }, +} + +INSTALLED_APPS: list[str] = [ + # Django built-in apps + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.staticfiles", + "django.contrib.postgres", + # Internal apps + # Third-party apps + "django_celery_results", + "django_celery_beat", +] + +MIDDLEWARE: list[str] = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", +] + +TEMPLATES: list[dict[str, Any]] = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + ], + }, + }, +] + +DATABASES: dict[str, dict[str, Any]] = ( + {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} + if TESTING + else { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("POSTGRES_DB", "panso"), + "USER": os.getenv("POSTGRES_USER", "panso"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD", ""), + "HOST": os.getenv("POSTGRES_HOST", "localhost"), + "PORT": int(os.getenv("POSTGRES_PORT", str(5432))), + "CONN_MAX_AGE": int(os.getenv("CONN_MAX_AGE", str(60))), + "CONN_HEALTH_CHECKS": os.getenv("CONN_HEALTH_CHECKS", "True") == "True", + "OPTIONS": { + "connect_timeout": int(os.getenv("DB_CONNECT_TIMEOUT", str(10))), + }, + }, + } +) + +if not TESTING: + INSTALLED_APPS = [*INSTALLED_APPS, "debug_toolbar", "silk"] # pyright: ignore[reportConstantRedefinition] + MIDDLEWARE = [ # pyright: ignore[reportConstantRedefinition] + "debug_toolbar.middleware.DebugToolbarMiddleware", + "silk.middleware.SilkyMiddleware", + *MIDDLEWARE, + ] + + if not DEBUG: + sentry_sdk.init( + dsn="https://1aa1ac672090fb795783de0e90a2b19f@o4505228040339456.ingest.us.sentry.io/4511055670738944", + send_default_pii=True, + enable_logs=True, + traces_sample_rate=1.0, + profile_session_sample_rate=1.0, + profile_lifecycle="trace", + ) + +REDIS_URL_CACHE: str = os.getenv( + key="REDIS_URL_CACHE", + default="redis://localhost:6379/0", +) +REDIS_URL_CELERY: str = os.getenv( + key="REDIS_URL_CELERY", + default="redis://localhost:6379/1", +) + +CACHES: dict[str, dict[str, str]] = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": REDIS_URL_CACHE, + }, +} + +CELERY_BROKER_URL: str = REDIS_URL_CELERY +CELERY_RESULT_BACKEND = "django-db" +CELERY_RESULT_EXTENDED = True +CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..64ac868 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,6 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from django.urls.resolvers import URLResolver + +urlpatterns: list[URLResolver] = [] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..1951fb1 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,14 @@ +import os +from typing import TYPE_CHECKING + +from django.core.wsgi import get_wsgi_application + +if TYPE_CHECKING: + from django.core.handlers.wsgi import WSGIHandler + +os.environ.setdefault( + key="DJANGO_SETTINGS_MODULE", + value="config.settings", +) + +application: WSGIHandler = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..230b6db --- /dev/null +++ b/manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main() -> None: + """Run administrative tasks. + + Raises: + ImportError: If Django is not installed or cannot be imported. + """ + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line # noqa: PLC0415 + except ImportError as exc: + msg = "Couldn't import Django. Don't forget you have to use 'uv' to run this project." + raise ImportError(msg) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2014720 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,134 @@ +[project] +name = "Panso" +version = "0.1.0" +description = "Price tracker for Swedish stores" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "celery[redis]", + "colorama", + "dateparser", + "django-auto-prefetch", + "django-celery-beat", + "django-celery-results", + "django-debug-toolbar", + "django-silk", + "django", + "flower", + "gunicorn", + "hiredis", + "httpx", + "index-now-for-python", + "json-repair", + "pillow", + "platformdirs", + "psycopg[binary]", + "pydantic", + "pygments", + "python-dotenv", + "redis", + "sentry-sdk", + "setproctitle", + "sitemap-parser", + "tqdm", +] + + +[dependency-groups] +dev = [ + "django-stubs", + "djlint", + "hypothesis[django]", + "pytest-cov", + "pytest-django", + "pytest-randomly", + "pytest-xdist[psutil]", + "pytest", +] + +[tool.ruff] +fix = true +preview = true +unsafe-fixes = true + +format.docstring-code-format = true +format.preview = true + +lint.future-annotations = true +lint.isort.force-single-line = true +lint.pycodestyle.ignore-overlong-task-comments = true +lint.pydocstyle.convention = "google" +lint.select = ["ALL"] + +# Don't automatically remove unused variables +lint.unfixable = ["F841"] + +lint.ignore = [ + "ANN002", # Checks that function *args arguments have type annotations. + "ANN003", # Checks that function **kwargs arguments have type annotations. + "C901", # Checks for functions with a high McCabe complexity. + "CPY001", # Checks for the absence of copyright notices within Python files. + "D100", # Checks for undocumented public module definitions. + "D104", # Checks for undocumented public package definitions. + "D105", # Checks for undocumented magic method definitions. + "D106", # Checks for undocumented public class definitions, for nested classes. + "E501", # Checks for lines that exceed the specified maximum character length. + "ERA001", # Checks for commented-out Python code. + "FIX002", # Checks for "TODO" comments. + "PLR0911", # Checks for functions or methods with too many return statements. + "PLR0912", # Checks for functions or methods with too many branches, including (nested) if, elif, and else branches, for loops, try-except clauses, and match and case statements. + "PLR6301", # Checks for the presence of unused self parameter in methods definitions. + "RUF012", # Checks for mutable default values in class attributes. + "ARG001", # Checks for the presence of unused arguments in function definitions. + + # Conflicting lint rules when using Ruff's formatter + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "COM812", # Checks for the absence of trailing commas. + "COM819", # Checks for the presence of prohibited trailing commas. + "D206", # Checks for docstrings that are indented with tabs. + "D300", # Checks for docstrings that use '''triple single quotes''' instead of """triple double quotes""". + "E111", # Checks for indentation with a non-multiple of 4 spaces. + "E114", # Checks for indentation of comments with a non-multiple of 4 spaces. + "E117", # Checks for over-indented code. + "ISC001", # Checks for implicitly concatenated strings on a single line. + "ISC002", # Checks for implicitly concatenated strings that span multiple lines. + "Q000", # Checks for inline strings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.inline-quotes option. + "Q001", # Checks for multiline strings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.multiline-quotes setting. + "Q002", # Checks for docstrings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.docstring-quotes setting. + "Q003", # Checks for strings that include escaped quotes, and suggests changing the quote style to avoid the need to escape them. + "W191", # Checks for indentation that uses tabs. +] + +[tool.ruff.lint.per-file-ignores] +"**/tests/**" = [ + "ARG", + "FBT", + "PLR0904", + "PLR2004", + "PLR6301", + "S101", + "S105", + "S106", + "S311", + "SLF001", +] +"**/migrations/**" = ["RUF012"] + +[tool.djlint] +profile = "django" +ignore = "H021,H030" +blank_line_after_tag = "load,extends" +close_void_tags = true +format_css = true +format_js = true +indent = 2 +max_line_length = 119 + +[tool.djlint.css] +indent_size = 2 + +[tool.djlint.js] +indent_size = 2 + +[tool.uv.sources] +sitemap-parser = { git = "https://github.com/TheLovinator1/sitemap-parser.git" } diff --git a/tools/systemd/panso.service b/tools/systemd/panso.service new file mode 100644 index 0000000..977c6ae --- /dev/null +++ b/tools/systemd/panso.service @@ -0,0 +1,32 @@ +[Unit] +Description=Panso +Requires=panso.socket +After=network.target + +[Service] +Type=simple +User=panso +Group=panso +WorkingDirectory=/home/panso/panso +EnvironmentFile=/home/panso/panso/.env +RuntimeDirectory=panso +UMask=0077 +ExecStart=/usr/bin/uv run gunicorn config.wsgi:application --bind unix:/run/panso/panso.sock --workers 13 --name panso --max-requests-jitter 50 --max-requests 1200 +ExecReload=/bin/kill -s HUP $MAINPID + +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=no +ReadWritePaths=/home/panso/panso /run/panso +PrivateDevices=yes +CapabilityBoundingSet= +AmbientCapabilities= +RestrictRealtime=yes +LockPersonality=yes + +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/tools/systemd/panso.socket b/tools/systemd/panso.socket new file mode 100644 index 0000000..6a6efcb --- /dev/null +++ b/tools/systemd/panso.socket @@ -0,0 +1,11 @@ +[Unit] +Description=Panso Socket + +[Socket] +ListenStream=/run/panso/panso.sock +SocketUser=panso +SocketGroup=panso +SocketMode=0660 + +[Install] +WantedBy=sockets.target