Add initial project structure and configuration files
This commit is contained in:
parent
bc40d93d37
commit
988d131c49
13 changed files with 688 additions and 20 deletions
31
.env.example
Normal file
31
.env.example
Normal file
|
|
@ -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
|
||||
88
.gitignore
vendored
88
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
51
.pre-commit-config.yaml
Normal file
51
.pre-commit-config.yaml
Normal file
|
|
@ -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
|
||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
92
README.md
92
README.md
|
|
@ -1,3 +1,95 @@
|
|||
# panso.se
|
||||
|
||||
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
|
||||
```
|
||||
|
|
|
|||
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
205
config/settings.py
Normal file
205
config/settings.py
Normal file
|
|
@ -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"
|
||||
6
config/urls.py
Normal file
6
config/urls.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.urls.resolvers import URLResolver
|
||||
|
||||
urlpatterns: list[URLResolver] = []
|
||||
14
config/wsgi.py
Normal file
14
config/wsgi.py
Normal file
|
|
@ -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()
|
||||
24
manage.py
Executable file
24
manage.py
Executable file
|
|
@ -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()
|
||||
134
pyproject.toml
Normal file
134
pyproject.toml
Normal file
|
|
@ -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" }
|
||||
32
tools/systemd/panso.service
Normal file
32
tools/systemd/panso.service
Normal file
|
|
@ -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
|
||||
11
tools/systemd/panso.socket
Normal file
11
tools/systemd/panso.socket
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
Description=Panso Socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/panso/panso.sock
|
||||
SocketUser=panso
|
||||
SocketGroup=panso
|
||||
SocketMode=0660
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
Loading…
Add table
Add a link
Reference in a new issue