diff --git a/.djinit b/.djinit index b841008..2f894fa 100644 --- a/.djinit +++ b/.djinit @@ -8,13 +8,14 @@ }, "apps": { "nested": true, - "nested_dir": "apps" + "nested_dir": "src" }, "settings": { - "use_database_url": true, - "database_type": "postgresql", + "use_database_url": false, + "database_type": "mysql", "use_tailwind": true, - "use_htmx": true + "use_htmx": true, + "use_vite": false }, "cicd": { "github": true, diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..505d698 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +.venv +venv +ENV +.env +.git +.gitignore +*.md +README* +LICENSE +.coverage +htmlcov +.pytest_cache +.mypy_cache +.ruff_cache +.DS_Store +node_modules +*.log +db.sqlite3 +media +staticfiles \ No newline at end of file diff --git a/.env.sample b/.env.sample index d52096e..d11aaef 100644 --- a/.env.sample +++ b/.env.sample @@ -1,13 +1,23 @@ +# Django settings DJANGO_SETTINGS_MODULE=config.settings.development SECRET_KEY=your-secret-key ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com -DATABASE_URL=postgres://user:password@localhost:5432/config +# Database (individual parameters) +DB_NAME= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= +# Frontend URL +FRONTEND_URL=https://frontend.your-domain.com,https://www.frontend.your-domain.com + +# Email settings EMAIL_HOST= EMAIL_PORT= EMAIL_USE_TLS= EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= DEFAULT_FROM_EMAIL= -SERVER_EMAIL= +SERVER_EMAIL= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de1b892..230ce2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,22 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.13"] - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: test_db - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 + python-version: ["3.13"] steps: - uses: actions/checkout@v4 @@ -39,71 +24,43 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v4 with: - version: "latest" + enable-cache: true + cache-suffix: ${{ matrix.python-version }} - name: Install dependencies run: uv sync + - name: Run linting run: uv run ruff check . - name: Format check run: uv run ruff format --check . - - name: Set up PostgreSQL - env: - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - echo "PostgreSQL is ready" - - - name: Run Django tests + - name: Run Django checks env: DJANGO_SETTINGS_MODULE: config.settings.development SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 run: | uv run python manage.py check + + - name: Run tests + env: + DJANGO_SETTINGS_MODULE: config.settings.development + SECRET_KEY: test-secret-key-for-ci + run: | uv run python manage.py test - name: Check migrations env: DJANGO_SETTINGS_MODULE: config.settings.development SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 run: | uv run python manage.py makemigrations --check --dry-run - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - run: uv sync - - - name: Run security check - run: uv run python manage.py check --deploy \ No newline at end of file + - name: Security check + env: + DJANGO_SETTINGS_MODULE: config.settings.development + SECRET_KEY: test-secret-key-for-ci + run: | + uv run python manage.py check --deploy \ No newline at end of file diff --git a/.gitignore b/.gitignore index 07a5ed5..3aa4657 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ - #--------------------------------------------------# # The following was generated by djinit: # #--------------------------------------------------# -.django_tailwind_cli/ -.django_tailwind_cli # Python *.py[cod] @@ -54,6 +51,7 @@ media/ staticfiles/ staticfiles/ static_root/ +/static/ # Testing .coverage @@ -92,7 +90,4 @@ secrets.json *.sql # Cloud -.cloud - -# Migrations — regenerated per environment -**/migrations/ +.cloud \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..04aa6ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + default-libmysqlclient-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* +ENV NODE_VERSION=20 +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN python manage.py tailwind install + + +COPY . . + +RUN python manage.py collectstatic --noinput || true + +EXPOSE 8000 + +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"] \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..a36a146 --- /dev/null +++ b/Procfile @@ -0,0 +1,20 @@ +# Procfile for config +# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc. + +# Web server process (using Gunicorn) +# Adjust workers based on your needs: (2 x CPU cores) + 1 +web: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info + +# Alternative web server configurations (uncomment to use): +# web: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 +# web: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 + +# Release task (runs migrations before deployment) +release: python manage.py migrate --noinput && python manage.py collectstatic --noinput + +# Uncomment below to run scheduled tasks (if using Celery) +# worker: celery -A config worker --loglevel=info +# beat: celery -A config beat --loglevel=info + +# Development server (for local testing with Foreman/Overmind) +# web: python manage.py runserver 0.0.0.0:$PORT \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..002b126 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# config + +A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit). + +## Features + +- Modern project structure with environment-specific settings +- Pre-configured REST API with JWT authentication +- Essential dependencies and utilities +- Production-ready configuration + +## Setup + +### Using Just (Recommended) + +1. Set environment variables in `.env` file + +2. Run setup (installs dependencies, runs migrations, creates superuser): + ```bash + just setup + ``` + +3. Start development server: + ```bash + just dev + ``` + +### Traditional Method + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +2. Set environment variables in `.env` file + +3. Run migrations: + ```bash + python manage.py migrate + ``` + +4. Create superuser: + ```bash + python manage.py createsuperuser + ``` + +5. Start development server: + ```bash + python manage.py runserver + ``` + +## Available Commands + +Run `just` to see all available commands, including: + +- `just dev` - Run development server +- `just migrate` - Run migrations +- `just makemigrations` - Create migrations +- `just createsuperuser` - Create superuser +- `just test` - Run tests +- `just lint` - Lint code +- `just format` - Format code +- `just shell` - Django shell +- `just clean` - Clean cache files +- `just install ` - Install package +- `just remove ` - Remove package +- `just server` - Run production server +- `just ci` - Run all linting and checks +- `just test-coverage` - Run tests with coverage + +## Project Structure + +``` +config/ +├── config/ +│ ├── settings/ +│ │ ├── base.py +│ │ ├── development.py +│ │ └── production.py +│ ├── admin/ +│ ├── api/ # API endpoints organized by model +│ │ ├── user/ # Example: if you have a User model +│ │ │ ├── views.py +│ │ │ ├── serializers.py +│ │ │ └── urls.py +│ │ └── urls.py +│ ├── models/ # Django models +│ ├── tests/ +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +├── manage.py +├── requirements.txt +├── pyproject.toml +├── uv.lock +├── .env.sample +└── README.md + +**Note:** For the single folder layout, organize your API endpoints by model name under the `api/` directory. +For example, if you have a `User` model in `models/user.py`, create: +- `api/user/views.py` - API views for User +- `api/user/serializers.py` - Serializers for User +- `api/user/urls.py` - URL patterns for User endpoints + +See the README files in `api/` and `models/` directories for more details. +``` + +## API Documentation + +When running in development mode, API documentation is available at: +- Swagger UI: http://localhost:8000/docs/ +- Schema: http://localhost:8000/schema/ \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..edc05e7 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,13 @@ +"""generated with djinit""" + +import os +from pathlib import Path +import environ +from django.core.asgi import get_asgi_application + +env_path = Path(__file__).resolve().parent.parent / ".env" +env = environ.Env() +environ.Env.read_env(env_path) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="config.settings.production")) +application = get_asgi_application() \ No newline at end of file diff --git a/config/settings/__init__.py b/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings/base.py b/config/settings/base.py new file mode 100644 index 0000000..11cf99c --- /dev/null +++ b/config/settings/base.py @@ -0,0 +1,220 @@ +""" +generated with djinit +Common settings shared between development and production environment +""" + +import os +from datetime import timedelta +from pathlib import Path + +import environ + +env_path = Path(__file__).resolve().parent.parent.parent / ".env" +env = environ.Env() +environ.Env.read_env(env_path) + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + + +# Application definition +THIRD_PARTY_APPS = [ + "rest_framework", + "rest_framework_simplejwt", + "rest_framework_simplejwt.token_blacklist", + "corsheaders", + "drf_yasg", + "django_tailwind_cli", + "django_htmx", +] + +USER_DEFINED_APPS = [ + "src.epics.apps.EpicsConfig", + "src.notes.apps.NotesConfig", + "src.projects.apps.ProjectsConfig", + "src.accounts.apps.AccountsConfig", + "src.external.apps.ExternalConfig", + "src.notifications.apps.NotificationsConfig", + "src.tickets.apps.TicketsConfig", +] + +BUILT_IN_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS +INSTALLED_APPS.insert(0, "jazzmin") + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_htmx.middleware.HtmxMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +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", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +# Static files and media files +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" + +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# REST Framework Settings +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, +} + +# JWT Settings +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=3), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, +} + +# CORS Settings +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +] + +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +] + +CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours +# Security Settings +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" + +# for tailwind +STATICFILES_DIRS = [BASE_DIR / "static/"] +TAILWIND_CLI_VERSION = "2.7.8" +TAILWIND_CLI_USE_DAISY_UI = True +TAILWIND_CLI_CONFIG = { + "theme": { + "extend": { + "colors": { + "primary": "#DC2626", + "secondary": "#EF4444", + "accent": "#FCFECC", + } + } + }, + "daisyui": { + "themes": [ + { + "light": { + "primary": "#DC2626", + "secondary": "#EF4444", + "accent": "#FCFECC", + "neutral": "#1a1a2e", + "base-100": "#ffffff", + "base-200": "#f8fafc", + "base-300": "#e2e8f0", + "info": "#0E4D80", + "success": "#16a34a", + "warning": "#f59e0b", + "error": "#DC2626", + } + } + ] + }, +} diff --git a/config/settings/development.py b/config/settings/development.py new file mode 100644 index 0000000..b00054b --- /dev/null +++ b/config/settings/development.py @@ -0,0 +1,42 @@ +"""generated with djinit""" + +from .base import * # noqa: F401, F403 + +SECRET_KEY = "Z*xa0W7sE9x2A+6#GGR#%+z+!m1!g2WYdE%JY6cAH#B2U+k1xX" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + "OPTIONS": { + "transaction_mode": "IMMEDIATE", + "timeout": 5, # seconds + "init_command": """ + PRAGMA journal_mode=WAL; + PRAGMA synchronous=NORMAL; + PRAGMA mmap_size = 134217728; + PRAGMA journal_size_limit = 27103364; + PRAGMA cache_size=2000; + """, + }, + } +} + +# Email backend for development +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# CORS settings for development +# Allow all origins in development (convenient but not secure) +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True + +# Disable HTTPS redirect in development +SECURE_SSL_REDIRECT = False diff --git a/config/settings/production.py b/config/settings/production.py new file mode 100644 index 0000000..ad54abd --- /dev/null +++ b/config/settings/production.py @@ -0,0 +1,57 @@ +"""generated with djinit""" + +from .base import * # noqa: F401, F403 + +DEBUG = False +SECRET_KEY = env('SECRET_KEY') +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) + +# Database +# Using individual database parameters +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'PORT': env('DB_PORT', default='3306'), + 'NAME': env('DB_NAME'), + 'USER': env('DB_USER'), + 'PASSWORD': env('DB_PASSWORD'), + 'HOST': env('DB_HOST'), + } +} + +# CORS settings for production +# Filter out empty strings from FRONTEND_URL +CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[]) +CORS_ALLOW_CREDENTIALS = True + +# Additional CORS security for production +CORS_ALLOW_PRIVATE_NETWORK = False + +# Email settings +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com") +EMAIL_PORT = env.int('EMAIL_PORT', default=587) +EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True) +EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="") +EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="") +DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="") +SERVER_EMAIL = env('EMAIL_HOST_USER', default="") + +# Security settings for production +SECURE_SSL_REDIRECT = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# Session settings +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# Static files +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +## Tailwind +TAILWIND_CLI_VERSION = "4.1.3" # Pin version +TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI +TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css" diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..1047589 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,58 @@ +"""generated with djinit""" + +from django.contrib import admin +from django.urls import path, re_path +from django.conf import settings +from django.views.static import serve +from django.urls import include +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) + +urlpatterns = [ + path("admin/", admin.site.urls), + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), + # App URLs + path("", include("src.epics.urls")), + path("", include("src.notes.urls")), + path("", include("src.projects.urls")), + path("", include("src.accounts.urls")), + path("", include("src.external.urls")), + path("", include("src.notifications.urls")), + path("", include("src.tickets.urls")), +] + +# Don't show schema in production +if settings.DEBUG: + schema_view = get_schema_view( + openapi.Info( + title="config API", + default_version="v1", + ), + public=True, + ) + urlpatterns += [ + path("docs/", schema_view.with_ui("swagger", cache_timeout=0)), + path("schema/", schema_view.with_ui("redoc", cache_timeout=0)), + ] + +# django-browser-reload for tailwind live reload (dev only) +if settings.DEBUG: + urlpatterns += [ + path("__reload__/", include("django_browser_reload.urls")), + ] + +urlpatterns += [ + re_path( + r"^media/(?P.*)$", + serve, + {"document_root": settings.MEDIA_ROOT}, + ), +] \ No newline at end of file diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..edcfc80 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,14 @@ +"""generated with djinit""" + +import os +from pathlib import Path +import environ +from django.core.wsgi import get_wsgi_application + +env_path = Path(__file__).resolve().parent.parent / ".env" +env = environ.Env() +environ.Env.read_env(env_path) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="config.settings.production")) + +application = get_wsgi_application() \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..a1212a4 --- /dev/null +++ b/justfile @@ -0,0 +1,230 @@ +# Justfile for config +# Task runner for Django development +# Install just: https://github.com/casey/just + +# Default task: show available commands +default: + @just --list + +# Run development server +dev: + uv run python manage.py runserver + +# Run development server with custom host and port +dev-server HOST="0.0.0.0" PORT="8000": + uv run python manage.py runserver {{ HOST }}:{{ PORT }} + + +# Create migrations for all apps +makemigrations: + uv run python manage.py makemigrations + +# Create migrations for specific app +makemigrations-app APP: + uv run python manage.py makemigrations {{ APP }} + +# Run all migrations +migrate: + uv run python manage.py migrate + +# Reset database (delete all migrations) +reset-db: + uv run python manage.py flush --noinput + uv run python manage.py migrate + +# Create superuser +createsuperuser: + uv run python manage.py createsuperuser + +# Collect static files +collectstatic: + uv run python manage.py collectstatic --noinput + +# Run Django shell +shell: + uv run python manage.py shell + +# Run Django shell plus (IPython) +shell-plus: + uv run python manage.py shell_plus + +# Run tests +test: + uv run python manage.py test + +# Run tests with coverage +test-coverage: + uv run pytest --cov=. --cov-report=html --cov-report=term + +# Lint code with ruff +lint: + uv run ruff check . + +# Format code with ruff +format: + uv run ruff format . + +# Lint and format code +style: + uv run ruff check . + uv run ruff format . + +# Check security issues +check: + uv run python manage.py check + +# Check security issues (production settings) +check-deploy: + uv run python manage.py check --deploy + +# Show all database migrations status +showmigrations: + uv run python manage.py showmigrations + +# Sync dependencies (uv equivalent of pip freeze) +requirements: + #!/usr/bin/env -S uv run python + import tomllib + from pathlib import Path + pyproject = Path("pyproject.toml") + if not pyproject.exists(): + print("pyproject.toml not found") + raise SystemExit(1) + data = tomllib.loads(pyproject.read_text()) + deps = data.get("project", {}).get("dependencies", []) + Path("requirements.txt").write_text("\n".join(deps) + "\n") + print("Generated requirements.txt from pyproject.toml") + +# Run all linting and checks +ci: + uv run ruff check . + uv run ruff format --check . + uv run python manage.py check + +# Start interactive shell +shell-ipython: + uv run python manage.py shell_plus + +# Clear all cache +clear-cache: + uv run python manage.py clear_cache + +# Show Django version +version: + uv run python manage.py version + +# Run database shell +dbshell: + uv run python manage.py dbshell + +# Start production server with gunicorn +server: + uv run gunicorn config.wsgi:application --bind 0.0.0.0:8000 + +# Start production server with gunicorn workers +server-prod WORKERS="4": + uv run gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }} + +# Start production server with gunicorn and reload on file changes +server-watch: + uv run gunicorn config.wsgi:application --bind 0.0.0.0:8000 --reload + +# Clean Python cache files +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + find . -type f -name ".coverage" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + + rm -rf htmlcov/ + rm -rf .pytest_cache/ + +# Pin dependencies in pyproject.toml using versions from uv.lock, keeping >= and extras +pin-deps: + #!/usr/bin/env -S uv run python + import tomllib, re + from pathlib import Path + + lock = Path("uv.lock") + if not lock.exists(): + print("uv.lock not found — run `uv sync` first") + raise SystemExit(0) + + data = tomllib.loads(lock.read_text()) + packages = {p["name"].lower(): p["version"] for p in data.get("package", [])} + + py = Path("pyproject.toml") + lines = py.read_text().splitlines() + + pattern = re.compile(r'^(\s*)"([^"]+)"(,?)') + + out = [] + for line in lines: + m = pattern.match(line) + if not m: + out.append(line) + continue + + indent, dep, comma = m.groups() + + base = dep + extras = "" + operator = ">=" + version = None + + # extras: django[postgres] + if "[" in base and "]" in base: + before, after = base.split("[", 1) + extras = "[" + after.split("]")[0] + "]" + base = before + + # detect >= or == + if ">=" in dep: + operator = ">=" + base, version = dep.split(">=") + base = base.strip() + elif "==" in dep: + operator = "==" + base, version = dep.split("==") + base = base.strip() + else: + base = dep + + base_lower = base.lower() + new_version = packages.get(base_lower) + + if new_version: + new_dep = f'{base}{extras}{operator}{new_version}' + line = f'{indent}"{new_dep}"{comma}' + + out.append(line) + + py.write_text("\n".join(out) + "\n") + print("Pinned dependencies (>= preserved, extras preserved).") + + +# Setup project for development (using uv) +setup: + uv sync + just pin-deps + uv run python manage.py migrate + uv run python manage.py createsuperuser + +# Complete setup including static files +setup-complete: + uv sync + uv run python manage.py migrate + uv run python manage.py collectstatic --noinput + uv run python manage.py createsuperuser + +# Install a new package +install PACKAGE: + uv add {{ PACKAGE }} + +# Install a dev package +install-dev PACKAGE: + uv add --dev {{ PACKAGE }} + +# Remove a package +remove PACKAGE: + uv remove {{ PACKAGE }} \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..b68a555 --- /dev/null +++ b/manage.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +"""generated with djinit""" + +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from pathlib import Path + +import environ + +env_path = Path(__file__).resolve().parent / ".env" +env = environ.Env() +environ.Env.read_env(env_path) + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + env("DJANGO_SETTINGS_MODULE", default="config.settings.production"), + ) + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + env = environ.Env() + # Read .env file, if it exists + environ.Env.read_env() + + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4ca9c58 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "backend" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.13" +license = { text = "MIT" } +dependencies = [ + "django", + "django-environ", + "django-jazzmin", + "djangorestframework", + "djangorestframework-simplejwt", + "drf-yasg", + "django-cors-headers", + "whitenoise", + "mysqlclient", + "django-tailwind-cli", + "django-htmx", + "gunicorn", +] + +[dependency-groups] +dev = [ + "ruff>=0.14.2", + "pytest>=8.0.0", + "pytest-django>=4.9.0", +] + +[tool.ruff] +line-length = 88 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by formatter + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.lint.isort] +known-first-party = ["config"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7a9ec29 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +Django +django-environ +django-jazzmin +djangorestframework +djangorestframework_simplejwt +drf-yasg +django-cors-headers +whitenoise +django-tailwind-cli +django-htmx +mysqlclient +gunicorn \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/admin.py b/src/accounts/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/accounts/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/accounts/apps.py b/src/accounts/apps.py new file mode 100644 index 0000000..1438ca9 --- /dev/null +++ b/src/accounts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.accounts' \ No newline at end of file diff --git a/src/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/models.py b/src/accounts/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/accounts/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/accounts/serializers.py b/src/accounts/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/accounts/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/accounts/tests.py b/src/accounts/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/accounts/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/accounts/urls.py b/src/accounts/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/accounts/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/accounts/views.py b/src/accounts/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/accounts/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/epics/__init__.py b/src/epics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/epics/admin.py b/src/epics/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/epics/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/epics/apps.py b/src/epics/apps.py new file mode 100644 index 0000000..f32b3dc --- /dev/null +++ b/src/epics/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class EpicsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.epics' \ No newline at end of file diff --git a/src/epics/migrations/__init__.py b/src/epics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/epics/models.py b/src/epics/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/epics/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/epics/serializers.py b/src/epics/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/epics/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/epics/tests.py b/src/epics/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/epics/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/epics/urls.py b/src/epics/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/epics/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/epics/views.py b/src/epics/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/epics/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/external/__init__.py b/src/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/external/admin.py b/src/external/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/external/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/external/apps.py b/src/external/apps.py new file mode 100644 index 0000000..1b19bf1 --- /dev/null +++ b/src/external/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class ExternalConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.external' \ No newline at end of file diff --git a/src/external/migrations/__init__.py b/src/external/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/external/models.py b/src/external/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/external/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/external/serializers.py b/src/external/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/external/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/external/tests.py b/src/external/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/external/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/external/urls.py b/src/external/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/external/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/external/views.py b/src/external/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/external/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/notes/__init__.py b/src/notes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/notes/admin.py b/src/notes/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/notes/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/notes/apps.py b/src/notes/apps.py new file mode 100644 index 0000000..872fe05 --- /dev/null +++ b/src/notes/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class NotesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.notes' \ No newline at end of file diff --git a/src/notes/migrations/__init__.py b/src/notes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/notes/models.py b/src/notes/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/notes/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/notes/serializers.py b/src/notes/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/notes/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/notes/tests.py b/src/notes/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/notes/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/notes/urls.py b/src/notes/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/notes/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/notes/views.py b/src/notes/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/notes/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/notifications/__init__.py b/src/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/notifications/admin.py b/src/notifications/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/notifications/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/notifications/apps.py b/src/notifications/apps.py new file mode 100644 index 0000000..bdd4e96 --- /dev/null +++ b/src/notifications/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class NotificationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.notifications' \ No newline at end of file diff --git a/src/notifications/migrations/__init__.py b/src/notifications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/notifications/models.py b/src/notifications/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/notifications/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/notifications/serializers.py b/src/notifications/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/notifications/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/notifications/tests.py b/src/notifications/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/notifications/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/notifications/urls.py b/src/notifications/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/notifications/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/notifications/views.py b/src/notifications/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/notifications/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/projects/__init__.py b/src/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/projects/admin.py b/src/projects/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/projects/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/projects/apps.py b/src/projects/apps.py new file mode 100644 index 0000000..15827da --- /dev/null +++ b/src/projects/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class ProjectsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.projects' \ No newline at end of file diff --git a/src/projects/migrations/__init__.py b/src/projects/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/projects/models.py b/src/projects/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/projects/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/projects/serializers.py b/src/projects/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/projects/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/projects/tests.py b/src/projects/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/projects/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/projects/urls.py b/src/projects/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/projects/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/projects/views.py b/src/projects/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/projects/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/src/tickets/__init__.py b/src/tickets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tickets/admin.py b/src/tickets/admin.py new file mode 100644 index 0000000..0dc5f91 --- /dev/null +++ b/src/tickets/admin.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.contrib import admin + +# Register your models here. \ No newline at end of file diff --git a/src/tickets/apps.py b/src/tickets/apps.py new file mode 100644 index 0000000..222445a --- /dev/null +++ b/src/tickets/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + + +class TicketsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'src.tickets' \ No newline at end of file diff --git a/src/tickets/migrations/__init__.py b/src/tickets/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tickets/models.py b/src/tickets/models.py new file mode 100644 index 0000000..d7b9f75 --- /dev/null +++ b/src/tickets/models.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/src/tickets/serializers.py b/src/tickets/serializers.py new file mode 100644 index 0000000..fbcbed8 --- /dev/null +++ b/src/tickets/serializers.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import serializers + +# Create your serializers here. \ No newline at end of file diff --git a/src/tickets/tests.py b/src/tickets/tests.py new file mode 100644 index 0000000..a8b0ed1 --- /dev/null +++ b/src/tickets/tests.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.test import TestCase + +# Create your tests here. \ No newline at end of file diff --git a/src/tickets/urls.py b/src/tickets/urls.py new file mode 100644 index 0000000..8020533 --- /dev/null +++ b/src/tickets/urls.py @@ -0,0 +1,7 @@ +"""generated with djinit""" + +from django.urls import path + +urlpatterns = [ + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/tickets/views.py b/src/tickets/views.py new file mode 100644 index 0000000..6630beb --- /dev/null +++ b/src/tickets/views.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from rest_framework import viewsets + +# Create your views here. \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..7057bf1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,502 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "backend" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-htmx" }, + { name = "django-jazzmin" }, + { name = "django-tailwind-cli" }, + { name = "djangorestframework" }, + { name = "djangorestframework-simplejwt" }, + { name = "drf-yasg" }, + { name = "gunicorn" }, + { name = "mysqlclient" }, + { name = "whitenoise" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-django" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-htmx" }, + { name = "django-jazzmin" }, + { name = "django-tailwind-cli" }, + { name = "djangorestframework" }, + { name = "djangorestframework-simplejwt" }, + { name = "drf-yasg" }, + { name = "gunicorn" }, + { name = "mysqlclient" }, + { name = "whitenoise" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-django", specifier = ">=4.9.0" }, + { name = "ruff", specifier = ">=0.14.2" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "django" +version = "6.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, +] + +[[package]] +name = "django-environ" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/e1/4c4ddcf6e90f023e89edb1d92f9a7fffce61ad4b691df497f2f5c0be6e26/django_environ-0.14.0.tar.gz", hash = "sha256:b6c48d93b9d2ff8a3ea14099e90c35aa4f101c1b4d5f262dfee0d27b06742ed7", size = 60417, upload-time = "2026-06-18T22:49:55.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/67b423fc30f16d10259e901320fbc121746bf168aaaad9043e6ebb0110c1/django_environ-0.14.0-py3-none-any.whl", hash = "sha256:8dbe8a57f0a540ab8abd6f54f230de5e99e3a2c9d797cb9caecb037bca3d47d8", size = 20934, upload-time = "2026-06-18T22:49:54.355Z" }, +] + +[[package]] +name = "django-htmx" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/f2/8c3e28a5eed8e5226835c762892bfef74eda7e8629c65b49c186098eb303/django_htmx-1.27.0.tar.gz", hash = "sha256:036e5da801bfdf5f1ca815f21592cfb9f004a898f330c842f15e55c70e301a75", size = 65362, upload-time = "2025-11-28T23:18:55.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ac/25d28489dc43224e260f4ebee7565f7ef1efe12af0f284a89500c19f75e2/django_htmx-1.27.0-py3-none-any.whl", hash = "sha256:13e1e13b87d39b57f95aae6e4987cb3df056d0b1373a41f4a94504a00298ffd8", size = 62126, upload-time = "2025-11-28T23:18:53.57Z" }, +] + +[[package]] +name = "django-jazzmin" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/70/a652284cd612bcd45da3cc18715db391c6cdeafd1ec9af60f4a3a243c74e/django_jazzmin-3.0.4.tar.gz", hash = "sha256:e8aef8b75edd8792408a69407c22ece924ab7a3d5c9d6f84d7a4eb57e06e7a44", size = 2423027, upload-time = "2026-03-13T11:29:18.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/1c/42d37c90bec6f6763206b966252dabe019f0c0197a6aaebafa5b213b78f1/django_jazzmin-3.0.4-py3-none-any.whl", hash = "sha256:70935a17604cc530e99be68d3d2b8c576095e1dac4af50464c827d66646f40fa", size = 2495119, upload-time = "2026-03-13T11:29:20.447Z" }, +] + +[[package]] +name = "django-tailwind-cli" +version = "4.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-typer" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/4e/67143e297e6196f3a7661ab912f888493fb9b40c2f50dd04964a7f22f055/django_tailwind_cli-4.6.2.tar.gz", hash = "sha256:479b82096ed2656c8990f59ae2ee2d1ea2abace7d82ba6013a0861502a906004", size = 122664, upload-time = "2026-05-14T09:12:48.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/8b/ed7e4d5757341e7a7daba9d8daa7152c80613208693d1ebb180d579e1428/django_tailwind_cli-4.6.2-py3-none-any.whl", hash = "sha256:5fed9b26c56533fa7c41a92f369ebd41da50b2f520414d5f77b0f2bf9a3caebc", size = 38388, upload-time = "2026-05-14T09:12:46.968Z" }, +] + +[[package]] +name = "django-typer" +version = "3.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "django" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/de/8b38b5d7277e70a00adc6328532f174c632facdf99553b904f79e8aad026/django_typer-3.7.3.tar.gz", hash = "sha256:833059b0bddd5a0c68945c4963064ebb0662b54eec803f86b43904ba6038b777", size = 2977182, upload-time = "2026-05-24T14:33:55.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/ee/ff00e923a81de3ea95deac71667744079d1e2709249631a5484a57b7baf6/django_typer-3.7.3-py3-none-any.whl", hash = "sha256:2e6591e081117725ef036578ada407dd5882b0213a8aff821d50399854b2c6f1", size = 296863, upload-time = "2026-05-24T14:33:53.502Z" }, +] + +[[package]] +name = "djangorestframework" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" }, +] + +[[package]] +name = "djangorestframework-simplejwt" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/27/2874a325c11112066139769f7794afae238a07ce6adf96259f08fd37a9d7/djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f", size = 101265, upload-time = "2025-07-21T16:52:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, +] + +[[package]] +name = "drf-yasg" +version = "1.21.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "packaging" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/345135459b9cbaff0e8ee3270819e89ca92064a35a0a94a1cfce41c084db/drf_yasg-1.21.15.tar.gz", hash = "sha256:ef86838c4ef10dcd3ac1ebf2be601cbe02978b999671caa43667f7c9db961468", size = 5153419, upload-time = "2026-02-24T18:09:21.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/a4/400b0565cf25395f1d5e1a24e5d18dab8f4199e4212174341fea6f05747c/drf_yasg-1.21.15-py3-none-any.whl", hash = "sha256:7c7a7ab9feb0e13cdd6e25147d99adb0500a68bd96509ffd8f8cf7efd4bdc77e", size = 4856209, upload-time = "2026-02-24T18:09:18.982Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mysqlclient" +version = "2.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/d5/76e369b0fdccd2eb9ed7d890e4e3e23aa1344fea62f0180d7f1574285e54/mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300", size = 207158, upload-time = "2026-02-10T10:58:38.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/44/6ae21b4e8490eb5167d04fa7452c462ffd72a502b96bf531477ef044a380/mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0", size = 211644, upload-time = "2026-02-10T10:58:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" }, + { url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" }, + { url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" }, + { url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" }, + { url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" }, + { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +]