FossilRepo

Initial project from boilerworks-django-htmx

anonymous 2026-04-06 00:19 trunk
Commit d3f1e08ebb8c32868b4da6a3cef663d16df781e9f2bc543967068412c71f5e27
91 files changed +24 +27 +8 +17 +12 +22 +83 +28 +3 +31 +36 +32 +24 +21 +62 +13 +29 +7 +20 +46 +10 +29 +243 +9 +161 +196 +6 +12 +23 +6 +19 +74 +35 +13 +149 +7 +1 +90 +12 +6 +18 +168 +22 +15 +133 +13 +86 +14 +24 +6 +330 +29 +35 +53 +78 +19 +627 +110 +4 +110 +44 +36 +71 +19 +63 +28 +70 +42 +29 +44 +6 +44 +1350
+ .env.example + .github/ISSUE_TEMPLATE/bug_report.md + .github/ISSUE_TEMPLATE/config.yml + .github/ISSUE_TEMPLATE/feature_request.md + .github/dependabot.yml + .github/pull_request_template.md + .github/workflows/ci.yaml + .gitignore + AGENTS.md + CLAUDE.md + CODE_OF_CONDUCT.md + CONTRIBUTING.md + Dockerfile + LICENSE + Makefile + README.md + SECURITY.md + auth1/__init__.py + auth1/apps.py + auth1/forms.py + auth1/migrations/__init__.py + auth1/tests.py + auth1/urls.py + auth1/views.py + bootstrap.md + config/__init__.py + config/celery.py + config/settings.py + config/urls.py + config/wsgi.py + conftest.py + core/__init__.py + core/admin.py + core/apps.py + core/management/__init__.py + core/management/commands/__init__.py + core/middleware/__init__.py + core/middleware/current_user.py + core/migrations/__init__.py + core/models.py + core/permissions.py + core/templatetags/__init__.py + core/templatetags/permissions_tags.py + core/tests.py + core/urls.py + core/views.py + docker-compose.yaml + items/__init__.py + items/admin.py + items/apps.py + items/forms.py + items/migrations/0001_initial.py + items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py + items/migrations/__init__.py + items/models.py + items/tests.py + items/urls.py + items/views.py + manage.py + organization/__init__.py + organization/admin.py + organization/apps.py + organization/migrations/0001_initial.py + organization/migrations/__init__.py + organization/models.py + organization/tests.py + pyproject.toml + run.sh + startup.py + static/admin/css/dark_theme.css + static/admin/img/logo-dark.svg + static/css/input.css + static/img/fossilrepo-logo-dark.png + static/img/fossilrepo-logo-dark.svg + templates/admin/base_site.html + templates/auth1/login.html + templates/base.html + templates/dashboard.html + templates/includes/nav.html + templates/items/item_confirm_delete.html + templates/items/item_detail.html + templates/items/item_form.html + templates/items/item_list.html + templates/items/partials/item_table.html + testdata/__init__.py + testdata/apps.py + testdata/management/__init__.py + testdata/management/commands/__init__.py + testdata/management/commands/seed.py + testdata/migrations/__init__.py + uv.lock
+24
--- a/.env.example
+++ b/.env.example
@@ -0,0 +1,24 @@
1
+DJANGO_SECRET_KEY=change-me-in-production
2
+DJANGO_DEBUG=true
3
+DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
4
+
5
+POSTGRES_DB=fossilrepo
6
+POSTGRES_USER=dbadmin
7
+POSTGRES_PASSWORD=Password123
8
+POSTGRES_HOST=postgres
9
+POSTGRES_PORT=5432
10
+
11
+REDIS_URL=redis://redis:6379/0
12
+CELERY_BROKER=redis://redis:6379/0
13
+CELERY_BACKEND=redis://redis:6379/2
14
+
15
+EMAIL_HOST=mailpit
16
+EMAIL_PORT=1025
17
+
18
+USE_S3=false
19
+AWS_S3_ENDPOINT_URL=http://minio:9000
20
+AWS_STORAGE_BUCKET_NAME=fossilrepo
21
+AWS_ACCESS_KEY_ID=minioadmin
22
+AWS_SECRET_ACCESS_KEY=minioadmin
23
+
24
+SENTRY_DSN=
--- a/.env.example
+++ b/.env.example
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.env.example
+++ b/.env.example
@@ -0,0 +1,24 @@
1 DJANGO_SECRET_KEY=change-me-in-production
2 DJANGO_DEBUG=true
3 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
4
5 POSTGRES_DB=fossilrepo
6 POSTGRES_USER=dbadmin
7 POSTGRES_PASSWORD=Password123
8 POSTGRES_HOST=postgres
9 POSTGRES_PORT=5432
10
11 REDIS_URL=redis://redis:6379/0
12 CELERY_BROKER=redis://redis:6379/0
13 CELERY_BACKEND=redis://redis:6379/2
14
15 EMAIL_HOST=mailpit
16 EMAIL_PORT=1025
17
18 USE_S3=false
19 AWS_S3_ENDPOINT_URL=http://minio:9000
20 AWS_STORAGE_BUCKET_NAME=fossilrepo
21 AWS_ACCESS_KEY_ID=minioadmin
22 AWS_SECRET_ACCESS_KEY=minioadmin
23
24 SENTRY_DSN=
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,27 @@
1
+---
2
+name: Bug report
3
+about: Something is broken
4
+labels: bug
5
+---
6
+
7
+**Describe the bug**
8
+A clear description of what is wrong.
9
+
10
+**To reproduce**
11
+Steps to reproduce the behaviour:
12
+1. ...
13
+2. ...
14
+
15
+**Expected behaviour**
16
+What you expected to happen.
17
+
18
+**Actual behaviour**
19
+What actually happened. Include error messages, stack traces, or screenshots.
20
+
21
+**Environment**
22
+- OS:
23
+- Docker version:
24
+- Branch/commit:
25
+
26
+**Additional context**
27
+Any other relevant information.
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,27 @@
1 ---
2 name: Bug report
3 about: Something is broken
4 labels: bug
5 ---
6
7 **Describe the bug**
8 A clear description of what is wrong.
9
10 **To reproduce**
11 Steps to reproduce the behaviour:
12 1. ...
13 2. ...
14
15 **Expected behaviour**
16 What you expected to happen.
17
18 **Actual behaviour**
19 What actually happened. Include error messages, stack traces, or screenshots.
20
21 **Environment**
22 - OS:
23 - Docker version:
24 - Branch/commit:
25
26 **Additional context**
27 Any other relevant information.
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
1
+blank_issues_enabled: false
2
+contact_links:
3
+ - name: Question or discussion
4
+ url: https://github.com/ConflictHQ/fossilrepo-django-htmx/discussions
5
+ about: Ask questions and discuss ideas here
6
+ - name: Security vulnerability
7
+ url: https://github.com/ConflictHQ/fossilrepo-django-htmx/security/advisories/new
8
+ about: Report security issues privately
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
1 blank_issues_enabled: false
2 contact_links:
3 - name: Question or discussion
4 url: https://github.com/ConflictHQ/fossilrepo-django-htmx/discussions
5 about: Ask questions and discuss ideas here
6 - name: Security vulnerability
7 url: https://github.com/ConflictHQ/fossilrepo-django-htmx/security/advisories/new
8 about: Report security issues privately
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
1
+---
2
+name: Feature request
3
+about: Suggest an addition or improvement
4
+labels: enhancement
5
+---
6
+
7
+**Problem or motivation**
8
+What problem does this solve, or what use case does it enable?
9
+
10
+**Proposed solution**
11
+Describe what you'd like to see. Include any relevant design decisions or trade-offs.
12
+
13
+**Alternatives considered**
14
+Any other approaches you considered and why you ruled them out.
15
+
16
+**Additional context**
17
+Any other relevant information, links, or examples.
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
1 ---
2 name: Feature request
3 about: Suggest an addition or improvement
4 labels: enhancement
5 ---
6
7 **Problem or motivation**
8 What problem does this solve, or what use case does it enable?
9
10 **Proposed solution**
11 Describe what you'd like to see. Include any relevant design decisions or trade-offs.
12
13 **Alternatives considered**
14 Any other approaches you considered and why you ruled them out.
15
16 **Additional context**
17 Any other relevant information, links, or examples.
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
1
+version: 2
2
+updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ open-pull-requests-limit: 5
8
+ - package-ecosystem: "pip"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
12
+ open-pull-requests-limit: 10
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
1 version: 2
2 updates:
3 - package-ecosystem: "github-actions"
4 directory: "/"
5 schedule:
6 interval: "weekly"
7 open-pull-requests-limit: 5
8 - package-ecosystem: "pip"
9 directory: "/"
10 schedule:
11 interval: "weekly"
12 open-pull-requests-limit: 10
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -0,0 +1,22 @@
1
+## What does this PR do?
2
+
3
+<!-- Brief description of the change -->
4
+
5
+## Type of change
6
+
7
+- [ ] Bug fix
8
+- [ ] New feature
9
+- [ ] Refactor / chore
10
+- [ ] Documentation
11
+
12
+## Checklist
13
+
14
+- [ ] Lint passes
15
+- [ ] Tests pass
16
+- [ ] Migrations created for any model/schema changes
17
+- [ ] Tests added or updated
18
+- [ ] `bootstrap.md` updated if conventions changed
19
+
20
+## Related issues
21
+
22
+<!-- Closes #123 -->
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -0,0 +1,22 @@
1 ## What does this PR do?
2
3 <!-- Brief description of the change -->
4
5 ## Type of change
6
7 - [ ] Bug fix
8 - [ ] New feature
9 - [ ] Refactor / chore
10 - [ ] Documentation
11
12 ## Checklist
13
14 - [ ] Lint passes
15 - [ ] Tests pass
16 - [ ] Migrations created for any model/schema changes
17 - [ ] Tests added or updated
18 - [ ] `bootstrap.md` updated if conventions changed
19
20 ## Related issues
21
22 <!-- Closes #123 -->
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,83 @@
1
+name: CI
2
+
3
+on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install ruff
18
+ - run: ruff check .
19
+ - run: ruff format --check .
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ services:
24
+ postgres:
25
+ image: postgres:16-alpine
26
+ env:
27
+ POSTGRES_DB: fossilrepo_test
28
+ POSTGRES_USER: dbadmin
29
+ POSTGRES_PASSWORD: Password123
30
+ ports:
31
+ - 5432:5432
32
+ options: >-
33
+ --health-cmd "pg_isready -U dbadmin -d fossilrepo_test"
34
+ --health-interval 5s
35
+ --health-timeout 5s
36
+ --health-retries 5
37
+ redis:
38
+ image: redis:7-alpine
39
+ ports:
40
+ - 6379:6379
41
+ options: >-
42
+ --health-cmd "redis-cli ping"
43
+ --health-interval 5s
44
+ --health-timeout 5s
45
+ --health-retries 5
46
+ env:
47
+ DJANGO_SECRET_KEY: test-secret-key
48
+ DJANGO_DEBUG: "true"
49
+ POSTGRES_DB: fossilrepo_test
50
+ POSTGRES_USER: dbadmin
51
+ POSTGRES_PASSWORD: Password123
52
+ POSTGRES_HOST: localhost
53
+ POSTGRES_PORT: "5432"
54
+ REDIS_URL: redis://localhost:6379/1
55
+ CELERY_BROKER: redis://localhost:6379/0
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ - uses: actions/setup-python@v5
59
+ with:
60
+ python-version: "3.12"
61
+ - name: Install dependencies
62
+ run: |
63
+ python -c "
64
+ import tomllib, subprocess, sys
65
+ with open('pyproject.toml', 'rb') as f:
66
+ d = tomllib.load(f)
67
+ deps = (d['project'].get('dependencies', []) +
68
+ d['project'].get('optional-dependencies', {}).get('dev', []))
69
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + deps)
70
+ "
71
+ - run: python manage.py migrate --noinput
72
+ - run: python manage.py collectstatic --no-input
73
+ - run: python -m pytest --cov --cov-report=term-missing --cov-fail-under=80 -v
74
+
75
+ audit:
76
+ runs-on: ubuntu-latest
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+ - uses: actions/setup-python@v5
80
+ with:
81
+ python-version: "3.12"
82
+ - run: pip install pip-audit
83
+ - run: pip-audit --strict --desc || true
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,83 @@
1 name: CI
2
3 on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9 jobs:
10 lint:
11 runs-on: ubuntu-latest
12 steps:
13 - uses: actions/checkout@v4
14 - uses: actions/setup-python@v5
15 with:
16 python-version: "3.12"
17 - run: pip install ruff
18 - run: ruff check .
19 - run: ruff format --check .
20
21 test:
22 runs-on: ubuntu-latest
23 services:
24 postgres:
25 image: postgres:16-alpine
26 env:
27 POSTGRES_DB: fossilrepo_test
28 POSTGRES_USER: dbadmin
29 POSTGRES_PASSWORD: Password123
30 ports:
31 - 5432:5432
32 options: >-
33 --health-cmd "pg_isready -U dbadmin -d fossilrepo_test"
34 --health-interval 5s
35 --health-timeout 5s
36 --health-retries 5
37 redis:
38 image: redis:7-alpine
39 ports:
40 - 6379:6379
41 options: >-
42 --health-cmd "redis-cli ping"
43 --health-interval 5s
44 --health-timeout 5s
45 --health-retries 5
46 env:
47 DJANGO_SECRET_KEY: test-secret-key
48 DJANGO_DEBUG: "true"
49 POSTGRES_DB: fossilrepo_test
50 POSTGRES_USER: dbadmin
51 POSTGRES_PASSWORD: Password123
52 POSTGRES_HOST: localhost
53 POSTGRES_PORT: "5432"
54 REDIS_URL: redis://localhost:6379/1
55 CELERY_BROKER: redis://localhost:6379/0
56 steps:
57 - uses: actions/checkout@v4
58 - uses: actions/setup-python@v5
59 with:
60 python-version: "3.12"
61 - name: Install dependencies
62 run: |
63 python -c "
64 import tomllib, subprocess, sys
65 with open('pyproject.toml', 'rb') as f:
66 d = tomllib.load(f)
67 deps = (d['project'].get('dependencies', []) +
68 d['project'].get('optional-dependencies', {}).get('dev', []))
69 subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + deps)
70 "
71 - run: python manage.py migrate --noinput
72 - run: python manage.py collectstatic --no-input
73 - run: python -m pytest --cov --cov-report=term-missing --cov-fail-under=80 -v
74
75 audit:
76 runs-on: ubuntu-latest
77 steps:
78 - uses: actions/checkout@v4
79 - uses: actions/setup-python@v5
80 with:
81 python-version: "3.12"
82 - run: pip install pip-audit
83 - run: pip-audit --strict --desc || true
+28
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,28 @@
1
+__pycache__/
2
+*.py[cod]
3
+*$py.class
4
+*.so
5
+.Python
6
+build/
7
+dist/
8
+*.egg-info/
9
+*.egg
10
+.eggs/
11
+*.whl
12
+.venv/
13
+venv/
14
+env/
15
+.env
16
+*.sqlite3
17
+db.sqlite3
18
+.coverage
19
+htmlcov/
20
+.pytest_cache/
21
+.ruff_cache/
22
+*.log
23
+.DS_Store
24
+Thumbs.db
25
+static/css/output.css
26
+node_modules/
27
+runtime/
28
+assets/
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,28 @@
1 __pycache__/
2 *.py[cod]
3 *$py.class
4 *.so
5 .Python
6 build/
7 dist/
8 *.egg-info/
9 *.egg
10 .eggs/
11 *.whl
12 .venv/
13 venv/
14 env/
15 .env
16 *.sqlite3
17 db.sqlite3
18 .coverage
19 htmlcov/
20 .pytest_cache/
21 .ruff_cache/
22 *.log
23 .DS_Store
24 Thumbs.db
25 static/css/output.css
26 node_modules/
27 runtime/
28 assets/
+3
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -0,0 +1,3 @@
1
+# Agents -- Fossilrepo Django + HTMX
2
+
3
+Read [`bootstrap.md`](bootstrap.md) before writing any code. It is the primary conventions document.
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -0,0 +1,3 @@
 
 
 
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -0,0 +1,3 @@
1 # Agents -- Fossilrepo Django + HTMX
2
3 Read [`bootstrap.md`](bootstrap.md) before writing any code. It is the primary conventions document.
+31
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -0,0 +1,31 @@
1
+
2
+
3
+Primary conventions doc: [`bootstrap.md`](bootstrap.md)
4
+
5
+Read it before writing any code.
6
+
7
+## Project Overview
8
+
9
+fossilrepo is an omnibus-style installer for a self-hosted Fossil forge. Django+HTMX management layer wrapping Fossil SCM server infrastructure with Caddy (SSL/routing), Litestream (S3 backups), and a sync bridge to GitHub/GitLab. Open source (MIT).
10
+
11
+## Stack
12
+
13
+- **Backend**: Django 5 (Python 3.12+)
14
+- **Frontend**: HTMX 2.0 + Alpine.js 3 + Tailwind CSS (CDN)
15
+- **API**: Django views returning HTML (full pages + HTMX partials)
16
+- **ORM**: Django ORM with `Tracking` and `BaseCoreModel` base classes
17
+- **Auth**: Session-based (Django native, httpOnly cookies)
18
+- **Permissions**: Group-based via `P` enum (`core/permissions.py`)
19
+- **Jobs**: Celery + Redis
20
+- **Database**: PostgreSQL 16
21
+- **Linter**: Ruff (check + formaStack
22
+
23
+- **Backend**: Django 5 (Python 3.12+)
24
+- **Frontend**: HTMX 2.0 + Alpine.js 3 + Tailwind CSS (CDN)
25
+- **API**: Django views returning HTML (full pages + HTMX partials)
26
+- **ORM**: Django ORM with `Tracking` and `BaseCoreModel` base classes
27
+- **Auth**: Session-based (Django native, httpOnly cookies)
28
+- **Permissions**: Group-based via `P` enum (`core/permissions.py`)
29
+- **Jobs**: Celery + Redis
30
+- **Database**: PostgreSQL 16
31
+- **Linter**: Ruff (check + fo—�
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -0,0 +1,31 @@
1
2
3 Primary conventions doc: [`bootstrap.md`](bootstrap.md)
4
5 Read it before writing any code.
6
7 ## Project Overview
8
9 fossilrepo is an omnibus-style installer for a self-hosted Fossil forge. Django+HTMX management layer wrapping Fossil SCM server infrastructure with Caddy (SSL/routing), Litestream (S3 backups), and a sync bridge to GitHub/GitLab. Open source (MIT).
10
11 ## Stack
12
13 - **Backend**: Django 5 (Python 3.12+)
14 - **Frontend**: HTMX 2.0 + Alpine.js 3 + Tailwind CSS (CDN)
15 - **API**: Django views returning HTML (full pages + HTMX partials)
16 - **ORM**: Django ORM with `Tracking` and `BaseCoreModel` base classes
17 - **Auth**: Session-based (Django native, httpOnly cookies)
18 - **Permissions**: Group-based via `P` enum (`core/permissions.py`)
19 - **Jobs**: Celery + Redis
20 - **Database**: PostgreSQL 16
21 - **Linter**: Ruff (check + formaStack
22
23 - **Backend**: Django 5 (Python 3.12+)
24 - **Frontend**: HTMX 2.0 + Alpine.js 3 + Tailwind CSS (CDN)
25 - **API**: Django views returning HTML (full pages + HTMX partials)
26 - **ORM**: Django ORM with `Tracking` and `BaseCoreModel` base classes
27 - **Auth**: Session-based (Django native, httpOnly cookies)
28 - **Permissions**: Group-based via `P` enum (`core/permissions.py`)
29 - **Jobs**: Celery + Redis
30 - **Database**: PostgreSQL 16
31 - **Linter**: Ruff (check + fo—�
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,36 @@
1
+# Contributor Covenant Code of Conduct
2
+
3
+## Our Pledge
4
+
5
+We as members, contributors, and leaders pledge to make participation in our
6
+community a harassment-free experience for everyone, regardless of age, body
7
+size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+identity and expression, level of experience, education, socio-economic status,
9
+nationality, personal appearance, race, religion, or sexual identity
10
+and orientation.
11
+
12
+## Our Standards
13
+
14
+Examples of behavior that contributes to a positive environment:
15
+
16
+- Using welcoming and inclusive language
17
+- Being respectful of differing viewpoints and experiences
18
+- Gracefully accepting constructive criticism
19
+- Focusing on what is best for the community
20
+
21
+Examples of unacceptable behavior:
22
+
23
+- Trolling, insulting or derogatory comments, and personal or political attacks
24
+- Public or private harassment
25
+- Publishing others' private information without explicit permission
26
+- Other conduct which could reasonably be considered inappropriate
27
+
28
+## Enforcement
29
+
30
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
31
+reported to the project team at **[email protected]**. All complaints will be
32
+reviewed and investigated.
33
+
34
+## Attribution
35
+
36
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.0.
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,36 @@
1 # Contributor Covenant Code of Conduct
2
3 ## Our Pledge
4
5 We as members, contributors, and leaders pledge to make participation in our
6 community a harassment-free experience for everyone, regardless of age, body
7 size, visible or invisible disability, ethnicity, sex characteristics, gender
8 identity and expression, level of experience, education, socio-economic status,
9 nationality, personal appearance, race, religion, or sexual identity
10 and orientation.
11
12 ## Our Standards
13
14 Examples of behavior that contributes to a positive environment:
15
16 - Using welcoming and inclusive language
17 - Being respectful of differing viewpoints and experiences
18 - Gracefully accepting constructive criticism
19 - Focusing on what is best for the community
20
21 Examples of unacceptable behavior:
22
23 - Trolling, insulting or derogatory comments, and personal or political attacks
24 - Public or private harassment
25 - Publishing others' private information without explicit permission
26 - Other conduct which could reasonably be considered inappropriate
27
28 ## Enforcement
29
30 Instances of abusive, harassing, or otherwise unacceptable behavior may be
31 reported to the project team at **[email protected]**. All complaints will be
32 reviewed and investigated.
33
34 ## Attribution
35
36 This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.0.
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -0,0 +1,32 @@
1
+# Contributing to Fossilrepo django + htmx
2
+
3
+Thank you for your interest in contributing!
4
+
5
+## Getting Started
6
+
7
+1. Fork the repository
8
+2. Clone your fork
9
+3. Run `docker compose up -d` (or see README.md for stack-specific setup)
10
+4. Create a feature branch from `main`
11
+
12
+## Development Process
13
+
14
+1. Pick an issue from the project board
15
+2. Comment your plan on the issue before starting
16
+3. Create a branch: `feature/issue-number-description` or `fix/issue-number-description`
17
+4. Make your changes following `bootstrap.md` conventions
18
+5. Write or update tests
19
+6. Run lint and tests (see README.md for commands)
20
+7. Submit a pull request
21
+
22
+## Code Style
23
+
24
+See `bootstrap.md` for conventions. Run the linter before committing.
25
+
26
+## Testing
27
+
28
+All new features need tests. All bug fixes need regression tests. Tests must use a real database — never mock.
29
+
30
+## Questions?
31
+
32
+Open an issue or start a discussion in this repository.
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -0,0 +1,32 @@
1 # Contributing to Fossilrepo django + htmx
2
3 Thank you for your interest in contributing!
4
5 ## Getting Started
6
7 1. Fork the repository
8 2. Clone your fork
9 3. Run `docker compose up -d` (or see README.md for stack-specific setup)
10 4. Create a feature branch from `main`
11
12 ## Development Process
13
14 1. Pick an issue from the project board
15 2. Comment your plan on the issue before starting
16 3. Create a branch: `feature/issue-number-description` or `fix/issue-number-description`
17 4. Make your changes following `bootstrap.md` conventions
18 5. Write or update tests
19 6. Run lint and tests (see README.md for commands)
20 7. Submit a pull request
21
22 ## Code Style
23
24 See `bootstrap.md` for conventions. Run the linter before committing.
25
26 ## Testing
27
28 All new features need tests. All bug fixes need regression tests. Tests must use a real database — never mock.
29
30 ## Questions?
31
32 Open an issue or start a discussion in this repository.
+24
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,24 @@
1
+FROM python:3.12-slim-bookworm
2
+
3
+RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ postgresql-client ca-certificates && \
5
+ rm -rf /var/lib/apt/lists/*
6
+
7
+RUN pip install --no-cache-dir uv
8
+
9
+WORKDIR /app
10
+
11
+COPY pyproject.toml ./
12
+RUN uv pip install --system --no-cache -r pyproject.toml
13
+
14
+COPY . .
15
+
16
+RUN python manage.py collectstatic --noinput 2>/dev/null || true
17
+
18
+ENV PYTHONUNBUFFERED=1
19
+ENV PYTHONDONTWRITEBYTECODE=1
20
+ENV DJANGO_SETTINGS_MODULE=config.settings
21
+
22
+EXPOSE 8000
23
+
24
+CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120"]
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,24 @@
1 FROM python:3.12-slim-bookworm
2
3 RUN apt-get update && apt-get install -y --no-install-recommends \
4 postgresql-client ca-certificates && \
5 rm -rf /var/lib/apt/lists/*
6
7 RUN pip install --no-cache-dir uv
8
9 WORKDIR /app
10
11 COPY pyproject.toml ./
12 RUN uv pip install --system --no-cache -r pyproject.toml
13
14 COPY . .
15
16 RUN python manage.py collectstatic --noinput 2>/dev/null || true
17
18 ENV PYTHONUNBUFFERED=1
19 ENV PYTHONDONTWRITEBYTECODE=1
20 ENV DJANGO_SETTINGS_MODULE=config.settings
21
22 EXPOSE 8000
23
24 CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120"]
+21
--- a/LICENSE
+++ b/LICENSE
@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2026 Conflict LLC
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.
--- a/LICENSE
+++ b/LICENSE
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/LICENSE
+++ b/LICENSE
@@ -0,0 +1,21 @@
1 MIT License
2
3 Copyright (c) 2026 Conflict LLC
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in all
13 copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 SOFTWARE.
+62
--- a/Makefile
+++ b/Makefile
@@ -0,0 +1,62 @@
1
+.PHONY: up down build logs shell migrate migrations seed test lint check superuser ps
2
+
3
+up:
4
+ docker compose up -d
5
+
6
+down:
7
+ docker compose down
8
+
9
+build:
10
+ docker compose up -d --build
11
+
12
+restart:
13
+ docker compose restart backend
14
+
15
+logs:
16
+ docker compose logs -f backend
17
+
18
+shell:
19
+ docker compose exec backend bash
20
+
21
+migrate:
22
+ docker compose exec backend python manage.py migrate
23
+
24
+migrations:
25
+ docker compose exec backend python manage.py makemigrations $(app)
26
+
27
+seed:
28
+ifdef flush
29
+ docker compose exec backend python manage.py seed --flush
30
+else
31
+ docker compose exec backend python manage.py seed
32
+endif
33
+
34
+test:
35
+ docker compose exec backend python -m pytest --cov --cov-report=term-missing -v
36
+
37
+lint:
38
+ docker compose exec backend python -m ruff check . && docker compose exec backend python -m ruff format --check .
39
+
40
+check: lint test
41
+
42
+superuser:
43
+ docker compose exec backend python manage.py createsuperuser
44
+
45
+ps:
46
+ docker compose ps
47
+
48
+# Local dev (no Docker)
49
+local-install:
50
+ uv sync --all-extras
51
+
52
+local-migrate:
53
+ uv run python manage.py migrate
54
+
55
+local-run:
56
+ uv run python manage.py runserver
57
+
58
+local-test:
59
+ uv run python -m pytest --cov --cov-report=term-missing -v
60
+
61
+local-lint:
62
+ uv run ruff check . && uv run ruff format --check .
--- a/Makefile
+++ b/Makefile
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/Makefile
+++ b/Makefile
@@ -0,0 +1,62 @@
1 .PHONY: up down build logs shell migrate migrations seed test lint check superuser ps
2
3 up:
4 docker compose up -d
5
6 down:
7 docker compose down
8
9 build:
10 docker compose up -d --build
11
12 restart:
13 docker compose restart backend
14
15 logs:
16 docker compose logs -f backend
17
18 shell:
19 docker compose exec backend bash
20
21 migrate:
22 docker compose exec backend python manage.py migrate
23
24 migrations:
25 docker compose exec backend python manage.py makemigrations $(app)
26
27 seed:
28 ifdef flush
29 docker compose exec backend python manage.py seed --flush
30 else
31 docker compose exec backend python manage.py seed
32 endif
33
34 test:
35 docker compose exec backend python -m pytest --cov --cov-report=term-missing -v
36
37 lint:
38 docker compose exec backend python -m ruff check . && docker compose exec backend python -m ruff format --check .
39
40 check: lint test
41
42 superuser:
43 docker compose exec backend python manage.py createsuperuser
44
45 ps:
46 docker compose ps
47
48 # Local dev (no Docker)
49 local-install:
50 uv sync --all-extras
51
52 local-migrate:
53 uv run python manage.py migrate
54
55 local-run:
56 uv run python manage.py runserver
57
58 local-test:
59 uv run python -m pytest --cov --cov-report=term-missing -v
60
61 local-lint:
62 uv run ruff check . && uv run ruff format --check .
+13
--- a/README.md
+++ b/README.md
@@ -0,0 +1,13 @@
1
+# asn't changDjango + HTMX
2
+
3
+Server-rendered Django with HTMX for dynamic behavior and Alpine.js for lightweight client state. Tailwind CSS for styling. Choose this for content-heavy CRUD, admin-centric tools, and apps where server-rendered sted Fossil forge with a modern web interface.**
4
+
5
+Fossilrepo wraps [Fossil SCM](https://fossil-scm.org) with a Django + HTMX management layer, replacing Fossil's built-in web UI with a GitHub/GitLab-caliber experience while preserving everything that makes Fossil unique: single-file repos, built-in wiki, tickets, forum, and technotes.
6
+
7
+## Why Fossilrepo?
8
+
9
+Fossil is the most underrated version control system. Every repository is a single SQLite file containing your code, wiki, tickets, forum, and technotes. No external services, no complex setup. But its web UI hasn't changed since 1998.
10
+
11
+Fossilrepo fixes that. You get:
12
+
13
+- A modern dark/light UI built with Django, HTMX, Al
--- a/README.md
+++ b/README.md
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/README.md
+++ b/README.md
@@ -0,0 +1,13 @@
1 # asn't changDjango + HTMX
2
3 Server-rendered Django with HTMX for dynamic behavior and Alpine.js for lightweight client state. Tailwind CSS for styling. Choose this for content-heavy CRUD, admin-centric tools, and apps where server-rendered sted Fossil forge with a modern web interface.**
4
5 Fossilrepo wraps [Fossil SCM](https://fossil-scm.org) with a Django + HTMX management layer, replacing Fossil's built-in web UI with a GitHub/GitLab-caliber experience while preserving everything that makes Fossil unique: single-file repos, built-in wiki, tickets, forum, and technotes.
6
7 ## Why Fossilrepo?
8
9 Fossil is the most underrated version control system. Every repository is a single SQLite file containing your code, wiki, tickets, forum, and technotes. No external services, no complex setup. But its web UI hasn't changed since 1998.
10
11 Fossilrepo fixes that. You get:
12
13 - A modern dark/light UI built with Django, HTMX, Al
+29
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -0,0 +1,29 @@
1
+# Security Policy
2
+
3
+## Reporting a Vulnerability
4
+
5
+If you discover a security vulnerability in Fossilrepo, please report it responsibly.
6
+
7
+**Do nInstead, et open a public issue.**
8
+
9
+Email **[email protected]** with:
10
+
11
+- Description of the vulnerability
12
+- Steps to reproduce
13
+- Potential impact
14
+- Suggested fix (if any)
15
+
16
+We will acknowledge your report within 48 hours and aim to release a fix within 7 days for critical issues.
17
+
18
+## Supported Versions
19
+
20
+| Version | Supported |
21
+| -----Best Practiceslocalauth`)
22
+
23
+### Deployment:
24
+out one when `DEBUG=Facredentials (database, MinIO, session secret)
25
+- Use HTTPS in production
26
+- Set `NODE_ENV=pr_ORIGINS` and `CSRF_TRUSTED_ORIGINS` to your domain
27
+- Review Constance settings in Django admin (OAuth secrets, S3 credentials)
28
+- Use a reverse proxy (Caddy/nginx) for SSL termination
29
+- Keep t
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -0,0 +1,29 @@
1 # Security Policy
2
3 ## Reporting a Vulnerability
4
5 If you discover a security vulnerability in Fossilrepo, please report it responsibly.
6
7 **Do nInstead, et open a public issue.**
8
9 Email **[email protected]** with:
10
11 - Description of the vulnerability
12 - Steps to reproduce
13 - Potential impact
14 - Suggested fix (if any)
15
16 We will acknowledge your report within 48 hours and aim to release a fix within 7 days for critical issues.
17
18 ## Supported Versions
19
20 | Version | Supported |
21 | -----Best Practiceslocalauth`)
22
23 ### Deployment:
24 out one when `DEBUG=Facredentials (database, MinIO, session secret)
25 - Use HTTPS in production
26 - Set `NODE_ENV=pr_ORIGINS` and `CSRF_TRUSTED_ORIGINS` to your domain
27 - Review Constance settings in Django admin (OAuth secrets, S3 credentials)
28 - Use a reverse proxy (Caddy/nginx) for SSL termination
29 - Keep t

No diff available

--- a/auth1/apps.py
+++ b/auth1/apps.py
@@ -0,0 +1,7 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class Auth1Config(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "auth1"
7
+ verbose_name = "Authentication"
--- a/auth1/apps.py
+++ b/auth1/apps.py
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
--- a/auth1/apps.py
+++ b/auth1/apps.py
@@ -0,0 +1,7 @@
1 from django.apps import AppConfig
2
3
4 class Auth1Config(AppConfig):
5 default_auto_field = "django.db.models.BigAutoField"
6 name = "auth1"
7 verbose_name = "Authentication"
--- a/auth1/forms.py
+++ b/auth1/forms.py
@@ -0,0 +1,20 @@
1
+from django import forms
2
+from django.contrib.auth.forms import AuthenticationForm
3
+
4
+
5
+class LoginForm(AuthenticationForm):
6
+ username = forms.CharField(
7
+ widget=forms.TextInput(
8
+ attrs={
9
+ "class": "w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500rder-brand focus:ring-brand",
10
+ "placeholder": "Username",
11
+ "autofocus": True,
12
+ }
13
+ )
14
+ )
15
+ password = forms.CharField(
16
+ widget=forms.PasswordInput(
17
+ attrs={
18
+ "class": "w-full rounded-md border-gray-indigo-500 focus:ring-indigo-500rder-brand focus:ring-brand",
19
+ "placeholder": "Password",
20
+
--- a/auth1/forms.py
+++ b/auth1/forms.py
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/auth1/forms.py
+++ b/auth1/forms.py
@@ -0,0 +1,20 @@
1 from django import forms
2 from django.contrib.auth.forms import AuthenticationForm
3
4
5 class LoginForm(AuthenticationForm):
6 username = forms.CharField(
7 widget=forms.TextInput(
8 attrs={
9 "class": "w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500rder-brand focus:ring-brand",
10 "placeholder": "Username",
11 "autofocus": True,
12 }
13 )
14 )
15 password = forms.CharField(
16 widget=forms.PasswordInput(
17 attrs={
18 "class": "w-full rounded-md border-gray-indigo-500 focus:ring-indigo-500rder-brand focus:ring-brand",
19 "placeholder": "Password",
20

No diff available

--- a/auth1/tests.py
+++ b/auth1/tests.py
@@ -0,0 +1,46 @@
1
+import pytest
2
+from django.urls import reverse
3
+
4
+
5
+@pytest.mark.django_db
6
+class TestLogin:
7
+ def test_login_page_renders(self, client):
8
+ response = client.get(reverse("auth1:login"))
9
+ assert response.status_code == 200
10
+ assert b"Sign in" in response.content
11
+
12
+ def test_login_success_redirects_to_dashboard(self, client, admin_user):
13
+ response = client.post(reverse("auth1:login"), {"username": "admin", "password": "testpass123"})
14
+ assert response.status_code == 302
15
+ assert response.url == reverse("dashboard")
16
+
17
+ def test_login_failure_shows_error(self, client, admin_user):
18
+ response = client.post(reverse("auth1:login"), {"username": "admin", "password": "wrong"})
19
+ assert response.status_code == 200
20
+ assert b"Invalid username or password" in response.content
21
+
22
+ def test_login_redirect_when_already_authenticated(self, admin_client):
23
+ response = admin_client.get(reverse("auth1:login"))
24
+ assert response.status_code == 302
25
+
26
+ def test_login_with_next_param(self, client, admin_user):
27
+ response = client.post(reverse("auth1:login") + "?next=/items/", {"username": "admin", "password": "testpass123"})
28
+ assert response.status_code == 302
29
+ assert response.url == "/items/"
30
+
31
+
32
+@pytest.mark.django_db
33
+class TestLogout:
34
+ def test_logout_redirects_to_login(self, admin_client):
35
+ response = admin_client.post(reverse("auth1:logout"))
36
+ assert response.status_code == 302
37
+ assert reverse("auth1:login") in response.url
38
+
39
+ def test_logout_clears_session(self, admin_client):
40
+ admin_client.post(reverse("auth1:logout"))
41
+ response = admin_client.get(reverse("dashboard"))
42
+ assert response.status_code == 302 # redirected to login
43
+
44
+ def test_logout_rejects_get(self, admin_client):
45
+ response = admin_client.get(reverse("auth1:logout"))
46
+ assert response.status_code == 405
--- a/auth1/tests.py
+++ b/auth1/tests.py
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/auth1/tests.py
+++ b/auth1/tests.py
@@ -0,0 +1,46 @@
1 import pytest
2 from django.urls import reverse
3
4
5 @pytest.mark.django_db
6 class TestLogin:
7 def test_login_page_renders(self, client):
8 response = client.get(reverse("auth1:login"))
9 assert response.status_code == 200
10 assert b"Sign in" in response.content
11
12 def test_login_success_redirects_to_dashboard(self, client, admin_user):
13 response = client.post(reverse("auth1:login"), {"username": "admin", "password": "testpass123"})
14 assert response.status_code == 302
15 assert response.url == reverse("dashboard")
16
17 def test_login_failure_shows_error(self, client, admin_user):
18 response = client.post(reverse("auth1:login"), {"username": "admin", "password": "wrong"})
19 assert response.status_code == 200
20 assert b"Invalid username or password" in response.content
21
22 def test_login_redirect_when_already_authenticated(self, admin_client):
23 response = admin_client.get(reverse("auth1:login"))
24 assert response.status_code == 302
25
26 def test_login_with_next_param(self, client, admin_user):
27 response = client.post(reverse("auth1:login") + "?next=/items/", {"username": "admin", "password": "testpass123"})
28 assert response.status_code == 302
29 assert response.url == "/items/"
30
31
32 @pytest.mark.django_db
33 class TestLogout:
34 def test_logout_redirects_to_login(self, admin_client):
35 response = admin_client.post(reverse("auth1:logout"))
36 assert response.status_code == 302
37 assert reverse("auth1:login") in response.url
38
39 def test_logout_clears_session(self, admin_client):
40 admin_client.post(reverse("auth1:logout"))
41 response = admin_client.get(reverse("dashboard"))
42 assert response.status_code == 302 # redirected to login
43
44 def test_logout_rejects_get(self, admin_client):
45 response = admin_client.get(reverse("auth1:logout"))
46 assert response.status_code == 405
--- a/auth1/urls.py
+++ b/auth1/urls.py
@@ -0,0 +1,10 @@
1
+from django.urls import path
2
+
3
+from . import views
4
+
5
+app_name = "auth1"
6
+
7
+urlpatterns = [
8
+ path("login/", views.login_view, name="login"),
9
+ path("logout/", views.logout_view, name="logout"),
10
+]
--- a/auth1/urls.py
+++ b/auth1/urls.py
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
--- a/auth1/urls.py
+++ b/auth1/urls.py
@@ -0,0 +1,10 @@
1 from django.urls import path
2
3 from . import views
4
5 app_name = "auth1"
6
7 urlpatterns = [
8 path("login/", views.login_view, name="login"),
9 path("logout/", views.logout_view, name="logout"),
10 ]
--- a/auth1/views.py
+++ b/auth1/views.py
@@ -0,0 +1,29 @@
1
+from django.contribessages
2
+fromshortcuts import redirect, render
3
+from django.views.decorators.http import require_POST
4
+from django_ratelimit.decorators import ratelimit
5
+
6
+from .forms import LoginForm
7
+
8
+
9
+@ratelimit(key="ip", rate="10/m", block=True)
10
+def login_view(request):
11
+ if request.user.is_authenticated:
12
+ return redirect("dashboard")
13
+
14
+ if request.method == "POST":
15
+ form = LoginForm(request, data=request.POST)
16
+ if form.is_valid():
17
+ login(request, form.get_user())
18
+ next_url = request.GET.get("next", "dashboard")
19
+ return redirect(next_url)
20
+ else:
21
+ form = LoginForm()
22
+
23
+ return render(request, "auth1/login.html", {"form": form})
24
+
25
+
26
+@require_POST
27
+def logout_view(request):
28
+ logout(request)
29
+ retu
--- a/auth1/views.py
+++ b/auth1/views.py
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/auth1/views.py
+++ b/auth1/views.py
@@ -0,0 +1,29 @@
1 from django.contribessages
2 fromshortcuts import redirect, render
3 from django.views.decorators.http import require_POST
4 from django_ratelimit.decorators import ratelimit
5
6 from .forms import LoginForm
7
8
9 @ratelimit(key="ip", rate="10/m", block=True)
10 def login_view(request):
11 if request.user.is_authenticated:
12 return redirect("dashboard")
13
14 if request.method == "POST":
15 form = LoginForm(request, data=request.POST)
16 if form.is_valid():
17 login(request, form.get_user())
18 next_url = request.GET.get("next", "dashboard")
19 return redirect(next_url)
20 else:
21 form = LoginForm()
22
23 return render(request, "auth1/login.html", {"form": form})
24
25
26 @require_POST
27 def logout_view(request):
28 logout(request)
29 retu
+243
--- a/bootstrap.md
+++ b/bootstrap.md
@@ -0,0 +1,243 @@
1
+# Fossilrepo Django + HTMX -- Bootstrap
2
+
3
+This is the primary conventions document. All agent shims (`CLAUDE.md`, `AGENTS.md`) point here.
4
+
5
+An agent given this document and a business requirement should be able to generate correct, idiomatic code without exploring the codebase.
6
+
7
+---
8
+
9
+## What's Already Built
10
+
11
+| Layer | What's there |
12
+|---|---|
13
+| Auth | Session-based auth (auth1), login/logout views with templates, rate limiting |
14
+| Data | Postgres 16, `Tracking` base model (version, created/updated/deleted by+at, soft deletes, history) |
15
+| API | Django views returning HTML (full pages + HTMX partials) |
16
+| Permissions | Group-based via `P` enum, checked in every view |
17
+| Async | Celery worker + beat, Redis broker |
18
+| Admin | Django Admin with `BaseCoreAdmin` (import/export, tracking fields) |
19
+| Infra | Docker Compose: postgres, redis, celery-worker, celery-beat, mailpit |
20
+| CI | GitHub Actions: lint (Ruff) + tests (Postgres + Redis services) |
21
+| Seed | `python manage.py seed` creates admin/viewer users, sample items |
22
+| Frontend | HTMX 2.0 + Alpine.js 3 + Tailwind CSS, server-rendered templates |
23
+
24
+---
25
+
26
+## App Structure
27
+
28
+| App | Purpose |
29
+|---|---|
30
+| `config` | Django settings, URLs, Celery configuration |
31
+| `core` | Base models (Tracking, BaseCoreModel), admin (BaseCoreAdmin), permissions (P enum), middleware |
32
+| `auth1` | Session-based authentication: login/logout views with rate limiting |
33
+| `organization` | Organization + OrganizationMember models |
34
+| `items` | Example CRUD domain demonstrating all patterns |
35
+| `testdata` | `seed` management command for development data |
36
+
37
+---
38
+
39
+## Conventions
40
+
41
+### Models
42
+
43
+All business models inherit from one of:
44
+
45
+**`Tracking`** (abstract) — audit trails:
46
+```python
47
+from core.models import Tracking
48
+
49
+class Invoice(Tracking):
50
+ amount = models.DecimalField(...)
51
+```
52
+Provides: `version` (auto-increments), `created_at/by`, `updated_at/by`, `deleted_at/by`, `history` (simple_history).
53
+
54
+**`BaseCoreModel(Tracking)`** (abstract) — named entities:
55
+```python
56
+from core.models import BaseCoreModel
57
+
58
+class Item(BaseCoreModel):
59
+ price = models.DecimalField(...)
60
+```
61
+Adds: `guid` (UUID), `name`, `slug` (auto-generated, unique), `description`.
62
+
63
+**Soft deletes:** call `obj.soft_delete(user=request.user)`, never `.delete()`.
64
+
65
+**ActiveManager:** Use `objects` (excludes deleted) for queries, `all_objects` for admin.
66
+
67
+---
68
+
69
+### Views (HTMX Pattern)
70
+
71
+Views return full pages for normal requests, HTMX partials for `HX-Request`:
72
+
73
+```python
74
+@login_required
75
+def item_list(request):
76
+ P.ITEM_VIEW.check(request.user)
77
+ items = Item.objects.all()
78
+
79
+ if request.headers.get("HX-Request"):
80
+ return render(request, "items/partials/item_table.html", {"items": items})
81
+
82
+ return render(request, "items/item_list.html", {"items": items})
83
+```
84
+
85
+**URL patterns** follow CRUD convention:
86
+```python
87
+urlpatterns = [
88
+ path("", views.item_list, name="list"),
89
+ path("create/", views.item_create, name="create"),
90
+ path("<slug:slug>/", views.item_detail, name="detail"),
91
+ path("<slug:slug>/edit/", views.item_update, name="update"),
92
+ path("<slug:slug>/delete/", views.item_delete, name="delete"),
93
+]
94
+```
95
+
96
+---
97
+
98
+### Permissions
99
+
100
+Group-based. Never user-based. Checked in every view.
101
+
102
+```python
103
+from core.permissions import P
104
+
105
+P.ITEM_VIEW.check(request.user) # raises PermissionDenied if denied
106
+P.ITEM_ADD.check(request.user, raise_error=False) # returns False instead
107
+```
108
+
109
+Template guards:
110
+```html
111
+{% if perms.items.view_item %}
112
+ <a href="{% url 'items:list' %}">Items</a>
113
+{% endif %}
114
+```
115
+
116
+---
117
+
118
+### Admin
119
+
120
+All admin classes inherit `BaseCoreAdmin`:
121
+```python
122
+from core.admin import BaseCoreAdmin
123
+
124
+@admin.register(Item)
125
+class ItemAdmin(BaseCoreAdmin):
126
+ list_display = ("name", "slug", "price", "created_at")
127
+ search_fields = ("name", "slug")
128
+```
129
+
130
+`BaseCoreAdmin` provides: audit fields as readonly, `created_by`/`updated_by` auto-set, import/export.
131
+
132
+---
133
+
134
+### Templates
135
+
136
+- `base.html` — layout with HTMX, Alpine.js, Tailwind CSS, CSRF injection, messages
137
+- `includes/nav.html` — navigation bar with permission guards
138
+- `{app}/partials/*.html` — HTMX partial templates (no `{% extends %}`)
139
+- CSRF token sent with all HTMX requests via `htmx:configRequest` event
140
+
141
+Alpine.js patterns for client-side interactivity:
142
+```html
143
+<div x-data="{ open: false }">
144
+ <button @click="open = !open">Toggle</button>
145
+ <div x-show="open" x-transition>Content</div>
146
+</div>
147
+```
148
+
149
+---
150
+
151
+### Tests
152
+
153
+pytest + real Postgres. Assert against database state.
154
+
155
+```python
156
+@pytest.mark.django_db
157
+class TestItemCreate:
158
+ def test_create_saves_item(self, admin_client, admin_user):
159
+ response = admin_client.post(reverse("items:create"), {
160
+ "name": "Widget", "price": "9.99", ...
161
+ })
162
+ assert response.status_code == 302
163
+ item = Item.objects.get(name="Widget")
164
+ assert item.created_by == admin_user
165
+
166
+ def test_create_denied_for_viewer(self, viewer_client):
167
+ response = viewer_client.get(reverse("items:create"))
168
+ assert response.status_code == 403
169
+```
170
+
171
+Both allowed AND denied permission cases for every endpoint.
172
+
173
+---
174
+
175
+### Code Style
176
+
177
+| Tool | Config |
178
+|------|--------|
179
+| Ruff (lint + format) | `pyproject.toml`, line length 140 |
180
+| Import sorting | Ruff isort rules |
181
+| Python version | 3.12+ |
182
+
183
+Run `ruff check .` and `ruff format --check .` before committing.
184
+
185
+---
186
+
187
+## Adding a New App
188
+
189
+```bash
190
+# 1. Create the app
191
+python manage.py startapp myapp
192
+
193
+# 2. Add to INSTALLED_APPS in config/settings.py
194
+
195
+# 3. Create models inheriting Tracking or BaseCoreModel
196
+
197
+# 4. Create migrations
198
+python manage.py makemigrations
199
+
200
+# 5. Create admin (inherit BaseCoreAdmin)
201
+
202
+# 6. Create views with @login_required + P.PERMISSION.check()
203
+
204
+# 7. Create URL patterns (list, detail, create, update, delete)
205
+
206
+# 8. Create templates (full page + HTMX partials)
207
+
208
+# 9. Add permission entries to core/permissions.py P enum
209
+
210
+# 10. Write tests (allowed + denied)
211
+python -m pytest --cov -v
212
+```
213
+
214
+---
215
+
216
+## Ports (local Docker)
217
+
218
+| Service | URL |
219
+|---|---|
220
+| Django | http://localhost:8000 |
221
+| Django Admin | http://localhost:8000/admin/ |
222
+| Health | http://localhost:8000/health/ |
223
+| Mailpit | http://localhost:8025 |
224
+| Postgres | localhost:5432 |
225
+| Redis | localhost:6379 |
226
+
227
+---
228
+
229
+## Common Commands
230
+
231
+```bash
232
+make up # Start the stack
233
+make build # Build and start
234
+make down # Stop the stack
235
+make migrate # Run migrations
236
+make migrations # Create migrations
237
+make seed # Load dev fixtures
238
+make test # Run tests with coverage
239
+make lint # Run Ruff check + format
240
+make superuser # Create Django superuser
241
+make shell # Shell into container
242
+make logs # Tail Django logs
243
+```
--- a/bootstrap.md
+++ b/bootstrap.md
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/bootstrap.md
+++ b/bootstrap.md
@@ -0,0 +1,243 @@
1 # Fossilrepo Django + HTMX -- Bootstrap
2
3 This is the primary conventions document. All agent shims (`CLAUDE.md`, `AGENTS.md`) point here.
4
5 An agent given this document and a business requirement should be able to generate correct, idiomatic code without exploring the codebase.
6
7 ---
8
9 ## What's Already Built
10
11 | Layer | What's there |
12 |---|---|
13 | Auth | Session-based auth (auth1), login/logout views with templates, rate limiting |
14 | Data | Postgres 16, `Tracking` base model (version, created/updated/deleted by+at, soft deletes, history) |
15 | API | Django views returning HTML (full pages + HTMX partials) |
16 | Permissions | Group-based via `P` enum, checked in every view |
17 | Async | Celery worker + beat, Redis broker |
18 | Admin | Django Admin with `BaseCoreAdmin` (import/export, tracking fields) |
19 | Infra | Docker Compose: postgres, redis, celery-worker, celery-beat, mailpit |
20 | CI | GitHub Actions: lint (Ruff) + tests (Postgres + Redis services) |
21 | Seed | `python manage.py seed` creates admin/viewer users, sample items |
22 | Frontend | HTMX 2.0 + Alpine.js 3 + Tailwind CSS, server-rendered templates |
23
24 ---
25
26 ## App Structure
27
28 | App | Purpose |
29 |---|---|
30 | `config` | Django settings, URLs, Celery configuration |
31 | `core` | Base models (Tracking, BaseCoreModel), admin (BaseCoreAdmin), permissions (P enum), middleware |
32 | `auth1` | Session-based authentication: login/logout views with rate limiting |
33 | `organization` | Organization + OrganizationMember models |
34 | `items` | Example CRUD domain demonstrating all patterns |
35 | `testdata` | `seed` management command for development data |
36
37 ---
38
39 ## Conventions
40
41 ### Models
42
43 All business models inherit from one of:
44
45 **`Tracking`** (abstract) — audit trails:
46 ```python
47 from core.models import Tracking
48
49 class Invoice(Tracking):
50 amount = models.DecimalField(...)
51 ```
52 Provides: `version` (auto-increments), `created_at/by`, `updated_at/by`, `deleted_at/by`, `history` (simple_history).
53
54 **`BaseCoreModel(Tracking)`** (abstract) — named entities:
55 ```python
56 from core.models import BaseCoreModel
57
58 class Item(BaseCoreModel):
59 price = models.DecimalField(...)
60 ```
61 Adds: `guid` (UUID), `name`, `slug` (auto-generated, unique), `description`.
62
63 **Soft deletes:** call `obj.soft_delete(user=request.user)`, never `.delete()`.
64
65 **ActiveManager:** Use `objects` (excludes deleted) for queries, `all_objects` for admin.
66
67 ---
68
69 ### Views (HTMX Pattern)
70
71 Views return full pages for normal requests, HTMX partials for `HX-Request`:
72
73 ```python
74 @login_required
75 def item_list(request):
76 P.ITEM_VIEW.check(request.user)
77 items = Item.objects.all()
78
79 if request.headers.get("HX-Request"):
80 return render(request, "items/partials/item_table.html", {"items": items})
81
82 return render(request, "items/item_list.html", {"items": items})
83 ```
84
85 **URL patterns** follow CRUD convention:
86 ```python
87 urlpatterns = [
88 path("", views.item_list, name="list"),
89 path("create/", views.item_create, name="create"),
90 path("<slug:slug>/", views.item_detail, name="detail"),
91 path("<slug:slug>/edit/", views.item_update, name="update"),
92 path("<slug:slug>/delete/", views.item_delete, name="delete"),
93 ]
94 ```
95
96 ---
97
98 ### Permissions
99
100 Group-based. Never user-based. Checked in every view.
101
102 ```python
103 from core.permissions import P
104
105 P.ITEM_VIEW.check(request.user) # raises PermissionDenied if denied
106 P.ITEM_ADD.check(request.user, raise_error=False) # returns False instead
107 ```
108
109 Template guards:
110 ```html
111 {% if perms.items.view_item %}
112 <a href="{% url 'items:list' %}">Items</a>
113 {% endif %}
114 ```
115
116 ---
117
118 ### Admin
119
120 All admin classes inherit `BaseCoreAdmin`:
121 ```python
122 from core.admin import BaseCoreAdmin
123
124 @admin.register(Item)
125 class ItemAdmin(BaseCoreAdmin):
126 list_display = ("name", "slug", "price", "created_at")
127 search_fields = ("name", "slug")
128 ```
129
130 `BaseCoreAdmin` provides: audit fields as readonly, `created_by`/`updated_by` auto-set, import/export.
131
132 ---
133
134 ### Templates
135
136 - `base.html` — layout with HTMX, Alpine.js, Tailwind CSS, CSRF injection, messages
137 - `includes/nav.html` — navigation bar with permission guards
138 - `{app}/partials/*.html` — HTMX partial templates (no `{% extends %}`)
139 - CSRF token sent with all HTMX requests via `htmx:configRequest` event
140
141 Alpine.js patterns for client-side interactivity:
142 ```html
143 <div x-data="{ open: false }">
144 <button @click="open = !open">Toggle</button>
145 <div x-show="open" x-transition>Content</div>
146 </div>
147 ```
148
149 ---
150
151 ### Tests
152
153 pytest + real Postgres. Assert against database state.
154
155 ```python
156 @pytest.mark.django_db
157 class TestItemCreate:
158 def test_create_saves_item(self, admin_client, admin_user):
159 response = admin_client.post(reverse("items:create"), {
160 "name": "Widget", "price": "9.99", ...
161 })
162 assert response.status_code == 302
163 item = Item.objects.get(name="Widget")
164 assert item.created_by == admin_user
165
166 def test_create_denied_for_viewer(self, viewer_client):
167 response = viewer_client.get(reverse("items:create"))
168 assert response.status_code == 403
169 ```
170
171 Both allowed AND denied permission cases for every endpoint.
172
173 ---
174
175 ### Code Style
176
177 | Tool | Config |
178 |------|--------|
179 | Ruff (lint + format) | `pyproject.toml`, line length 140 |
180 | Import sorting | Ruff isort rules |
181 | Python version | 3.12+ |
182
183 Run `ruff check .` and `ruff format --check .` before committing.
184
185 ---
186
187 ## Adding a New App
188
189 ```bash
190 # 1. Create the app
191 python manage.py startapp myapp
192
193 # 2. Add to INSTALLED_APPS in config/settings.py
194
195 # 3. Create models inheriting Tracking or BaseCoreModel
196
197 # 4. Create migrations
198 python manage.py makemigrations
199
200 # 5. Create admin (inherit BaseCoreAdmin)
201
202 # 6. Create views with @login_required + P.PERMISSION.check()
203
204 # 7. Create URL patterns (list, detail, create, update, delete)
205
206 # 8. Create templates (full page + HTMX partials)
207
208 # 9. Add permission entries to core/permissions.py P enum
209
210 # 10. Write tests (allowed + denied)
211 python -m pytest --cov -v
212 ```
213
214 ---
215
216 ## Ports (local Docker)
217
218 | Service | URL |
219 |---|---|
220 | Django | http://localhost:8000 |
221 | Django Admin | http://localhost:8000/admin/ |
222 | Health | http://localhost:8000/health/ |
223 | Mailpit | http://localhost:8025 |
224 | Postgres | localhost:5432 |
225 | Redis | localhost:6379 |
226
227 ---
228
229 ## Common Commands
230
231 ```bash
232 make up # Start the stack
233 make build # Build and start
234 make down # Stop the stack
235 make migrate # Run migrations
236 make migrations # Create migrations
237 make seed # Load dev fixtures
238 make test # Run tests with coverage
239 make lint # Run Ruff check + format
240 make superuser # Create Django superuser
241 make shell # Shell into container
242 make logs # Tail Django logs
243 ```

No diff available

--- a/config/celery.py
+++ b/config/celery.py
@@ -0,0 +1,9 @@
1
+import os
2
+
3
+from celery import Celery
4
+
5
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+
7
+app = Celery("fossilrepo")
8
+app.config_from_object("django.conf:settings", namespace="CELERY")
9
+app.autodiscover_tasks()
--- a/config/celery.py
+++ b/config/celery.py
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
--- a/config/celery.py
+++ b/config/celery.py
@@ -0,0 +1,9 @@
1 import os
2
3 from celery import Celery
4
5 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
7 app = Celery("fossilrepo")
8 app.config_from_object("django.conf:settings", namespace="CELERY")
9 app.autodiscover_tasks()
--- a/config/settings.py
+++ b/config/settings.py
@@ -0,0 +1,161 @@
1
+import logging
2
+import os
3
+from pathlib import Path
4
+
5
+from django.core.exceptions import ImproperlyConfigured
6
+
7
+logger = logging.getLogger(__name__)
8
+
9
+VERSION = "0.1.0"
10
+
11
+BASE_DIR = Path(__file__).resolve().parent.parent
12
+
13
+
14
+def env_str(name: str, default: str | None = None) -> str | None:
15
+ return os.getenv(name, default)
16
+
17
+
18
+def env_bool(name: str, default: bool = False) -> bool:
19
+ return os.getenv(name, str(default)).lower() in ("true", "1", "yes")
20
+
21
+
22
+def env_int(name: str, default: int = 0) -> int:
23
+ return int(os.getenv(name, str(default)))
24
+
25
+
26
+# --- Security ---
27
+
28
+SECRET_KEY = env_str("DJANGO_SECRET_KEY", "change-me-in-production")
29
+DEBUG = env_bool("DJANGO_DEBUG", False)
30
+ALLOWED_HOSTS = [h.strip() for h in env_str("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(",")]
31
+
32
+if not DEBUG and SECRET_KEY == "change-me-in-production":
33
+ raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set to a unique, unpredictable value when DEBUG is False.")
34
+
35
+# --- Application ---
36
+
37
+ROOT_URLCONF = "config.urls"
38
+WSGI_APPLICATION = "config.wsgi.application"
39
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
40
+LOGIN_URL = "/auth/login/"
41
+LOGIN_REDIRECT_URL = "/"
42
+LOGOUT_REDIRECT_URL = "/auth/login/"
43
+
44
+INSTALLED_APPS = [
45
+ "django.contrib.admin",
46
+ "django.contrib.auth",
47
+ "django.contrib.contenttypes",
48
+ "django.contrib.sessions",
49
+ "django.contrib.messages",
50
+ "django.contrib.staticfiles",
51
+ ngo.contrib.humanize",
52
+ # Third-party
53
+ "import_export",
54
+ "simple_history",
55
+ "django_celery_results",
56
+ "django_celery_beat",
57
+ "corsheaders",
58
+ "constance",
59
+ "constance.backends.database",
60
+ # Project apps
61
+ "core",
62
+ "auth1",
63
+ "organitestdata",
64
+]
65
+
66
+MIDDLEWARE = [
67
+ "corsheaders.middleware.CorsMiddleware",
68
+ "django.middleware.security.SecurityMiddleware",
69
+ "whitenoise.middleware.WhiteNoiseMiddleware",
70
+ "django.contrib.sessions.middleware.SessionMiddleware",
71
+ "django.middleware.common.CommonMiddleware",
72
+ "django.middleware.csrf.CsrfViewMiddleware",
73
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
74
+ "django.contrib.messages.middleware.MessageMiddleware",
75
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
76
+ "simple_history.middleware.HistoryRequestMiddleware",
77
+ "core.middleware.current_user.CurrentUserMiddleware",
78
+]
79
+
80
+TEMPLATES = [
81
+ {
82
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
83
+ "DIRS": [BASE_DIR / "templates"],
84
+ "APP_DIRS": True,
85
+ "OPTIONS": {
86
+ "context_processors": [
87
+ "django.template.context_processors.debug",
88
+ "django.template.context_processors.request",
89
+ "django.contrib.auth.context_processors.auth",
90
+ "django.contrib.messages.context_processors.messages",
91
+ UT_REDIRECT_URL = "/auth/login/"
92
+
93
+INSTALLED_APPS = [
94
+ "django.contrib.admin",
95
+ "django.contrib.auth",
96
+ "django.contrib.contenttypes",
97
+ "django.contrib.sessions",
98
+ "django.contrib.messages",
99
+ "django.contrib.staticfiles",
100
+ "django.contrib.humanize",
101
+ # Third-party
102
+ "import_export",
103
+ "simple_history",
104
+ "django_celery_results",
105
+ "django_celery_beat",
106
+ "corsheaders",
107
+ "constance",
108
+ "constance.backends.database",
109
+ # Project apps
110
+ "core",
111
+ "auth1",
112
+ "organization",
113
+ "items",
114
+ "projects",
115
+ "pages",
116
+ "fossil",
117
+ "testdata",
118
+]
119
+
120
+MIDDLEWARE = [
121
+ "corsheaders.middleware.CorsMiddleware",
122
+ "django.middleware.security.SecurityMiddleware",
123
+ "whitenoise.middleware.WhiteNoiseMiddleware",
124
+ "django.contrib.sessions.middleware.SessionMiddleware",
125
+ "django.middleware.common.CommonMiddleware",
126
+ "django.middleware.csrf.CsrfViewMiddleware",
127
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
128
+ "django.contrib.messages.middleware.MessageMiddleware",
129
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
130
+ "simple_history.middleware.HistoryRequestMiddleware",
131
+ "core.middleware.current_user.CurrentUserMiddleware",
132
+]
133
+
134
+TEMPLATES = [
135
+ {
136
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
137
+ "DIRS": [BASE_DIR / "templates"],
138
+ "APP_DIRS": True,
139
+ "OPTIONS": {
140
+ "context_processors": [
141
+ "django.template.context_processors.debug",
142
+ "django.template.context_processors.request",
143
+ "django.contrib.auth.context_processors.authmail ---
144
+
145
+EMAIL_BACKEND = env_str("DJANGO_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
146
+EMAIL_HOST = env_str("EMAIL_HOST", "localhost")
147
+EMAIL_PORT = env_int("EMAIL_PORT", 1025)
148
+DEFAULT_FROM_EMAIL = env_str("FROM_EMAIL", "[email protected]")
149
+
150
+# --- Celery ---
151
+
152
+CELERY_BROKER_URL = env_str("CELERY_BROKER", "redis://localhost:6379/0")
153
+CELERY_RESULT_BACKEND = "django-db"
154
+CELERY_TASK_TRACK_STARTED = True
155
+CELERY_TASK_TIME_LIMIT = 3600
156
+CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
157
+CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
158
+
159
+# --- CORS ---
160
+
161
+CORS_ALLOW_CRE
--- a/config/settings.py
+++ b/config/settings.py
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/config/settings.py
+++ b/config/settings.py
@@ -0,0 +1,161 @@
1 import logging
2 import os
3 from pathlib import Path
4
5 from django.core.exceptions import ImproperlyConfigured
6
7 logger = logging.getLogger(__name__)
8
9 VERSION = "0.1.0"
10
11 BASE_DIR = Path(__file__).resolve().parent.parent
12
13
14 def env_str(name: str, default: str | None = None) -> str | None:
15 return os.getenv(name, default)
16
17
18 def env_bool(name: str, default: bool = False) -> bool:
19 return os.getenv(name, str(default)).lower() in ("true", "1", "yes")
20
21
22 def env_int(name: str, default: int = 0) -> int:
23 return int(os.getenv(name, str(default)))
24
25
26 # --- Security ---
27
28 SECRET_KEY = env_str("DJANGO_SECRET_KEY", "change-me-in-production")
29 DEBUG = env_bool("DJANGO_DEBUG", False)
30 ALLOWED_HOSTS = [h.strip() for h in env_str("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(",")]
31
32 if not DEBUG and SECRET_KEY == "change-me-in-production":
33 raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set to a unique, unpredictable value when DEBUG is False.")
34
35 # --- Application ---
36
37 ROOT_URLCONF = "config.urls"
38 WSGI_APPLICATION = "config.wsgi.application"
39 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
40 LOGIN_URL = "/auth/login/"
41 LOGIN_REDIRECT_URL = "/"
42 LOGOUT_REDIRECT_URL = "/auth/login/"
43
44 INSTALLED_APPS = [
45 "django.contrib.admin",
46 "django.contrib.auth",
47 "django.contrib.contenttypes",
48 "django.contrib.sessions",
49 "django.contrib.messages",
50 "django.contrib.staticfiles",
51 ngo.contrib.humanize",
52 # Third-party
53 "import_export",
54 "simple_history",
55 "django_celery_results",
56 "django_celery_beat",
57 "corsheaders",
58 "constance",
59 "constance.backends.database",
60 # Project apps
61 "core",
62 "auth1",
63 "organitestdata",
64 ]
65
66 MIDDLEWARE = [
67 "corsheaders.middleware.CorsMiddleware",
68 "django.middleware.security.SecurityMiddleware",
69 "whitenoise.middleware.WhiteNoiseMiddleware",
70 "django.contrib.sessions.middleware.SessionMiddleware",
71 "django.middleware.common.CommonMiddleware",
72 "django.middleware.csrf.CsrfViewMiddleware",
73 "django.contrib.auth.middleware.AuthenticationMiddleware",
74 "django.contrib.messages.middleware.MessageMiddleware",
75 "django.middleware.clickjacking.XFrameOptionsMiddleware",
76 "simple_history.middleware.HistoryRequestMiddleware",
77 "core.middleware.current_user.CurrentUserMiddleware",
78 ]
79
80 TEMPLATES = [
81 {
82 "BACKEND": "django.template.backends.django.DjangoTemplates",
83 "DIRS": [BASE_DIR / "templates"],
84 "APP_DIRS": True,
85 "OPTIONS": {
86 "context_processors": [
87 "django.template.context_processors.debug",
88 "django.template.context_processors.request",
89 "django.contrib.auth.context_processors.auth",
90 "django.contrib.messages.context_processors.messages",
91 UT_REDIRECT_URL = "/auth/login/"
92
93 INSTALLED_APPS = [
94 "django.contrib.admin",
95 "django.contrib.auth",
96 "django.contrib.contenttypes",
97 "django.contrib.sessions",
98 "django.contrib.messages",
99 "django.contrib.staticfiles",
100 "django.contrib.humanize",
101 # Third-party
102 "import_export",
103 "simple_history",
104 "django_celery_results",
105 "django_celery_beat",
106 "corsheaders",
107 "constance",
108 "constance.backends.database",
109 # Project apps
110 "core",
111 "auth1",
112 "organization",
113 "items",
114 "projects",
115 "pages",
116 "fossil",
117 "testdata",
118 ]
119
120 MIDDLEWARE = [
121 "corsheaders.middleware.CorsMiddleware",
122 "django.middleware.security.SecurityMiddleware",
123 "whitenoise.middleware.WhiteNoiseMiddleware",
124 "django.contrib.sessions.middleware.SessionMiddleware",
125 "django.middleware.common.CommonMiddleware",
126 "django.middleware.csrf.CsrfViewMiddleware",
127 "django.contrib.auth.middleware.AuthenticationMiddleware",
128 "django.contrib.messages.middleware.MessageMiddleware",
129 "django.middleware.clickjacking.XFrameOptionsMiddleware",
130 "simple_history.middleware.HistoryRequestMiddleware",
131 "core.middleware.current_user.CurrentUserMiddleware",
132 ]
133
134 TEMPLATES = [
135 {
136 "BACKEND": "django.template.backends.django.DjangoTemplates",
137 "DIRS": [BASE_DIR / "templates"],
138 "APP_DIRS": True,
139 "OPTIONS": {
140 "context_processors": [
141 "django.template.context_processors.debug",
142 "django.template.context_processors.request",
143 "django.contrib.auth.context_processors.authmail ---
144
145 EMAIL_BACKEND = env_str("DJANGO_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
146 EMAIL_HOST = env_str("EMAIL_HOST", "localhost")
147 EMAIL_PORT = env_int("EMAIL_PORT", 1025)
148 DEFAULT_FROM_EMAIL = env_str("FROM_EMAIL", "[email protected]")
149
150 # --- Celery ---
151
152 CELERY_BROKER_URL = env_str("CELERY_BROKER", "redis://localhost:6379/0")
153 CELERY_RESULT_BACKEND = "django-db"
154 CELERY_TASK_TRACK_STARTED = True
155 CELERY_TASK_TIME_LIMIT = 3600
156 CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
157 CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
158
159 # --- CORS ---
160
161 CORS_ALLOW_CRE
--- a/config/urls.py
+++ b/config/urls.py
@@ -0,0 +1,196 @@
1
+import time
2
+from datetime import UTC, datetime
3
+
4
+from django.conf import settings
5
+from django.contrib import admin
6
+from django.http import HttpResponse, JsonResponse
7
+from django.urls import include, path
8
+from django.views.generic import RedirectView
9
+
10
+admin.site.site_header = settings.ADMIN_SITE_HEADER
11
+admin.site.site_title = settings.ADMIN_SITE_TITLE
12
+admin.site.index_title = "Welcome to Fossilrepo"
13
+
14
+_START_TIME = time.monotonic()
15
+
16
+
17
+def _uptime_str():
18
+ secs = int(time.monotonic() - _START_TIME)
19
+ h, rem = divmod(secs, 3600)
20
+ m, s = divmod(rem, 60)
21
+ if h:
22
+ return f"{h}h {m}m {s}s"
23
+ if m:
24
+ return f"{m}m {s}s"
25
+ return f"{s}s"
26
+
27
+
28
+def health_check(request):
29
+ from django.db import connection
30
+
31
+ try:
32
+ with connection.cursor() as cursor:
33
+ cursor.execute("SELECT 1")
34
+ db_ok = True
35
+ except Exception as e:
36
+ return JsonResponse(
37
+ {
38
+ "service": "fossilrepo-django-htmx",
39
+ "version": settings.VERSION,
40
+ "status": "error",
41
+ "uptime": _uptime_str(),
42
+ "timestamp": datetime.now(UTC).isoformat(),
43
+ "checks": {"database": "error", "detail": str(e)},
44
+ },
45
+ status=503,
46
+ )
47
+
48
+ return JsonResponse(
49
+ {
50
+ "service": "fossilrepo-django-htmx",
51
+ "version": settings.VERSION,
52
+ "status": "ok",
53
+ "uptime": _uptime_str(),
54
+ "timestamp": datetime.now(UTC).isoformat(),
55
+ "checks": {"database": "ok" if db_ok else "error"},
56
+ "links": {
57
+ "app": "/dashboard/",
58
+ "admin": "/admin/",
59
+ "status": "/status/",
60
+ "login": "/auth/login/",
61
+ },
62
+ }
63
+ )
64
+
65
+
66
+def status_page(request):
67
+ version = settings.VERSION
68
+ env = getattr(settings, "DJANGO_CONFIGURATION", "Local")
69
+ uptime = _uptime_str()
70
+ now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
71
+
72
+ env_color = {
73
+ "Local": "#22c55e",
74
+ "Staging": "#f59e0b",
75
+ "Production": "#ef4444",
76
+ }.get(env, "#6b7280")
77
+
78
+ links = [
79
+ ("App", "/dashboard/", "Django + HTMX application"),
80
+ ("Admin", "/admin/", "Django admin — users, permissions, data"),
81
+ ("Health", "/health/", "Service health checks (JSON)"),
82
+ ("Login", "/auth/login/", "Session-based authentication"),
83
+ ]
84
+
85
+ links_html = "\n".join(
86
+ f"""<a href="{url}" class="link-card">
87
+ <span class="link-title">{name}</span>
88
+ <span class="link-desc">{desc}</span>
89
+ <span class="link-arrow">&rarr;</span>
90
+ </a>"""
91
+ for name, url, desc in links
92
+ )
93
+
94
+ html = f"""<!DOCTYPE html>
95
+<html lang="en">
96
+<head>
97
+ <meta charset="UTF-8" />
98
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
99
+ <title>Fossilrepo Status</title>
100
+ <style>
101
+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
102
+ :root {{
103
+ --bg: #0a0a0a; --surface: #111111; --border: #1f1f1f;
104
+ --text: #e5e5e5; --muted: #6b7280; --accent: #ffffff; --green: #22c55e;
105
+ }}
106
+ body {{
107
+ background: var(--bg); color: var(--text);
108
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
109
+ min-height: 100vh; display: flex; flex-direction: column;
110
+ align-items: center; justify-content: center; padding: 2rem;
111
+ }}
112
+ .container {{ width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 2rem; }}
113
+ .header {{ display: flex; flex-direction: column; gap: 0.5rem; }}
114
+ .wordmark {{ font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; color: var(--accent); }}
115
+ .tagline {{ font-size: 0.875rem; color: var(--muted); }}
116
+ .status-bar {{
117
+ background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem;
118
+ padding: 1rem 1.25rem; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
119
+ }}
120
+ .status-dot {{
121
+ width: 8px; height: 8px; border-radius: 50%;
122
+ background: var(--green); box-shadow: 0 0 6px var(--green); flex-shrink: 0;
123
+ }}
124
+ .status-text {{ font-size: 0.875rem; font-weight: 500; flex: 1; }}
125
+ .meta-pills {{ display: flex; gap: 0.5rem; flex-wrap: wrap; }}
126
+ .pill {{
127
+ font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 999px;
128
+ border: 1px solid var(--border); color: var(--muted); white-space: nowrap;
129
+ }}
130
+ .pill-env {{ border-color: {env_color}33; color: {env_color}; }}
131
+ .links {{ display: flex; flex-direction: column; gap: 0.5rem; }}
132
+ .links-label {{
133
+ font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
134
+ letter-spacing: 0.08em; color: var(--muted);
135
+ padding: 0 0.25rem; margin-bottom: 0.25rem;
136
+ }}
137
+ .link-card {{
138
+ background: var(--surface); border: 1px solid var(--border); border-radius: 0.625rem;
139
+ padding: 0.875rem 1.25rem; display: grid;
140
+ grid-template-columns: 1fr auto; grid-template-rows: auto auto;
141
+ gap: 0.125rem 0.5rem; text-decoration: none; color: inherit;
142
+ transition: border-color 0.15s, background 0.15s;
143
+ }}
144
+ .link-card:hover {{ border-color: #2f2f2f; background: #161616; }}
145
+ .link-title {{
146
+ font-size: 0.875rem; font-weight: 500; color: var(--text);
147
+ grid-column: 1; grid-row: 1;
148
+ }}
149
+ .link-desc {{ font-size: 0.75rem; color: var(--muted); grid-column: 1; grid-row: 2; }}
150
+ .link-arrow {{
151
+ font-size: 1rem; color: var(--muted); grid-column: 2; grid-row: 1 / 3;
152
+ align-self: center; transition: color 0.15s, transform 0.15s;
153
+ }}
154
+ .link-card:hover .link-arrow {{ color: var(--text); transform: translateX(2px); }}
155
+ .footer {{ font-size: 0.7rem; color: var(--muted); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem; }}
156
+ </style>
157
+</head>
158
+<body>
159
+ <div class="container">
160
+ <div class="header">
161
+ <div class="wordmark">Fossilrepo</div>
162
+ <div class="tagline">Server-rendered Django + HTMX.</div>
163
+ </div>
164
+ <div class="status-bar">
165
+ <div class="status-dot"></div>
166
+ <div class="status-text">All systems operational</div>
167
+ <div class="meta-pills">
168
+ <span class="pill pill-env">{env}</span>
169
+ <span class="pill">v{version}</span>
170
+ <span class="pill">&uarr; {uptime}</span>
171
+ </div>
172
+ </div>
173
+ <div class="links">
174
+ <div class="links-label">Endpoints</div>
175
+ {links_html}
176
+ </div>
177
+ <div class="footer">
178
+ <span>fossilrepo-django-htmx</span>
179
+ <span>{now}</span>
180
+ </div>
181
+ </div>
182
+</body>
183
+</html>"""
184
+
185
+ return HttpResponse(html)
186
+
187
+
188
+urlpatterns = [
189
+ path("", RedirectView.as_view(pattern_name="dashboard", permanent=False)),
190
+ path("status/", status_page, name="status"),
191
+ path("dashboard/", include("core.urls")),
192
+ path("auth/", include("auth1.urls")),
193
+ path("items/", include("items.urls")),
194
+ path("admin/", admin.site.urls),
195
+ path("health/", health_check, name="health"),
196
+]
--- a/config/urls.py
+++ b/config/urls.py
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/config/urls.py
+++ b/config/urls.py
@@ -0,0 +1,196 @@
1 import time
2 from datetime import UTC, datetime
3
4 from django.conf import settings
5 from django.contrib import admin
6 from django.http import HttpResponse, JsonResponse
7 from django.urls import include, path
8 from django.views.generic import RedirectView
9
10 admin.site.site_header = settings.ADMIN_SITE_HEADER
11 admin.site.site_title = settings.ADMIN_SITE_TITLE
12 admin.site.index_title = "Welcome to Fossilrepo"
13
14 _START_TIME = time.monotonic()
15
16
17 def _uptime_str():
18 secs = int(time.monotonic() - _START_TIME)
19 h, rem = divmod(secs, 3600)
20 m, s = divmod(rem, 60)
21 if h:
22 return f"{h}h {m}m {s}s"
23 if m:
24 return f"{m}m {s}s"
25 return f"{s}s"
26
27
28 def health_check(request):
29 from django.db import connection
30
31 try:
32 with connection.cursor() as cursor:
33 cursor.execute("SELECT 1")
34 db_ok = True
35 except Exception as e:
36 return JsonResponse(
37 {
38 "service": "fossilrepo-django-htmx",
39 "version": settings.VERSION,
40 "status": "error",
41 "uptime": _uptime_str(),
42 "timestamp": datetime.now(UTC).isoformat(),
43 "checks": {"database": "error", "detail": str(e)},
44 },
45 status=503,
46 )
47
48 return JsonResponse(
49 {
50 "service": "fossilrepo-django-htmx",
51 "version": settings.VERSION,
52 "status": "ok",
53 "uptime": _uptime_str(),
54 "timestamp": datetime.now(UTC).isoformat(),
55 "checks": {"database": "ok" if db_ok else "error"},
56 "links": {
57 "app": "/dashboard/",
58 "admin": "/admin/",
59 "status": "/status/",
60 "login": "/auth/login/",
61 },
62 }
63 )
64
65
66 def status_page(request):
67 version = settings.VERSION
68 env = getattr(settings, "DJANGO_CONFIGURATION", "Local")
69 uptime = _uptime_str()
70 now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
71
72 env_color = {
73 "Local": "#22c55e",
74 "Staging": "#f59e0b",
75 "Production": "#ef4444",
76 }.get(env, "#6b7280")
77
78 links = [
79 ("App", "/dashboard/", "Django + HTMX application"),
80 ("Admin", "/admin/", "Django admin — users, permissions, data"),
81 ("Health", "/health/", "Service health checks (JSON)"),
82 ("Login", "/auth/login/", "Session-based authentication"),
83 ]
84
85 links_html = "\n".join(
86 f"""<a href="{url}" class="link-card">
87 <span class="link-title">{name}</span>
88 <span class="link-desc">{desc}</span>
89 <span class="link-arrow">&rarr;</span>
90 </a>"""
91 for name, url, desc in links
92 )
93
94 html = f"""<!DOCTYPE html>
95 <html lang="en">
96 <head>
97 <meta charset="UTF-8" />
98 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
99 <title>Fossilrepo Status</title>
100 <style>
101 *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
102 :root {{
103 --bg: #0a0a0a; --surface: #111111; --border: #1f1f1f;
104 --text: #e5e5e5; --muted: #6b7280; --accent: #ffffff; --green: #22c55e;
105 }}
106 body {{
107 background: var(--bg); color: var(--text);
108 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
109 min-height: 100vh; display: flex; flex-direction: column;
110 align-items: center; justify-content: center; padding: 2rem;
111 }}
112 .container {{ width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 2rem; }}
113 .header {{ display: flex; flex-direction: column; gap: 0.5rem; }}
114 .wordmark {{ font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; color: var(--accent); }}
115 .tagline {{ font-size: 0.875rem; color: var(--muted); }}
116 .status-bar {{
117 background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem;
118 padding: 1rem 1.25rem; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
119 }}
120 .status-dot {{
121 width: 8px; height: 8px; border-radius: 50%;
122 background: var(--green); box-shadow: 0 0 6px var(--green); flex-shrink: 0;
123 }}
124 .status-text {{ font-size: 0.875rem; font-weight: 500; flex: 1; }}
125 .meta-pills {{ display: flex; gap: 0.5rem; flex-wrap: wrap; }}
126 .pill {{
127 font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 999px;
128 border: 1px solid var(--border); color: var(--muted); white-space: nowrap;
129 }}
130 .pill-env {{ border-color: {env_color}33; color: {env_color}; }}
131 .links {{ display: flex; flex-direction: column; gap: 0.5rem; }}
132 .links-label {{
133 font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
134 letter-spacing: 0.08em; color: var(--muted);
135 padding: 0 0.25rem; margin-bottom: 0.25rem;
136 }}
137 .link-card {{
138 background: var(--surface); border: 1px solid var(--border); border-radius: 0.625rem;
139 padding: 0.875rem 1.25rem; display: grid;
140 grid-template-columns: 1fr auto; grid-template-rows: auto auto;
141 gap: 0.125rem 0.5rem; text-decoration: none; color: inherit;
142 transition: border-color 0.15s, background 0.15s;
143 }}
144 .link-card:hover {{ border-color: #2f2f2f; background: #161616; }}
145 .link-title {{
146 font-size: 0.875rem; font-weight: 500; color: var(--text);
147 grid-column: 1; grid-row: 1;
148 }}
149 .link-desc {{ font-size: 0.75rem; color: var(--muted); grid-column: 1; grid-row: 2; }}
150 .link-arrow {{
151 font-size: 1rem; color: var(--muted); grid-column: 2; grid-row: 1 / 3;
152 align-self: center; transition: color 0.15s, transform 0.15s;
153 }}
154 .link-card:hover .link-arrow {{ color: var(--text); transform: translateX(2px); }}
155 .footer {{ font-size: 0.7rem; color: var(--muted); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem; }}
156 </style>
157 </head>
158 <body>
159 <div class="container">
160 <div class="header">
161 <div class="wordmark">Fossilrepo</div>
162 <div class="tagline">Server-rendered Django + HTMX.</div>
163 </div>
164 <div class="status-bar">
165 <div class="status-dot"></div>
166 <div class="status-text">All systems operational</div>
167 <div class="meta-pills">
168 <span class="pill pill-env">{env}</span>
169 <span class="pill">v{version}</span>
170 <span class="pill">&uarr; {uptime}</span>
171 </div>
172 </div>
173 <div class="links">
174 <div class="links-label">Endpoints</div>
175 {links_html}
176 </div>
177 <div class="footer">
178 <span>fossilrepo-django-htmx</span>
179 <span>{now}</span>
180 </div>
181 </div>
182 </body>
183 </html>"""
184
185 return HttpResponse(html)
186
187
188 urlpatterns = [
189 path("", RedirectView.as_view(pattern_name="dashboard", permanent=False)),
190 path("status/", status_page, name="status"),
191 path("dashboard/", include("core.urls")),
192 path("auth/", include("auth1.urls")),
193 path("items/", include("items.urls")),
194 path("admin/", admin.site.urls),
195 path("health/", health_check, name="health"),
196 ]
--- a/config/wsgi.py
+++ b/config/wsgi.py
@@ -0,0 +1,6 @@
1
+import os
2
+
3
+from django.core.wsgi import get_wsgi_application
4
+
5
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+application = get_wsgi_application()
--- a/config/wsgi.py
+++ b/config/wsgi.py
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/config/wsgi.py
+++ b/config/wsgi.py
@@ -0,0 +1,6 @@
1 import os
2
3 from django.core.wsgi import get_wsgi_application
4
5 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6 application = get_wsgi_application()
+12
--- a/conftest.py
+++ b/conftest.py
@@ -0,0 +1,12 @@
1
+import pytest
2
+from django.contrib.auth.models import Group, Permission, User
3
+
4
+from organization.models import Organization, OrganizationMemberbjects.create_superuser(username="admin", email="[email protected]", password="testpass123")
5
+ return user
6
+
7
+
8
+@pytest.fixture
9
+def viewer_user(db):
10
+ user = User.objects.create_user(username="viewer", email="[email protected]", password="testpass123")
11
+ group, _ = Group.objects.get_or_create(name="Viewers")
12
+ view_perms = Permission.objects.filter(content_type__app_label="items",
--- a/conftest.py
+++ b/conftest.py
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/conftest.py
+++ b/conftest.py
@@ -0,0 +1,12 @@
1 import pytest
2 from django.contrib.auth.models import Group, Permission, User
3
4 from organization.models import Organization, OrganizationMemberbjects.create_superuser(username="admin", email="[email protected]", password="testpass123")
5 return user
6
7
8 @pytest.fixture
9 def viewer_user(db):
10 user = User.objects.create_user(username="viewer", email="[email protected]", password="testpass123")
11 group, _ = Group.objects.get_or_create(name="Viewers")
12 view_perms = Permission.objects.filter(content_type__app_label="items",

No diff available

--- a/core/admin.py
+++ b/core/admin.py
@@ -0,0 +1,23 @@
1
+from django.contrib import admin
2
+from import_export.admin import ImportExportMixin
3
+
4
+
5
+class BaseCoreAdmin(ImportExportMixin, admin.ModelAdmin):
6
+ """Base admin class for all Fossilrepo models. Provides audit field handling and import/export."""
7
+
8
+ def get_yset(request)
9
+
10
+ def get_readonly_fields(self, request, obj=None):
11
+ base = tuple(self.readonly_fields or ())
12
+ return base + ("version", "created_at", "created_by", "updated_at", "updated_by", "deleted_at", "deleted_by")
13
+
14
+ def get_raw_id_fields(self, request):
15
+ base = tuple(self.raw_id_fields or ())
16
+ return base + ("created_by", "updated_by", "deleted_by")
17
+
18
+ def save_model(self, request, obj, form, change):
19
+ if hasattr(obj, "created_by") and not obj.created_by:
20
+ obj.created_by = request.user
21
+ if hasattr(obj, "updated_by"):
22
+ obj.updated_by = request.user
23
+ super().save_model(r
--- a/core/admin.py
+++ b/core/admin.py
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/admin.py
+++ b/core/admin.py
@@ -0,0 +1,23 @@
1 from django.contrib import admin
2 from import_export.admin import ImportExportMixin
3
4
5 class BaseCoreAdmin(ImportExportMixin, admin.ModelAdmin):
6 """Base admin class for all Fossilrepo models. Provides audit field handling and import/export."""
7
8 def get_yset(request)
9
10 def get_readonly_fields(self, request, obj=None):
11 base = tuple(self.readonly_fields or ())
12 return base + ("version", "created_at", "created_by", "updated_at", "updated_by", "deleted_at", "deleted_by")
13
14 def get_raw_id_fields(self, request):
15 base = tuple(self.raw_id_fields or ())
16 return base + ("created_by", "updated_by", "deleted_by")
17
18 def save_model(self, request, obj, form, change):
19 if hasattr(obj, "created_by") and not obj.created_by:
20 obj.created_by = request.user
21 if hasattr(obj, "updated_by"):
22 obj.updated_by = request.user
23 super().save_model(r
--- a/core/apps.py
+++ b/core/apps.py
@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class CoreConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "core"
--- a/core/apps.py
+++ b/core/apps.py
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/core/apps.py
+++ b/core/apps.py
@@ -0,0 +1,6 @@
1 from django.apps import AppConfig
2
3
4 class CoreConfig(AppConfig):
5 default_auto_field = "django.db.models.BigAutoField"
6 name = "core"

No diff available

No diff available

--- a/core/middleware/current_user.py
+++ b/core/middleware/current_user.py
@@ -0,0 +1,19 @@
1
+import threading
2
+
3
+_thread_local = threading.local()
4
+
5
+
6
+def get_current_user():
7
+ return getattr(_thread_local, "user", None)
8
+
9
+
10
+class CurrentUserMiddleware:
11
+ """Store the current user on thread-local storage for use in signals and model save methods."""
12
+
13
+ def __init__(self, get_response):
14
+ self.get_response = get_response
15
+
16
+ def __call__(self, request):
17
+ _thread_local.user = getattr(request, "user", None)
18
+ response = self.get_response(request)
19
+ return response
--- a/core/middleware/current_user.py
+++ b/core/middleware/current_user.py
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/middleware/current_user.py
+++ b/core/middleware/current_user.py
@@ -0,0 +1,19 @@
1 import threading
2
3 _thread_local = threading.local()
4
5
6 def get_current_user():
7 return getattr(_thread_local, "user", None)
8
9
10 class CurrentUserMiddleware:
11 """Store the current user on thread-local storage for use in signals and model save methods."""
12
13 def __init__(self, get_response):
14 self.get_response = get_response
15
16 def __call__(self, request):
17 _thread_local.user = getattr(request, "user", None)
18 response = self.get_response(request)
19 return response

No diff available

--- a/core/models.py
+++ b/core/models.py
@@ -0,0 +1,74 @@
1
+import uuid
2
+
3
+from django.conf import settings
4
+from django.db import models
5
+from django.utils import timezone
6
+from django.utils.text import slugify
7
+from simple_history.models import HistoricalRecords
8
+
9
+
10
+class Tracking(models.Model):
11
+ """Abstract base providing audit trails and soft deletes for all business models."""
12
+
13
+ version = models.PositiveIntegerField(default=1, editable=False)
14
+ created_at = models.DateTimeField(auto_now_add=True)
15
+ created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
16
+ updated_at = models.DateTimeField(auto_now=True)
17
+ updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
18
+ deleted_at = models.DateTimeField(null=True, blank=True)
19
+ deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
20
+ history = HistoricalRecords(inherit=True)
21
+
22
+ class Meta:
23
+ abstract = True
24
+
25
+ def save(self, *args, **kwargs):
26
+ if self.pk:
27
+ self.version += 1
28
+ super().save(*args, **kwargs)
29
+
30
+ def soft_delete(self, user=None):
31
+ self.deleted_at = timezone.now()
32
+ self.deleted_by = user
33
+ self.save(update_fields=["deleted_at", "deleted_by", "updated_at", "version"])
34
+
35
+ @property
36
+ def is_deleted(self):
37
+ return self.deleted_at is not None
38
+
39
+
40
+class BaseCoreModel(Tracking):
41
+ """Abstract base for named, addressable entities with UUID external identifiers."""
42
+
43
+ guid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
44
+ name = models.CharField(max_length=200)
45
+ slug = models.SlugField(max_length=200, unique=True, db_index=True)
46
+ description = models.TextField(blank=True, default="")
47
+
48
+ class Meta:
49
+ abstract = True
50
+
51
+ def __str__(self):
52
+ return self.name
53
+
54
+ def save(self, *args, **kwargs):
55
+ if not self.slug:
56
+ base_slug = slugify(self.name)
57
+ slug = base_slug
58
+ counter = 1
59
+ model_class = type(self)
60
+ while model_class.objects.filter(slug=slug).exclude(pk=self.pk).exists():
61
+ slug = f"{base_slug}-{counter}"
62
+ counter += 1
63
+ self.slug = slug
64
+ super().save(*args, **kwargs)
65
+
66
+ def get_absolute_url(self):
67
+ return f"/{self._meta.app_label}/{self.slug}/"
68
+
69
+
70
+class ActiveManager(models.Manager):
71
+ """Manager that excludes soft-deleted records by default."""
72
+
73
+ def get_queryset(self):
74
+ return super().get_queryset().filter(deleted_at__isnull=True)
--- a/core/models.py
+++ b/core/models.py
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/models.py
+++ b/core/models.py
@@ -0,0 +1,74 @@
1 import uuid
2
3 from django.conf import settings
4 from django.db import models
5 from django.utils import timezone
6 from django.utils.text import slugify
7 from simple_history.models import HistoricalRecords
8
9
10 class Tracking(models.Model):
11 """Abstract base providing audit trails and soft deletes for all business models."""
12
13 version = models.PositiveIntegerField(default=1, editable=False)
14 created_at = models.DateTimeField(auto_now_add=True)
15 created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
16 updated_at = models.DateTimeField(auto_now=True)
17 updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
18 deleted_at = models.DateTimeField(null=True, blank=True)
19 deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
20 history = HistoricalRecords(inherit=True)
21
22 class Meta:
23 abstract = True
24
25 def save(self, *args, **kwargs):
26 if self.pk:
27 self.version += 1
28 super().save(*args, **kwargs)
29
30 def soft_delete(self, user=None):
31 self.deleted_at = timezone.now()
32 self.deleted_by = user
33 self.save(update_fields=["deleted_at", "deleted_by", "updated_at", "version"])
34
35 @property
36 def is_deleted(self):
37 return self.deleted_at is not None
38
39
40 class BaseCoreModel(Tracking):
41 """Abstract base for named, addressable entities with UUID external identifiers."""
42
43 guid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
44 name = models.CharField(max_length=200)
45 slug = models.SlugField(max_length=200, unique=True, db_index=True)
46 description = models.TextField(blank=True, default="")
47
48 class Meta:
49 abstract = True
50
51 def __str__(self):
52 return self.name
53
54 def save(self, *args, **kwargs):
55 if not self.slug:
56 base_slug = slugify(self.name)
57 slug = base_slug
58 counter = 1
59 model_class = type(self)
60 while model_class.objects.filter(slug=slug).exclude(pk=self.pk).exists():
61 slug = f"{base_slug}-{counter}"
62 counter += 1
63 self.slug = slug
64 super().save(*args, **kwargs)
65
66 def get_absolute_url(self):
67 return f"/{self._meta.app_label}/{self.slug}/"
68
69
70 class ActiveManager(models.Manager):
71 """Manager that excludes soft-deleted records by default."""
72
73 def get_queryset(self):
74 return super().get_queryset().filter(deleted_at__isnull=True)
--- a/core/permissions.py
+++ b/core/permissions.py
@@ -0,0 +1,35 @@
1
+import logging
2
+from enum import Enum
3
+
4
+from django.core.exceptions import PermissionDenied
5
+
6
+logger = logging.getLogger(__name__)
7
+
8
+
9
+class P(Enum):
10
+ """Permission enum. Check permissions via P.PERMISSION_NAME.check(user)."""
11
+
12
+ # Organization
13
+ ORGANIZATION_VIEW = "organization.view_organization"
14
+ ORGANIZATION_ADD = "organization.add_organization"
15
+ ORGANIZATION_CHANGE = "organization.change_organization"
16
+ ORGANIZATION_DELETE = "organization.dITEM_VIEW = "items.view_item"
17
+ ITEManizatioitems.add_item"
18
+ ITEM_CHANGE = "items.change_item"
19
+ ITEM_DELETE = "items.delete_itemDELETE = "pages.delete_page"
20
+
21
+ def check(self, user, raise_error=True):
22
+ """Check if user has this permission. Superusers always pass."""
23
+ if not user or not user.is_authenticated:
24
+ if raise_error:
25
+ raise PermissionDenied("Authentication required.")
26
+ return False
27
+
28
+ if user.is_superuser:
29
+ return True
30
+
31
+ if user.has_perm(self.value):
32
+ return True
33
+
34
+ if raise_error:
35
+ raise PermissionDenied(f"Permission denied: {self.val
--- a/core/permissions.py
+++ b/core/permissions.py
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/permissions.py
+++ b/core/permissions.py
@@ -0,0 +1,35 @@
1 import logging
2 from enum import Enum
3
4 from django.core.exceptions import PermissionDenied
5
6 logger = logging.getLogger(__name__)
7
8
9 class P(Enum):
10 """Permission enum. Check permissions via P.PERMISSION_NAME.check(user)."""
11
12 # Organization
13 ORGANIZATION_VIEW = "organization.view_organization"
14 ORGANIZATION_ADD = "organization.add_organization"
15 ORGANIZATION_CHANGE = "organization.change_organization"
16 ORGANIZATION_DELETE = "organization.dITEM_VIEW = "items.view_item"
17 ITEManizatioitems.add_item"
18 ITEM_CHANGE = "items.change_item"
19 ITEM_DELETE = "items.delete_itemDELETE = "pages.delete_page"
20
21 def check(self, user, raise_error=True):
22 """Check if user has this permission. Superusers always pass."""
23 if not user or not user.is_authenticated:
24 if raise_error:
25 raise PermissionDenied("Authentication required.")
26 return False
27
28 if user.is_superuser:
29 return True
30
31 if user.has_perm(self.value):
32 return True
33
34 if raise_error:
35 raise PermissionDenied(f"Permission denied: {self.val

No diff available

--- a/core/templatetags/permissions_tags.py
+++ b/core/templatetags/permissions_tags.py
@@ -0,0 +1,13 @@
1
+from django import template
2
+
3
+register = template.Library()
4
+
5
+
6
+@register.simple_tag(takes_context=True)
7
+def has_perm(context, perm_string):
8
+ """Check if the current user has a specific permission. Usage: {% has_perm 'items.view_itemperm 'projects.view_project' as can_view %}"""
9
+ user = context.get("user") or context["request"].user
10
+ if not user or not user.is_authenticated:
11
+ return False
12
+ if user.is_superuser:
13
+
--- a/core/templatetags/permissions_tags.py
+++ b/core/templatetags/permissions_tags.py
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/templatetags/permissions_tags.py
+++ b/core/templatetags/permissions_tags.py
@@ -0,0 +1,13 @@
1 from django import template
2
3 register = template.Library()
4
5
6 @register.simple_tag(takes_context=True)
7 def has_perm(context, perm_string):
8 """Check if the current user has a specific permission. Usage: {% has_perm 'items.view_itemperm 'projects.view_project' as can_view %}"""
9 user = context.get("user") or context["request"].user
10 if not user or not user.is_authenticated:
11 return False
12 if user.is_superuser:
13
+149
--- a/core/tests.py
+++ b/core/tests.py
@@ -0,0 +1,149 @@
1
+import pytest
2
+from django.contrib.auth.models import User
3
+from django.test import TestCase
4
+from django.urls import reverse
5
+
6
+from .permissions import P
7
+
8
+
9
+class TrackingModelTest(TestCase):
10
+ """Test the Tracking abstract model via a concrete model that uses it."""
11
+
12
+ defitems.models import Item
13
+
14
+ self.user = User.objects.create_superuser(username="test", password="x")
15
+ self.item = Itemoject
16
+
17
+ p2 = PrTest Widget", price="9.99", created_by=self.user)
18
+
19
+ def test_version_increments_on_save(self):
20
+ initial_version = self.itemtial_version = self.pritem.name = "Updated Widgetst_slug self.item.refresh_from_db()
21
+ item.version, initial_version + 1)
22
+
23
+ def test_soft_delete_sets_deleted_at(self):
24
+ self.itemself):
25
+ item.refresh_frIsNotNone(self.item.deleted_at)
26
+ item.deleted_by, self.user)
27
+ item.is_deleted)
28
+
29
+ def test_created_at_auto_set(self):
30
+ self.assertIsNotNone(self.itemssertIsNotNone(self.project.created_at)
31
+
32
+ def test_updated_at_auto_set(self):
33
+ item.updated_at)
34
+
35
+
36
+class BaseCoreModelTest(TestCase):
37
+ """Test BaseCoreModel slug generation and UUID."""
38
+
39
+ def setUp(self):
40
+ from items.models import Item
41
+
42
+ self.user = User.objects.create_superuser(username="test", password="x")
43
+ self.item = Itemoject
44
+
45
+ p2 = ProjeItem", price="19.99", created_by=self.user)
46
+
47
+ def test_slug_auto_generated(self):
48
+ self.assertEqual(self.item.slug, "my-item"m organization.mguid_is_uui):
49
+ self.aimport uuid
50
+
51
+ item.guid, uuid.UUID)
52
+
53
+ def test_slug_uniqueness(self):
54
+ from items.models import Item
55
+
56
+g", cre p2 = Itemoject
57
+
58
+ p2 = ProjeItem", price="29.99", created_assertNotEqual(self.itemcontr_deleted)
59
+
60
+ de initial_version = self.projitem), "My Item Project"
61
+ self.project.save()
62
+ self.project.refresh_from_db()
63
+ self.assertEqual(self.project.version, initial_version + 1)
64
+
65
+ def test_soft_delete_sets_deleted_at(self):
66
+ self.project.soft_delete(user=self.user)
67
+ self.project.refresh_from_db()
68
+ self.assertIsNotNone(self.project.deleted_at)
69
+ ITEMt.deleted_at)
70
+ self.aITEM_ADDeted_at)
71
+ self.as.updated_at)
72
+denied(self):
73
+ create_superuser(username="test", password="x")
74
+ self.org = Organization.objects.crITEMpdated_at)
75
+
76
+
77
+class BaseCoreModelTest(TestCase):
78
+ """Test BaseCoreModel slug generatITEMpdated_at)
79
+
80
+
81
+class Baseization.models import Organization
82
+ from projects.models import Project
83
+
84
+ self.user = User.objects.create_superuser(username="test", password="x")
85
+ self.org = Organization.objects.create(name="Test Org", created_by=self.user)
86
+ seITEMntrib.auth.models import User
87
+from django.test import TestCase
88
+from django.urls import reverse
89
+
90
+from .permissions import P
91
+
92
+
93
+class TrackingModelTest(TestCase):
94
+ """Test the Tracking abstract model via a concrete model that uses it."""
95
+
96
+ def setUp(self):
97
+ from organization.models import Organization
98
+ from projects.models import Project
99
+
100
+ self.user = User.objects.create_superuser(username="test", password="x")
101
+ self.org = Organization.objects.create(name="Test Org", created_by=self.user)
102
+ self.project = Project.objects.create(name="Test Project", organization=self.org, created_by=self.user)
103
+
104
+ def test_version_increments_on_save(self):
105
+ initial_version = self.project.version
106
+ self.project.name = "Updated Project"
107
+ self.project.save()
108
+ self.project.refresh_from_db()
109
+ self.assertEqual(self.project.version, initial_version + 1)
110
+
111
+ def test_soft_delete_sets_deleted_at(self):
112
+ self.project.soft_delete(user=self.user)
113
+ self.project.refresh_from_db()
114
+ self.assertIsNotNone(self.project.deleted_at)
115
+ self.assertEqual(self.project.deleted_by, self.user)
116
+ self.assertTrue(self.project.is_deleted)
117
+
118
+ def test_created_at_auto_set(self):
119
+ self.assertIsNotNone(self.project.created_at)
120
+
121
+ def test_updated_at_auto_set(self):
122
+ self.assertIsNotNone(self.project.updated_at)
123
+
124
+
125
+class BaseCoreModelTest(TestCase):
126
+ """Test BaseCoreModel slug generation and UUID."""
127
+
128
+ def setUp(self):
129
+ from organization.models import Organization
130
+ from projects.models import Project
131
+
132
+ self.user = User.objects.create_superuser(username="test", password="x")
133
+ self.org = Organization.objects.create(name="Test Org", created_by=self.user)
134
+ self.project = Project.objects.create(name="My Project", organization=self.org, created_by=self.usimport pytest
135
+from django.contrib.auth.models import User
136
+from django.test import TestCase
137
+from django.urls import reverse
138
+
139
+from .permissions import P
140
+
141
+
142
+class TrackingModelTest(TestCase):
143
+ """Test the Tracking abstract model via a concrete model that uses it."""
144
+
145
+ def setUp(self):
146
+ from organization.models import Organization
147
+ from projects.models import Project
148
+
149
+ self.user = User.obj
--- a/core/tests.py
+++ b/core/tests.py
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/tests.py
+++ b/core/tests.py
@@ -0,0 +1,149 @@
1 import pytest
2 from django.contrib.auth.models import User
3 from django.test import TestCase
4 from django.urls import reverse
5
6 from .permissions import P
7
8
9 class TrackingModelTest(TestCase):
10 """Test the Tracking abstract model via a concrete model that uses it."""
11
12 defitems.models import Item
13
14 self.user = User.objects.create_superuser(username="test", password="x")
15 self.item = Itemoject
16
17 p2 = PrTest Widget", price="9.99", created_by=self.user)
18
19 def test_version_increments_on_save(self):
20 initial_version = self.itemtial_version = self.pritem.name = "Updated Widgetst_slug self.item.refresh_from_db()
21 item.version, initial_version + 1)
22
23 def test_soft_delete_sets_deleted_at(self):
24 self.itemself):
25 item.refresh_frIsNotNone(self.item.deleted_at)
26 item.deleted_by, self.user)
27 item.is_deleted)
28
29 def test_created_at_auto_set(self):
30 self.assertIsNotNone(self.itemssertIsNotNone(self.project.created_at)
31
32 def test_updated_at_auto_set(self):
33 item.updated_at)
34
35
36 class BaseCoreModelTest(TestCase):
37 """Test BaseCoreModel slug generation and UUID."""
38
39 def setUp(self):
40 from items.models import Item
41
42 self.user = User.objects.create_superuser(username="test", password="x")
43 self.item = Itemoject
44
45 p2 = ProjeItem", price="19.99", created_by=self.user)
46
47 def test_slug_auto_generated(self):
48 self.assertEqual(self.item.slug, "my-item"m organization.mguid_is_uui):
49 self.aimport uuid
50
51 item.guid, uuid.UUID)
52
53 def test_slug_uniqueness(self):
54 from items.models import Item
55
56 g", cre p2 = Itemoject
57
58 p2 = ProjeItem", price="29.99", created_assertNotEqual(self.itemcontr_deleted)
59
60 de initial_version = self.projitem), "My Item Project"
61 self.project.save()
62 self.project.refresh_from_db()
63 self.assertEqual(self.project.version, initial_version + 1)
64
65 def test_soft_delete_sets_deleted_at(self):
66 self.project.soft_delete(user=self.user)
67 self.project.refresh_from_db()
68 self.assertIsNotNone(self.project.deleted_at)
69 ITEMt.deleted_at)
70 self.aITEM_ADDeted_at)
71 self.as.updated_at)
72 denied(self):
73 create_superuser(username="test", password="x")
74 self.org = Organization.objects.crITEMpdated_at)
75
76
77 class BaseCoreModelTest(TestCase):
78 """Test BaseCoreModel slug generatITEMpdated_at)
79
80
81 class Baseization.models import Organization
82 from projects.models import Project
83
84 self.user = User.objects.create_superuser(username="test", password="x")
85 self.org = Organization.objects.create(name="Test Org", created_by=self.user)
86 seITEMntrib.auth.models import User
87 from django.test import TestCase
88 from django.urls import reverse
89
90 from .permissions import P
91
92
93 class TrackingModelTest(TestCase):
94 """Test the Tracking abstract model via a concrete model that uses it."""
95
96 def setUp(self):
97 from organization.models import Organization
98 from projects.models import Project
99
100 self.user = User.objects.create_superuser(username="test", password="x")
101 self.org = Organization.objects.create(name="Test Org", created_by=self.user)
102 self.project = Project.objects.create(name="Test Project", organization=self.org, created_by=self.user)
103
104 def test_version_increments_on_save(self):
105 initial_version = self.project.version
106 self.project.name = "Updated Project"
107 self.project.save()
108 self.project.refresh_from_db()
109 self.assertEqual(self.project.version, initial_version + 1)
110
111 def test_soft_delete_sets_deleted_at(self):
112 self.project.soft_delete(user=self.user)
113 self.project.refresh_from_db()
114 self.assertIsNotNone(self.project.deleted_at)
115 self.assertEqual(self.project.deleted_by, self.user)
116 self.assertTrue(self.project.is_deleted)
117
118 def test_created_at_auto_set(self):
119 self.assertIsNotNone(self.project.created_at)
120
121 def test_updated_at_auto_set(self):
122 self.assertIsNotNone(self.project.updated_at)
123
124
125 class BaseCoreModelTest(TestCase):
126 """Test BaseCoreModel slug generation and UUID."""
127
128 def setUp(self):
129 from organization.models import Organization
130 from projects.models import Project
131
132 self.user = User.objects.create_superuser(username="test", password="x")
133 self.org = Organization.objects.create(name="Test Org", created_by=self.user)
134 self.project = Project.objects.create(name="My Project", organization=self.org, created_by=self.usimport pytest
135 from django.contrib.auth.models import User
136 from django.test import TestCase
137 from django.urls import reverse
138
139 from .permissions import P
140
141
142 class TrackingModelTest(TestCase):
143 """Test the Tracking abstract model via a concrete model that uses it."""
144
145 def setUp(self):
146 from organization.models import Organization
147 from projects.models import Project
148
149 self.user = User.obj
--- a/core/urls.py
+++ b/core/urls.py
@@ -0,0 +1,7 @@
1
+from django.urls import path
2
+
3
+from . import views
4
+
5
+urlpatterns = [
6
+ path("", views.dashboard, name="dashboard"),
7
+]
--- a/core/urls.py
+++ b/core/urls.py
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
--- a/core/urls.py
+++ b/core/urls.py
@@ -0,0 +1,7 @@
1 from django.urls import path
2
3 from . import views
4
5 urlpatterns = [
6 path("", views.dashboard, name="dashboard"),
7 ]
--- a/core/views.py
+++ b/core/views.py
@@ -0,0 +1 @@
1
+from django.contrib.return render(
--- a/core/views.py
+++ b/core/views.py
@@ -0,0 +1 @@
 
--- a/core/views.py
+++ b/core/views.py
@@ -0,0 +1 @@
1 from django.contrib.return render(
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -0,0 +1,90 @@
1
+services:
2
+ backend:
3
+ build: .
4
+ command: python manage.py runserver 0.0.0.0:8000
5
+ portenv_file: .env.example
6
+ environment:
7
+ DJANGO_DEBUG: "true"
8
+ POSTGRES_HOST: postgres
9
+ REDIS_URL: redis://redis:6379/1
10
+ CELERY_BROKER: redis://redis:6379/0
11
+ EMAIL_HOST: mailpit
12
+ volumfossil-ces:
13
+ backend:
14
+ celery-beat:
15
+ build: .
16
+ command: celery -A config.celery beat -l info --schedul environment:
17
+ DJANGO_DEBUG: "true"
18
+ POSTGRES_HOST: postgres
19
+ REDIS_URL: redis://redis:6379/1
20
+ CELERY_BROKER: redis://redis:6379/0
21
+ EMAIL_HOST: mailpit
22
+ volumes:
23
+ - .:/app
24
+ - ./repos:/data/repos
25
+ depends_on:
26
+ postgres:
27
+ condition: service_healthy
28
+ redis:
29
+ condition: service_healthy
30
+
31
+ celery-worker:
32
+ build: .
33
+ command: celery -A config.celery worker -l info -Q celery
34
+ env_file: .env.example
35
+ environment:
36
+ POSTGRES_HOST: postgres
37
+ REDIS_URL: redis://redis:6379/1
38
+ CELERY_BROKER: redis://redis:6379/0
39
+ volumes:
40
+ - .:/app
41
+ depends_on:
42
+ postgres:
43
+ condition: service_healthy
44
+ redis:
45
+ condition: service_healthy
46
+
47
+ celery-beat:
48
+ build: .
49
+ command: celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
50
+ env_file: .env.example
51
+ environment:
52
+ POSTGRES_HOST: postgres
53
+ REDIS_URL: redis://redis:6379/1
54
+ CELERY_BROKER: redis://redis:6379/0
55
+ volumes:
56
+ - .:/app
57
+ depends_on:
58
+ postgres:
59
+ condition: service_healthy
60
+ redis:
61
+ condition: service_healthy
62
+
63
+ postgres:
64
+ image: postgres:16-alpine
65
+ ports:
66
+ - "5432:5432"
67
+ environment:
68
+ POSTGRES_DB: fossilrepo
69
+ POSTGRES_USER: dbadmin
70
+ # Dev-only credentials. Override via .env in production.
71
+ POSTGRES_PASSWORD: Password123
72
+ volumes:
73
+ - pgdata:/var/lib/postgresql/data
74
+ healthcheck:
75
+ test: ["CMD-SHELL", "pg_isready -U dbadmin -d fossilrepo"]
76
+ interval: 5s
77
+ timeout: 5s
78
+ retries: 5
79
+
80
+ redis:
81
+ image: redis:7-alpine
82
+ ports:
83
+ - "6379:6379"
84
+ healthcheck:
85
+ test: ["CMD", "redis-cli", "ping"]
86
+ interval: 5s
87
+ timeout: 5s
88
+ services:
89
+ back
90
+3CLN8M;
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -0,0 +1,90 @@
1 services:
2 backend:
3 build: .
4 command: python manage.py runserver 0.0.0.0:8000
5 portenv_file: .env.example
6 environment:
7 DJANGO_DEBUG: "true"
8 POSTGRES_HOST: postgres
9 REDIS_URL: redis://redis:6379/1
10 CELERY_BROKER: redis://redis:6379/0
11 EMAIL_HOST: mailpit
12 volumfossil-ces:
13 backend:
14 celery-beat:
15 build: .
16 command: celery -A config.celery beat -l info --schedul environment:
17 DJANGO_DEBUG: "true"
18 POSTGRES_HOST: postgres
19 REDIS_URL: redis://redis:6379/1
20 CELERY_BROKER: redis://redis:6379/0
21 EMAIL_HOST: mailpit
22 volumes:
23 - .:/app
24 - ./repos:/data/repos
25 depends_on:
26 postgres:
27 condition: service_healthy
28 redis:
29 condition: service_healthy
30
31 celery-worker:
32 build: .
33 command: celery -A config.celery worker -l info -Q celery
34 env_file: .env.example
35 environment:
36 POSTGRES_HOST: postgres
37 REDIS_URL: redis://redis:6379/1
38 CELERY_BROKER: redis://redis:6379/0
39 volumes:
40 - .:/app
41 depends_on:
42 postgres:
43 condition: service_healthy
44 redis:
45 condition: service_healthy
46
47 celery-beat:
48 build: .
49 command: celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
50 env_file: .env.example
51 environment:
52 POSTGRES_HOST: postgres
53 REDIS_URL: redis://redis:6379/1
54 CELERY_BROKER: redis://redis:6379/0
55 volumes:
56 - .:/app
57 depends_on:
58 postgres:
59 condition: service_healthy
60 redis:
61 condition: service_healthy
62
63 postgres:
64 image: postgres:16-alpine
65 ports:
66 - "5432:5432"
67 environment:
68 POSTGRES_DB: fossilrepo
69 POSTGRES_USER: dbadmin
70 # Dev-only credentials. Override via .env in production.
71 POSTGRES_PASSWORD: Password123
72 volumes:
73 - pgdata:/var/lib/postgresql/data
74 healthcheck:
75 test: ["CMD-SHELL", "pg_isready -U dbadmin -d fossilrepo"]
76 interval: 5s
77 timeout: 5s
78 retries: 5
79
80 redis:
81 image: redis:7-alpine
82 ports:
83 - "6379:6379"
84 healthcheck:
85 test: ["CMD", "redis-cli", "ping"]
86 interval: 5s
87 timeout: 5s
88 services:
89 back
90 3CLN8M;

No diff available

--- a/items/admin.py
+++ b/items/admin.py
@@ -0,0 +1,12 @@
1
+from django.contrib import admin
2
+
3
+from core.admin import BaseCoreAdmin
4
+
5
+from .models import Item
6
+
7
+
8
+@admin.register(Item)
9
+class ItemAdmin(BaseCoreAdmin):
10
+ list_display = ("name", "slug", "price", "sku", "is_active", "created_at")
11
+ list_filter = ("is_active",)
12
+ search_fields = ("name", "slug", "sku")
--- a/items/admin.py
+++ b/items/admin.py
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/admin.py
+++ b/items/admin.py
@@ -0,0 +1,12 @@
1 from django.contrib import admin
2
3 from core.admin import BaseCoreAdmin
4
5 from .models import Item
6
7
8 @admin.register(Item)
9 class ItemAdmin(BaseCoreAdmin):
10 list_display = ("name", "slug", "price", "sku", "is_active", "created_at")
11 list_filter = ("is_active",)
12 search_fields = ("name", "slug", "sku")
--- a/items/apps.py
+++ b/items/apps.py
@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class ItemsConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "items"
--- a/items/apps.py
+++ b/items/apps.py
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/items/apps.py
+++ b/items/apps.py
@@ -0,0 +1,6 @@
1 from django.apps import AppConfig
2
3
4 class ItemsConfig(AppConfig):
5 default_auto_field = "django.db.models.BigAutoField"
6 name = "items"
--- a/items/forms.py
+++ b/items/forms.py
@@ -0,0 +1,18 @@
1
+from django import forms
2
+
3
+from .models import Item
4
+
5
+tw = "w-full rounded-md border-gray-indigo-500 focus:ring-indigo-500 sm:text-sm"
6
+
7
+
8
+class ItemForm(forms.ModelForm):
9
+ class Meta:
10
+ model = Item
11
+ fields = ["name", "description", "price", "sku", "is_active"]
12
+ widgets = {
13
+ "name": forms.TextInput(attrs={"class": tw, "placeholder": "Item name"}),
14
+ "description": forms.Textarea(attrs={"class": tw, "rows": 3, "placeholder": "Description"}),
15
+ "price": forms.NumberInput(attrs={"class": tw, "step": "0.01", "placeholder": "0.00"}),
16
+ "sku": forms.TextInput(attrs={"class": tw, "placeholder": "SKU-001"}),
17
+ "is_active": forms.CheckboxInput(attrs={"class": "rounded border-gray-300 text-indigo-600"}),
18
+ }
--- a/items/forms.py
+++ b/items/forms.py
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/forms.py
+++ b/items/forms.py
@@ -0,0 +1,18 @@
1 from django import forms
2
3 from .models import Item
4
5 tw = "w-full rounded-md border-gray-indigo-500 focus:ring-indigo-500 sm:text-sm"
6
7
8 class ItemForm(forms.ModelForm):
9 class Meta:
10 model = Item
11 fields = ["name", "description", "price", "sku", "is_active"]
12 widgets = {
13 "name": forms.TextInput(attrs={"class": tw, "placeholder": "Item name"}),
14 "description": forms.Textarea(attrs={"class": tw, "rows": 3, "placeholder": "Description"}),
15 "price": forms.NumberInput(attrs={"class": tw, "step": "0.01", "placeholder": "0.00"}),
16 "sku": forms.TextInput(attrs={"class": tw, "placeholder": "SKU-001"}),
17 "is_active": forms.CheckboxInput(attrs={"class": "rounded border-gray-300 text-indigo-600"}),
18 }
--- a/items/migrations/0001_initial.py
+++ b/items/migrations/0001_initial.py
@@ -0,0 +1,168 @@
1
+# Generated by Django 5.2.12 on 2026-03-26 05:59
2
+
3
+import uuid
4
+
5
+import django.db.models.deletion
6
+import simple_history.models
7
+from django.conf import settings
8
+from django.db import migrations, models
9
+
10
+
11
+class Migration(migrations.Migration):
12
+ initial = True
13
+
14
+ dependencies = [
15
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name="HistoricalItem",
21
+ fields=[
22
+ (
23
+ "id",
24
+ models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
25
+ ),
26
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
27
+ ("created_at", models.DateTimeField(blank=True, editable=False)),
28
+ ("updated_at", models.DateTimeField(blank=True, editable=False)),
29
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
30
+ (
31
+ "guid",
32
+ models.UUIDField(db_index=True, default=uuid.uuid4, editable=False),
33
+ ),
34
+ ("name", models.CharField(max_length=200)),
35
+ ("slug", models.SlugField(max_length=200)),
36
+ ("description", models.TextField(blank=True, default="")),
37
+ ("price", models.DecimalField(decimal_places=2, max_digits=10)),
38
+ (
39
+ "sku",
40
+ models.CharField(blank=True, db_index=True, default="", max_length=50),
41
+ ),
42
+ ("is_active", models.BooleanField(default=True)),
43
+ ("history_id", models.AutoField(primary_key=True, serialize=False)),
44
+ ("history_date", models.DateTimeField(db_index=True)),
45
+ ("history_change_reason", models.CharField(max_length=100, null=True)),
46
+ (
47
+ "history_type",
48
+ models.CharField(
49
+ choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
50
+ max_length=1,
51
+ ),
52
+ ),
53
+ (
54
+ "created_by",
55
+ models.ForeignKey(
56
+ blank=True,
57
+ db_constraint=False,
58
+ null=True,
59
+ on_delete=django.db.models.deletion.DO_NOTHING,
60
+ related_name="+",
61
+ to=settings.AUTH_USER_MODEL,
62
+ ),
63
+ ),
64
+ (
65
+ "deleted_by",
66
+ models.ForeignKey(
67
+ blank=True,
68
+ db_constraint=False,
69
+ null=True,
70
+ on_delete=django.db.models.deletion.DO_NOTHING,
71
+ related_name="+",
72
+ to=settings.AUTH_USER_MODEL,
73
+ ),
74
+ ),
75
+ (
76
+ "history_user",
77
+ models.ForeignKey(
78
+ null=True,
79
+ on_delete=django.db.models.deletion.SET_NULL,
80
+ related_name="+",
81
+ to=settings.AUTH_USER_MODEL,
82
+ ),
83
+ ),
84
+ (
85
+ "updated_by",
86
+ models.ForeignKey(
87
+ blank=True,
88
+ db_constraint=False,
89
+ null=True,
90
+ on_delete=django.db.models.deletion.DO_NOTHING,
91
+ related_name="+",
92
+ to=settings.AUTH_USER_MODEL,
93
+ ),
94
+ ),
95
+ ],
96
+ options={
97
+ "verbose_name": "historical item",
98
+ "verbose_name_plural": "historical items",
99
+ "ordering": ("-history_date", "-history_id"),
100
+ "get_latest_by": ("history_date", "history_id"),
101
+ },
102
+ bases=(simple_history.models.HistoricalChanges, models.Model),
103
+ ),
104
+ migrations.CreateModel(
105
+ name="Item",
106
+ fields=[
107
+ (
108
+ "id",
109
+ models.BigAutoField(
110
+ auto_created=True,
111
+ primary_key=True,
112
+ serialize=False,
113
+ verbose_name="ID",
114
+ ),
115
+ ),
116
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
117
+ ("created_at", models.DateTimeField(auto_now_add=True)),
118
+ ("updated_at", models.DateTimeField(auto_now=True)),
119
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
120
+ (
121
+ "guid",
122
+ models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True),
123
+ ),
124
+ ("name", models.CharField(max_length=200)),
125
+ ("slug", models.SlugField(max_length=200, unique=True)),
126
+ ("description", models.TextField(blank=True, default="")),
127
+ ("price", models.DecimalField(decimal_places=2, max_digits=10)),
128
+ (
129
+ "sku",
130
+ models.CharField(blank=True, default="", max_length=50, unique=True),
131
+ ),
132
+ ("is_active", models.BooleanField(default=True)),
133
+ (
134
+ "created_by",
135
+ models.ForeignKey(
136
+ blank=True,
137
+ null=True,
138
+ on_delete=django.db.models.deletion.SET_NULL,
139
+ related_name="+",
140
+ to=settings.AUTH_USER_MODEL,
141
+ ),
142
+ ),
143
+ (
144
+ "deleted_by",
145
+ models.ForeignKey(
146
+ blank=True,
147
+ null=True,
148
+ on_delete=django.db.models.deletion.SET_NULL,
149
+ related_name="+",
150
+ to=settings.AUTH_USER_MODEL,
151
+ ),
152
+ ),
153
+ (
154
+ "updated_by",
155
+ models.ForeignKey(
156
+ blank=True,
157
+ null=True,
158
+ on_delete=django.db.models.deletion.SET_NULL,
159
+ related_name="+",
160
+ to=settings.AUTH_USER_MODEL,
161
+ ),
162
+ ),
163
+ ],
164
+ options={
165
+ "ordering": ["-created_at"],
166
+ },
167
+ ),
168
+ ]
--- a/items/migrations/0001_initial.py
+++ b/items/migrations/0001_initial.py
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/migrations/0001_initial.py
+++ b/items/migrations/0001_initial.py
@@ -0,0 +1,168 @@
1 # Generated by Django 5.2.12 on 2026-03-26 05:59
2
3 import uuid
4
5 import django.db.models.deletion
6 import simple_history.models
7 from django.conf import settings
8 from django.db import migrations, models
9
10
11 class Migration(migrations.Migration):
12 initial = True
13
14 dependencies = [
15 migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16 ]
17
18 operations = [
19 migrations.CreateModel(
20 name="HistoricalItem",
21 fields=[
22 (
23 "id",
24 models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
25 ),
26 ("version", models.PositiveIntegerField(default=1, editable=False)),
27 ("created_at", models.DateTimeField(blank=True, editable=False)),
28 ("updated_at", models.DateTimeField(blank=True, editable=False)),
29 ("deleted_at", models.DateTimeField(blank=True, null=True)),
30 (
31 "guid",
32 models.UUIDField(db_index=True, default=uuid.uuid4, editable=False),
33 ),
34 ("name", models.CharField(max_length=200)),
35 ("slug", models.SlugField(max_length=200)),
36 ("description", models.TextField(blank=True, default="")),
37 ("price", models.DecimalField(decimal_places=2, max_digits=10)),
38 (
39 "sku",
40 models.CharField(blank=True, db_index=True, default="", max_length=50),
41 ),
42 ("is_active", models.BooleanField(default=True)),
43 ("history_id", models.AutoField(primary_key=True, serialize=False)),
44 ("history_date", models.DateTimeField(db_index=True)),
45 ("history_change_reason", models.CharField(max_length=100, null=True)),
46 (
47 "history_type",
48 models.CharField(
49 choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
50 max_length=1,
51 ),
52 ),
53 (
54 "created_by",
55 models.ForeignKey(
56 blank=True,
57 db_constraint=False,
58 null=True,
59 on_delete=django.db.models.deletion.DO_NOTHING,
60 related_name="+",
61 to=settings.AUTH_USER_MODEL,
62 ),
63 ),
64 (
65 "deleted_by",
66 models.ForeignKey(
67 blank=True,
68 db_constraint=False,
69 null=True,
70 on_delete=django.db.models.deletion.DO_NOTHING,
71 related_name="+",
72 to=settings.AUTH_USER_MODEL,
73 ),
74 ),
75 (
76 "history_user",
77 models.ForeignKey(
78 null=True,
79 on_delete=django.db.models.deletion.SET_NULL,
80 related_name="+",
81 to=settings.AUTH_USER_MODEL,
82 ),
83 ),
84 (
85 "updated_by",
86 models.ForeignKey(
87 blank=True,
88 db_constraint=False,
89 null=True,
90 on_delete=django.db.models.deletion.DO_NOTHING,
91 related_name="+",
92 to=settings.AUTH_USER_MODEL,
93 ),
94 ),
95 ],
96 options={
97 "verbose_name": "historical item",
98 "verbose_name_plural": "historical items",
99 "ordering": ("-history_date", "-history_id"),
100 "get_latest_by": ("history_date", "history_id"),
101 },
102 bases=(simple_history.models.HistoricalChanges, models.Model),
103 ),
104 migrations.CreateModel(
105 name="Item",
106 fields=[
107 (
108 "id",
109 models.BigAutoField(
110 auto_created=True,
111 primary_key=True,
112 serialize=False,
113 verbose_name="ID",
114 ),
115 ),
116 ("version", models.PositiveIntegerField(default=1, editable=False)),
117 ("created_at", models.DateTimeField(auto_now_add=True)),
118 ("updated_at", models.DateTimeField(auto_now=True)),
119 ("deleted_at", models.DateTimeField(blank=True, null=True)),
120 (
121 "guid",
122 models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True),
123 ),
124 ("name", models.CharField(max_length=200)),
125 ("slug", models.SlugField(max_length=200, unique=True)),
126 ("description", models.TextField(blank=True, default="")),
127 ("price", models.DecimalField(decimal_places=2, max_digits=10)),
128 (
129 "sku",
130 models.CharField(blank=True, default="", max_length=50, unique=True),
131 ),
132 ("is_active", models.BooleanField(default=True)),
133 (
134 "created_by",
135 models.ForeignKey(
136 blank=True,
137 null=True,
138 on_delete=django.db.models.deletion.SET_NULL,
139 related_name="+",
140 to=settings.AUTH_USER_MODEL,
141 ),
142 ),
143 (
144 "deleted_by",
145 models.ForeignKey(
146 blank=True,
147 null=True,
148 on_delete=django.db.models.deletion.SET_NULL,
149 related_name="+",
150 to=settings.AUTH_USER_MODEL,
151 ),
152 ),
153 (
154 "updated_by",
155 models.ForeignKey(
156 blank=True,
157 null=True,
158 on_delete=django.db.models.deletion.SET_NULL,
159 related_name="+",
160 to=settings.AUTH_USER_MODEL,
161 ),
162 ),
163 ],
164 options={
165 "ordering": ["-created_at"],
166 },
167 ),
168 ]
--- a/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
+++ b/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
@@ -0,0 +1,22 @@
1
+# Generated by Django 5.2.12 on 2026-03-26 06:01
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("items", "0001_initial"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterField(
13
+ model_name="historicalitem",
14
+ name="sku",
15
+ field=models.CharField(blank=True, db_index=True, default=None, max_length=50, null=True),
16
+ ),
17
+ migrations.AlterField(
18
+ model_name="item",
19
+ name="sku",
20
+ field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True),
21
+ ),
22
+ ]
--- a/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
+++ b/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
+++ b/items/migrations/0002_alter_historicalitem_sku_alter_item_sku.py
@@ -0,0 +1,22 @@
1 # Generated by Django 5.2.12 on 2026-03-26 06:01
2
3 from django.db import migrations, models
4
5
6 class Migration(migrations.Migration):
7 dependencies = [
8 ("items", "0001_initial"),
9 ]
10
11 operations = [
12 migrations.AlterField(
13 model_name="historicalitem",
14 name="sku",
15 field=models.CharField(blank=True, db_index=True, default=None, max_length=50, null=True),
16 ),
17 migrations.AlterField(
18 model_name="item",
19 name="sku",
20 field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True),
21 ),
22 ]

No diff available

--- a/items/models.py
+++ b/items/models.py
@@ -0,0 +1,15 @@
1
+from django.db import models
2
+
3
+from core.models import ActiveManager, BaseCoreModel
4
+
5
+
6
+class Item(BaseCoreModel):
7
+ price = models.DecimalField(max_digits=10, decimal_places=2)
8
+ sku = models.CharField(max_length=50, unique=True, blank=True, null=True, default=None)
9
+ is_active = models.BooleanField(default=True)
10
+
11
+ objects = ActiveManager()
12
+ all_objects = models.Manager()
13
+
14
+ class Meta:
15
+ ordering = ["-created_at"]
--- a/items/models.py
+++ b/items/models.py
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/models.py
+++ b/items/models.py
@@ -0,0 +1,15 @@
1 from django.db import models
2
3 from core.models import ActiveManager, BaseCoreModel
4
5
6 class Item(BaseCoreModel):
7 price = models.DecimalField(max_digits=10, decimal_places=2)
8 sku = models.CharField(max_length=50, unique=True, blank=True, null=True, default=None)
9 is_active = models.BooleanField(default=True)
10
11 objects = ActiveManager()
12 all_objects = models.Manager()
13
14 class Meta:
15 ordering = ["-created_at"]
--- a/items/tests.py
+++ b/items/tests.py
@@ -0,0 +1,133 @@
1
+import pytest
2
+from django.urls import reverse
3
+
4
+from .models import Item
5
+
6
+
7
+@pytest.fixture
8
+def sample_item(db, admin_user):
9
+ return Item.objects.create(name="Test Widget", price="29.99", sku="TST-001", created_by=admin_user)
10
+
11
+
12
+@pytest.mark.django_db
13
+class TestItemList:
14
+ def test_list_requires_login(self, client):
15
+ response = client.get(reverse("items:list"))
16
+ assert response.status_code == 302
17
+
18
+ def test_list_renders_for_superuser(self, admin_client, sample_item):
19
+ response = admin_client.get(reverse("items:list"))
20
+ assert response.status_code == 200
21
+ assert b"Test Widget" in response.content
22
+
23
+ def test_list_renders_for_viewer(self, viewer_client, sample_item):
24
+ response = viewer_client.get(reverse("items:list"))
25
+ assert response.status_code == 200
26
+ assert b"Test Widget" in response.content
27
+
28
+ def test_list_denied_for_user_without_perm(self, no_perm_client, sample_item):
29
+ response = no_perm_client.get(reverse("items:list"))
30
+ assert response.status_code == 403
31
+
32
+ def test_list_htmx_returns_partial(self, admin_client, sample_item):
33
+ response = admin_client.get(reverse("items:list"), HTTP_HX_REQUEST="true")
34
+ assert response.status_code == 200
35
+ assert b"item-table" in response.content
36
+ assert b"<!DOCTYPE" not in response.content # partial, not full page
37
+
38
+ def test_list_search_filters(self, admin_client, admin_user):
39
+ Item.objects.create(name="Alpha", price="10.00", created_by=admin_user)
40
+ Item.objects.create(name="Beta", price="20.00", created_by=admin_user)
41
+ response = admin_client.get(reverse("items:list") + "?search=Alpha")
42
+ assert b"Alpha" in response.content
43
+ assert b"Beta" not in response.content
44
+
45
+
46
+@pytest.mark.django_db
47
+class TestItemCreate:
48
+ def test_create_form_renders(self, admin_client):
49
+ response = admin_client.get(reverse("items:create"))
50
+ assert response.status_code == 200
51
+ assert b"New Item" in response.content
52
+
53
+ def test_create_saves_item(self, admin_client, admin_user):
54
+ response = admin_client.post(
55
+ reverse("items:create"),
56
+ {"name": "New Gadget", "description": "A new gadget", "price": "49.99", "sku": "NGT-001", "is_active": True},
57
+ )
58
+ assert response.status_code == 302
59
+ item = Item.objects.get(sku="NGT-001")
60
+ assert item.name == "New Gadget"
61
+ assert item.created_by == admin_user
62
+
63
+ def test_create_denied_for_viewer(self, viewer_client):
64
+ response = viewer_client.get(reverse("items:create"))
65
+ assert response.status_code == 403
66
+
67
+ def test_create_invalid_data_shows_errors(self, admin_client):
68
+ response = admin_client.post(reverse("items:create"), {"name": "", "price": ""})
69
+ assert response.status_code == 200 # re-renders form with errors
70
+
71
+
72
+@pytest.mark.django_db
73
+class TestItemDetail:
74
+ def test_detail_renders(self, admin_client, sample_item):
75
+ response = admin_client.get(reverse("items:detail", kwargs={"slug": sample_item.slug}))
76
+ assert response.status_code == 200
77
+ assert b"Test Widget" in response.content
78
+ assert str(sample_item.guid).encode() in response.content
79
+
80
+ def test_detail_404_for_deleted(self, admin_client, sample_item, admin_user):
81
+ sample_item.soft_delete(user=admin_user)
82
+ response = admin_client.get(reverse("items:detail", kwargs={"slug": sample_item.slug}))
83
+ assert response.status_code == 404
84
+
85
+
86
+@pytest.mark.django_db
87
+class TestItemUpdate:
88
+ def test_update_form_renders(self, admin_client, sample_item):
89
+ response = admin_client.get(reverse("items:update", kwargs={"slug": sample_item.slug}))
90
+ assert response.status_code == 200
91
+ assert b"Edit Item" in response.content
92
+
93
+ def test_update_saves_changes(self, admin_client, sample_item):
94
+ response = admin_client.post(
95
+ reverse("items:update", kwargs={"slug": sample_item.slug}),
96
+ {"name": "Updated Widget", "description": "Updated", "price": "39.99", "sku": "TST-001", "is_active": True},
97
+ )
98
+ assert response.status_code == 302
99
+ sample_item.refresh_from_db()
100
+ assert sample_item.name == "Updated Widget"
101
+ from decimal import Decimal
102
+
103
+ assert sample_item.price == Decimal("39.99")
104
+
105
+ def test_update_denied_for_viewer(self, viewer_client, sample_item):
106
+ response = viewer_client.get(reverse("items:update", kwargs={"slug": sample_item.slug}))
107
+ assert response.status_code == 403
108
+
109
+
110
+@pytest.mark.django_db
111
+class TestItemDelete:
112
+ def test_delete_confirm_renders(self, admin_client, sample_item):
113
+ response = admin_client.get(reverse("items:delete", kwargs={"slug": sample_item.slug}))
114
+ assert response.status_code == 200
115
+ assert b"Delete Item" in response.content
116
+
117
+ def test_delete_soft_deletes(self, admin_client, sample_item):
118
+ response = admin_client.post(reverse("items:delete", kwargs={"slug": sample_item.slug}))
119
+ assert response.status_code == 302
120
+ sample_item.refresh_from_db()
121
+ assert sample_item.is_deleted
122
+
123
+ def test_delete_htmx_returns_redirect_header(self, admin_client, sample_item):
124
+ response = admin_client.post(
125
+ reverse("items:delete", kwargs={"slug": sample_item.slug}),
126
+ HTTP_HX_REQUEST="true",
127
+ )
128
+ assert response.status_code == 200
129
+ assert response.headers.get("HX-Redirect") == "/items/"
130
+
131
+ def test_delete_denied_for_viewer(self, viewer_client, sample_item):
132
+ response = viewer_client.post(reverse("items:delete", kwargs={"slug": sample_item.slug}))
133
+ assert response.status_code == 403
--- a/items/tests.py
+++ b/items/tests.py
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/tests.py
+++ b/items/tests.py
@@ -0,0 +1,133 @@
1 import pytest
2 from django.urls import reverse
3
4 from .models import Item
5
6
7 @pytest.fixture
8 def sample_item(db, admin_user):
9 return Item.objects.create(name="Test Widget", price="29.99", sku="TST-001", created_by=admin_user)
10
11
12 @pytest.mark.django_db
13 class TestItemList:
14 def test_list_requires_login(self, client):
15 response = client.get(reverse("items:list"))
16 assert response.status_code == 302
17
18 def test_list_renders_for_superuser(self, admin_client, sample_item):
19 response = admin_client.get(reverse("items:list"))
20 assert response.status_code == 200
21 assert b"Test Widget" in response.content
22
23 def test_list_renders_for_viewer(self, viewer_client, sample_item):
24 response = viewer_client.get(reverse("items:list"))
25 assert response.status_code == 200
26 assert b"Test Widget" in response.content
27
28 def test_list_denied_for_user_without_perm(self, no_perm_client, sample_item):
29 response = no_perm_client.get(reverse("items:list"))
30 assert response.status_code == 403
31
32 def test_list_htmx_returns_partial(self, admin_client, sample_item):
33 response = admin_client.get(reverse("items:list"), HTTP_HX_REQUEST="true")
34 assert response.status_code == 200
35 assert b"item-table" in response.content
36 assert b"<!DOCTYPE" not in response.content # partial, not full page
37
38 def test_list_search_filters(self, admin_client, admin_user):
39 Item.objects.create(name="Alpha", price="10.00", created_by=admin_user)
40 Item.objects.create(name="Beta", price="20.00", created_by=admin_user)
41 response = admin_client.get(reverse("items:list") + "?search=Alpha")
42 assert b"Alpha" in response.content
43 assert b"Beta" not in response.content
44
45
46 @pytest.mark.django_db
47 class TestItemCreate:
48 def test_create_form_renders(self, admin_client):
49 response = admin_client.get(reverse("items:create"))
50 assert response.status_code == 200
51 assert b"New Item" in response.content
52
53 def test_create_saves_item(self, admin_client, admin_user):
54 response = admin_client.post(
55 reverse("items:create"),
56 {"name": "New Gadget", "description": "A new gadget", "price": "49.99", "sku": "NGT-001", "is_active": True},
57 )
58 assert response.status_code == 302
59 item = Item.objects.get(sku="NGT-001")
60 assert item.name == "New Gadget"
61 assert item.created_by == admin_user
62
63 def test_create_denied_for_viewer(self, viewer_client):
64 response = viewer_client.get(reverse("items:create"))
65 assert response.status_code == 403
66
67 def test_create_invalid_data_shows_errors(self, admin_client):
68 response = admin_client.post(reverse("items:create"), {"name": "", "price": ""})
69 assert response.status_code == 200 # re-renders form with errors
70
71
72 @pytest.mark.django_db
73 class TestItemDetail:
74 def test_detail_renders(self, admin_client, sample_item):
75 response = admin_client.get(reverse("items:detail", kwargs={"slug": sample_item.slug}))
76 assert response.status_code == 200
77 assert b"Test Widget" in response.content
78 assert str(sample_item.guid).encode() in response.content
79
80 def test_detail_404_for_deleted(self, admin_client, sample_item, admin_user):
81 sample_item.soft_delete(user=admin_user)
82 response = admin_client.get(reverse("items:detail", kwargs={"slug": sample_item.slug}))
83 assert response.status_code == 404
84
85
86 @pytest.mark.django_db
87 class TestItemUpdate:
88 def test_update_form_renders(self, admin_client, sample_item):
89 response = admin_client.get(reverse("items:update", kwargs={"slug": sample_item.slug}))
90 assert response.status_code == 200
91 assert b"Edit Item" in response.content
92
93 def test_update_saves_changes(self, admin_client, sample_item):
94 response = admin_client.post(
95 reverse("items:update", kwargs={"slug": sample_item.slug}),
96 {"name": "Updated Widget", "description": "Updated", "price": "39.99", "sku": "TST-001", "is_active": True},
97 )
98 assert response.status_code == 302
99 sample_item.refresh_from_db()
100 assert sample_item.name == "Updated Widget"
101 from decimal import Decimal
102
103 assert sample_item.price == Decimal("39.99")
104
105 def test_update_denied_for_viewer(self, viewer_client, sample_item):
106 response = viewer_client.get(reverse("items:update", kwargs={"slug": sample_item.slug}))
107 assert response.status_code == 403
108
109
110 @pytest.mark.django_db
111 class TestItemDelete:
112 def test_delete_confirm_renders(self, admin_client, sample_item):
113 response = admin_client.get(reverse("items:delete", kwargs={"slug": sample_item.slug}))
114 assert response.status_code == 200
115 assert b"Delete Item" in response.content
116
117 def test_delete_soft_deletes(self, admin_client, sample_item):
118 response = admin_client.post(reverse("items:delete", kwargs={"slug": sample_item.slug}))
119 assert response.status_code == 302
120 sample_item.refresh_from_db()
121 assert sample_item.is_deleted
122
123 def test_delete_htmx_returns_redirect_header(self, admin_client, sample_item):
124 response = admin_client.post(
125 reverse("items:delete", kwargs={"slug": sample_item.slug}),
126 HTTP_HX_REQUEST="true",
127 )
128 assert response.status_code == 200
129 assert response.headers.get("HX-Redirect") == "/items/"
130
131 def test_delete_denied_for_viewer(self, viewer_client, sample_item):
132 response = viewer_client.post(reverse("items:delete", kwargs={"slug": sample_item.slug}))
133 assert response.status_code == 403
--- a/items/urls.py
+++ b/items/urls.py
@@ -0,0 +1,13 @@
1
+from django.urls import path
2
+
3
+from . import views
4
+
5
+app_name = "items"
6
+
7
+urlpatterns = [
8
+ path("", views.item_list, name="list"),
9
+ path("create/", views.item_create, name="create"),
10
+ path("<slug:slug>/", views.item_detail, name="detail"),
11
+ path("<slug:slug>/edit/", views.item_update, name="update"),
12
+ path("<slug:slug>/delete/", views.item_delete, name="delete"),
13
+]
--- a/items/urls.py
+++ b/items/urls.py
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/urls.py
+++ b/items/urls.py
@@ -0,0 +1,13 @@
1 from django.urls import path
2
3 from . import views
4
5 app_name = "items"
6
7 urlpatterns = [
8 path("", views.item_list, name="list"),
9 path("create/", views.item_create, name="create"),
10 path("<slug:slug>/", views.item_detail, name="detail"),
11 path("<slug:slug>/edit/", views.item_update, name="update"),
12 path("<slug:slug>/delete/", views.item_delete, name="delete"),
13 ]
--- a/items/views.py
+++ b/items/views.py
@@ -0,0 +1,86 @@
1
+from django.contrib import messages
2
+from django.contrib.auth.decorators import login_required
3
+from django.shortcuts import get_object_or_404, redirect, render
4
+
5
+from core.permissions import P
6
+
7
+from .forms import ItemForm
8
+from .models import Item
9
+
10
+
11
+@login_required
12
+def item_list(request):
13
+ P.ITEM_VIEW.check(request.user)
14
+ items = Item.objects.all()
15
+
16
+ search = request.GET.get("search", "").strip()
17
+ if search:
18
+ items = items.filter(name__icontains=search)
19
+
20
+ if request.headers.get("HX-Request"):
21
+ return render(request, "items/partials/item_table.html", {"items": items})
22
+
23
+ return render(request, "items/item_list.html", {"items": items, "search": search})
24
+
25
+
26
+@login_required
27
+def item_create(request):
28
+ P.ITEM_ADD.check(request.user)
29
+
30
+ if request.method == "POST":
31
+ form = ItemForm(request.POST)
32
+ if form.is_valid():
33
+ item = form.save(commit=False)
34
+ item.created_by = request.user
35
+ item.save()
36
+ messages.success(request, f'Item "{item.name}" created.')
37
+ return redirect("items:detail", slug=item.slug)
38
+ else:
39
+ form = ItemForm()
40
+
41
+ return render(request, "items/item_form.html", {"form": form, "title": "New Item"})
42
+
43
+
44
+@login_required
45
+def item_detail(request, slug):
46
+ P.ITEM_VIEW.check(request.user)
47
+ item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
48
+ return render(request, "items/item_detail.html", {"item": item})
49
+
50
+
51
+@login_required
52
+def item_update(request, slug):
53
+ P.ITEM_CHANGE.check(request.user)
54
+ item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
55
+
56
+ if request.method == "POST":
57
+ form = ItemForm(request.POST, instance=item)
58
+ if form.is_valid():
59
+ item = form.save(commit=False)
60
+ item.updated_by = request.user
61
+ item.save()
62
+ messages.success(request, f'Item "{item.name}" updated.')
63
+ return redirect("items:detail", slug=item.slug)
64
+ else:
65
+ form = ItemForm(instance=item)
66
+
67
+ return render(request, "items/item_form.html", {"form": form, "item": item, "title": "Edit Item"})
68
+
69
+
70
+@login_required
71
+def item_delete(request, slug):
72
+ P.ITEM_DELETE.check(request.user)
73
+ item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
74
+
75
+ if request.method == "POST":
76
+ item.soft_delete(user=request.user)
77
+ messages.success(request, f'Item "{item.name}" deleted.')
78
+
79
+ if request.headers.get("HX-Request"):
80
+ from django.http import HttpResponse
81
+
82
+ return HttpResponse(status=200, headers={"HX-Redirect": "/items/"})
83
+
84
+ return redirect("items:list")
85
+
86
+ return render(request, "items/item_confirm_delete.html", {"item": item})
--- a/items/views.py
+++ b/items/views.py
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/items/views.py
+++ b/items/views.py
@@ -0,0 +1,86 @@
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
3 from django.shortcuts import get_object_or_404, redirect, render
4
5 from core.permissions import P
6
7 from .forms import ItemForm
8 from .models import Item
9
10
11 @login_required
12 def item_list(request):
13 P.ITEM_VIEW.check(request.user)
14 items = Item.objects.all()
15
16 search = request.GET.get("search", "").strip()
17 if search:
18 items = items.filter(name__icontains=search)
19
20 if request.headers.get("HX-Request"):
21 return render(request, "items/partials/item_table.html", {"items": items})
22
23 return render(request, "items/item_list.html", {"items": items, "search": search})
24
25
26 @login_required
27 def item_create(request):
28 P.ITEM_ADD.check(request.user)
29
30 if request.method == "POST":
31 form = ItemForm(request.POST)
32 if form.is_valid():
33 item = form.save(commit=False)
34 item.created_by = request.user
35 item.save()
36 messages.success(request, f'Item "{item.name}" created.')
37 return redirect("items:detail", slug=item.slug)
38 else:
39 form = ItemForm()
40
41 return render(request, "items/item_form.html", {"form": form, "title": "New Item"})
42
43
44 @login_required
45 def item_detail(request, slug):
46 P.ITEM_VIEW.check(request.user)
47 item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
48 return render(request, "items/item_detail.html", {"item": item})
49
50
51 @login_required
52 def item_update(request, slug):
53 P.ITEM_CHANGE.check(request.user)
54 item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
55
56 if request.method == "POST":
57 form = ItemForm(request.POST, instance=item)
58 if form.is_valid():
59 item = form.save(commit=False)
60 item.updated_by = request.user
61 item.save()
62 messages.success(request, f'Item "{item.name}" updated.')
63 return redirect("items:detail", slug=item.slug)
64 else:
65 form = ItemForm(instance=item)
66
67 return render(request, "items/item_form.html", {"form": form, "item": item, "title": "Edit Item"})
68
69
70 @login_required
71 def item_delete(request, slug):
72 P.ITEM_DELETE.check(request.user)
73 item = get_object_or_404(Item, slug=slug, deleted_at__isnull=True)
74
75 if request.method == "POST":
76 item.soft_delete(user=request.user)
77 messages.success(request, f'Item "{item.name}" deleted.')
78
79 if request.headers.get("HX-Request"):
80 from django.http import HttpResponse
81
82 return HttpResponse(status=200, headers={"HX-Redirect": "/items/"})
83
84 return redirect("items:list")
85
86 return render(request, "items/item_confirm_delete.html", {"item": item})
+14
--- a/manage.py
+++ b/manage.py
@@ -0,0 +1,14 @@
1
+#!/usr/bin/env python
2
+import os
3
+import sys
4
+
5
+
6
+def main():
7
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
8
+ from django.core.management import execute_from_command_line
9
+
10
+ execute_from_command_line(sys.argv)
11
+
12
+
13
+if __name__ == "__main__":
14
+ main()
--- a/manage.py
+++ b/manage.py
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/manage.py
+++ b/manage.py
@@ -0,0 +1,14 @@
1 #!/usr/bin/env python
2 import os
3 import sys
4
5
6 def main():
7 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
8 from django.core.management import execute_from_command_line
9
10 execute_from_command_line(sys.argv)
11
12
13 if __name__ == "__main__":
14 main()

No diff available

--- a/organization/admin.py
+++ b/organization/admin.py
@@ -0,0 +1,24 @@
1
+from django.contrib import admin
2
+
3
+from core.admin import BaseCoreAdmin
4
+
5
+from .models import Organ
6
+
7
+
8
+class
9
+@admin.register(Team)
10
+cmodel = OrganizationMember
11
+ extra = 0
12
+ raw_id_fields = ("member",)
13
+
14
+
15
+@admin.register(Organization)
16
+class OrganizationAdmin(BaseCoreAdmin):
17
+ list_display = ("name", "slug", "website", "created_at")
18
+ search_fields = ("name", "slug")
19
+ inlines = [OrganizationMemberInline]
20
+
21
+
22
+@admin.register(Team)
23
+class TeamAdmin(BaseCtra = 0
24
+ "organization")
--- a/organization/admin.py
+++ b/organization/admin.py
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/organization/admin.py
+++ b/organization/admin.py
@@ -0,0 +1,24 @@
1 from django.contrib import admin
2
3 from core.admin import BaseCoreAdmin
4
5 from .models import Organ
6
7
8 class
9 @admin.register(Team)
10 cmodel = OrganizationMember
11 extra = 0
12 raw_id_fields = ("member",)
13
14
15 @admin.register(Organization)
16 class OrganizationAdmin(BaseCoreAdmin):
17 list_display = ("name", "slug", "website", "created_at")
18 search_fields = ("name", "slug")
19 inlines = [OrganizationMemberInline]
20
21
22 @admin.register(Team)
23 class TeamAdmin(BaseCtra = 0
24 "organization")
--- a/organization/apps.py
+++ b/organization/apps.py
@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class OrganizationConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "organization"
--- a/organization/apps.py
+++ b/organization/apps.py
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/organization/apps.py
+++ b/organization/apps.py
@@ -0,0 +1,6 @@
1 from django.apps import AppConfig
2
3
4 class OrganizationConfig(AppConfig):
5 default_auto_field = "django.db.models.BigAutoField"
6 name = "organization"
--- a/organization/migrations/0001_initial.py
+++ b/organization/migrations/0001_initial.py
@@ -0,0 +1,330 @@
1
+# Generated by Django 5.2.12 on 2026-03-26 05:59
2
+
3
+import uuid
4
+
5
+import django.db.models.deletion
6
+import simple_history.models
7
+from django.conf import settings
8
+from django.db import migrations, models
9
+
10
+
11
+class Migration(migrations.Migration):
12
+ initial = True
13
+
14
+ dependencies = [
15
+ ("auth", "0012_alter_user_first_name_max_length"),
16
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17
+ ]
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name="HistoricalOrganization",
22
+ fields=[
23
+ (
24
+ "id",
25
+ models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
26
+ ),
27
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
28
+ ("created_at", models.DateTimeField(blank=True, editable=False)),
29
+ ("updated_at", models.DateTimeField(blank=True, editable=False)),
30
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
31
+ (
32
+ "guid",
33
+ models.UUIDField(db_index=True, default=uuid.uuid4, editable=False),
34
+ ),
35
+ ("name", models.CharField(max_length=200)),
36
+ ("slug", models.SlugField(max_length=200)),
37
+ ("description", models.TextField(blank=True, default="")),
38
+ ("website", models.URLField(blank=True, default="")),
39
+ ("history_id", models.AutoField(primary_key=True, serialize=False)),
40
+ ("history_date", models.DateTimeField(db_index=True)),
41
+ ("history_change_reason", models.CharField(max_length=100, null=True)),
42
+ (
43
+ "history_type",
44
+ models.CharField(
45
+ choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
46
+ max_length=1,
47
+ ),
48
+ ),
49
+ (
50
+ "created_by",
51
+ models.ForeignKey(
52
+ blank=True,
53
+ db_constraint=False,
54
+ null=True,
55
+ on_delete=django.db.models.deletion.DO_NOTHING,
56
+ related_name="+",
57
+ to=settings.AUTH_USER_MODEL,
58
+ ),
59
+ ),
60
+ (
61
+ "deleted_by",
62
+ models.ForeignKey(
63
+ blank=True,
64
+ db_constraint=False,
65
+ null=True,
66
+ on_delete=django.db.models.deletion.DO_NOTHING,
67
+ related_name="+",
68
+ to=settings.AUTH_USER_MODEL,
69
+ ),
70
+ ),
71
+ (
72
+ "history_user",
73
+ models.ForeignKey(
74
+ null=True,
75
+ on_delete=django.db.models.deletion.SET_NULL,
76
+ related_name="+",
77
+ to=settings.AUTH_USER_MODEL,
78
+ ),
79
+ ),
80
+ (
81
+ "updated_by",
82
+ models.ForeignKey(
83
+ blank=True,
84
+ db_constraint=False,
85
+ null=True,
86
+ on_delete=django.db.models.deletion.DO_NOTHING,
87
+ related_name="+",
88
+ to=settings.AUTH_USER_MODEL,
89
+ ),
90
+ ),
91
+ ],
92
+ options={
93
+ "verbose_name": "historical organization",
94
+ "verbose_name_plural": "historical organizations",
95
+ "ordering": ("-history_date", "-history_id"),
96
+ "get_latest_by": ("history_date", "history_id"),
97
+ },
98
+ bases=(simple_history.models.HistoricalChanges, models.Model),
99
+ ),
100
+ migrations.CreateModel(
101
+ name="Organization",
102
+ fields=[
103
+ (
104
+ "id",
105
+ models.BigAutoField(
106
+ auto_created=True,
107
+ primary_key=True,
108
+ serialize=False,
109
+ verbose_name="ID",
110
+ ),
111
+ ),
112
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
113
+ ("created_at", models.DateTimeField(auto_now_add=True)),
114
+ ("updated_at", models.DateTimeField(auto_now=True)),
115
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
116
+ (
117
+ "guid",
118
+ models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True),
119
+ ),
120
+ ("name", models.CharField(max_length=200)),
121
+ ("slug", models.SlugField(max_length=200, unique=True)),
122
+ ("description", models.TextField(blank=True, default="")),
123
+ ("website", models.URLField(blank=True, default="")),
124
+ (
125
+ "created_by",
126
+ models.ForeignKey(
127
+ blank=True,
128
+ null=True,
129
+ on_delete=django.db.models.deletion.SET_NULL,
130
+ related_name="+",
131
+ to=settings.AUTH_USER_MODEL,
132
+ ),
133
+ ),
134
+ (
135
+ "deleted_by",
136
+ models.ForeignKey(
137
+ blank=True,
138
+ null=True,
139
+ on_delete=django.db.models.deletion.SET_NULL,
140
+ related_name="+",
141
+ to=settings.AUTH_USER_MODEL,
142
+ ),
143
+ ),
144
+ (
145
+ "groups",
146
+ models.ManyToManyField(blank=True, related_name="organizations", to="auth.group"),
147
+ ),
148
+ (
149
+ "updated_by",
150
+ models.ForeignKey(
151
+ blank=True,
152
+ null=True,
153
+ on_delete=django.db.models.deletion.SET_NULL,
154
+ related_name="+",
155
+ to=settings.AUTH_USER_MODEL,
156
+ ),
157
+ ),
158
+ ],
159
+ options={
160
+ "ordering": ["name"],
161
+ },
162
+ ),
163
+ migrations.CreateModel(
164
+ name="HistoricalOrganizationMember",
165
+ fields=[
166
+ (
167
+ "id",
168
+ models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
169
+ ),
170
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
171
+ ("created_at", models.DateTimeField(blank=True, editable=False)),
172
+ ("updated_at", models.DateTimeField(blank=True, editable=False)),
173
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
174
+ ("is_active", models.BooleanField(default=True)),
175
+ ("history_id", models.AutoField(primary_key=True, serialize=False)),
176
+ ("history_date", models.DateTimeField(db_index=True)),
177
+ ("history_change_reason", models.CharField(max_length=100, null=True)),
178
+ (
179
+ "history_type",
180
+ models.CharField(
181
+ choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
182
+ max_length=1,
183
+ ),
184
+ ),
185
+ (
186
+ "created_by",
187
+ models.ForeignKey(
188
+ blank=True,
189
+ db_constraint=False,
190
+ null=True,
191
+ on_delete=django.db.models.deletion.DO_NOTHING,
192
+ related_name="+",
193
+ to=settings.AUTH_USER_MODEL,
194
+ ),
195
+ ),
196
+ (
197
+ "deleted_by",
198
+ models.ForeignKey(
199
+ blank=True,
200
+ db_constraint=False,
201
+ null=True,
202
+ on_delete=django.db.models.deletion.DO_NOTHING,
203
+ related_name="+",
204
+ to=settings.AUTH_USER_MODEL,
205
+ ),
206
+ ),
207
+ (
208
+ "history_user",
209
+ models.ForeignKey(
210
+ null=True,
211
+ on_delete=django.db.models.deletion.SET_NULL,
212
+ related_name="+",
213
+ to=settings.AUTH_USER_MODEL,
214
+ ),
215
+ ),
216
+ (
217
+ "member",
218
+ models.ForeignKey(
219
+ blank=True,
220
+ db_constraint=False,
221
+ null=True,
222
+ on_delete=django.db.models.deletion.DO_NOTHING,
223
+ related_name="+",
224
+ to=settings.AUTH_USER_MODEL,
225
+ ),
226
+ ),
227
+ (
228
+ "updated_by",
229
+ models.ForeignKey(
230
+ blank=True,
231
+ db_constraint=False,
232
+ null=True,
233
+ on_delete=django.db.models.deletion.DO_NOTHING,
234
+ related_name="+",
235
+ to=settings.AUTH_USER_MODEL,
236
+ ),
237
+ ),
238
+ (
239
+ "organization",
240
+ models.ForeignKey(
241
+ blank=True,
242
+ db_constraint=False,
243
+ null=True,
244
+ on_delete=django.db.models.deletion.DO_NOTHING,
245
+ related_name="+",
246
+ to="organization.organization",
247
+ ),
248
+ ),
249
+ ],
250
+ options={
251
+ "verbose_name": "historical organization member",
252
+ "verbose_name_plural": "historical organization members",
253
+ "ordering": ("-history_date", "-history_id"),
254
+ "get_latest_by": ("history_date", "history_id"),
255
+ },
256
+ bases=(simple_history.models.HistoricalChanges, models.Model),
257
+ ),
258
+ migrations.CreateModel(
259
+ name="OrganizationMember",
260
+ fields=[
261
+ (
262
+ "id",
263
+ models.BigAutoField(
264
+ auto_created=True,
265
+ primary_key=True,
266
+ serialize=False,
267
+ verbose_name="ID",
268
+ ),
269
+ ),
270
+ ("version", models.PositiveIntegerField(default=1, editable=False)),
271
+ ("created_at", models.DateTimeField(auto_now_add=True)),
272
+ ("updated_at", models.DateTimeField(auto_now=True)),
273
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
274
+ ("is_active", models.BooleanField(default=True)),
275
+ (
276
+ "created_by",
277
+ models.ForeignKey(
278
+ blank=True,
279
+ null=True,
280
+ on_delete=django.db.models.deletion.SET_NULL,
281
+ related_name="+",
282
+ to=settings.AUTH_USER_MODEL,
283
+ ),
284
+ ),
285
+ (
286
+ "deleted_by",
287
+ models.ForeignKey(
288
+ blank=True,
289
+ null=True,
290
+ on_delete=django.db.models.deletion.SET_NULL,
291
+ related_name="+",
292
+ to=settings.AUTH_USER_MODEL,
293
+ ),
294
+ ),
295
+ (
296
+ "groups",
297
+ models.ManyToManyField(blank=True, related_name="org_memberships", to="auth.group"),
298
+ ),
299
+ (
300
+ "member",
301
+ models.ForeignKey(
302
+ on_delete=django.db.models.deletion.CASCADE,
303
+ related_name="memberships",
304
+ to=settings.AUTH_USER_MODEL,
305
+ ),
306
+ ),
307
+ (
308
+ "organization",
309
+ models.ForeignKey(
310
+ on_delete=django.db.models.deletion.CASCADE,
311
+ related_name="members",
312
+ to="organization.organization",
313
+ ),
314
+ ),
315
+ (
316
+ "updated_by",
317
+ models.ForeignKey(
318
+ blank=True,
319
+ null=True,
320
+ on_delete=django.db.models.deletion.SET_NULL,
321
+ related_name="+",
322
+ to=settings.AUTH_USER_MODEL,
323
+ ),
324
+ ),
325
+ ],
326
+ options={
327
+ "unique_together": {("member", "organization")},
328
+ },
329
+ ),
330
+ ]
--- a/organization/migrations/0001_initial.py
+++ b/organization/migrations/0001_initial.py
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/organization/migrations/0001_initial.py
+++ b/organization/migrations/0001_initial.py
@@ -0,0 +1,330 @@
1 # Generated by Django 5.2.12 on 2026-03-26 05:59
2
3 import uuid
4
5 import django.db.models.deletion
6 import simple_history.models
7 from django.conf import settings
8 from django.db import migrations, models
9
10
11 class Migration(migrations.Migration):
12 initial = True
13
14 dependencies = [
15 ("auth", "0012_alter_user_first_name_max_length"),
16 migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17 ]
18
19 operations = [
20 migrations.CreateModel(
21 name="HistoricalOrganization",
22 fields=[
23 (
24 "id",
25 models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
26 ),
27 ("version", models.PositiveIntegerField(default=1, editable=False)),
28 ("created_at", models.DateTimeField(blank=True, editable=False)),
29 ("updated_at", models.DateTimeField(blank=True, editable=False)),
30 ("deleted_at", models.DateTimeField(blank=True, null=True)),
31 (
32 "guid",
33 models.UUIDField(db_index=True, default=uuid.uuid4, editable=False),
34 ),
35 ("name", models.CharField(max_length=200)),
36 ("slug", models.SlugField(max_length=200)),
37 ("description", models.TextField(blank=True, default="")),
38 ("website", models.URLField(blank=True, default="")),
39 ("history_id", models.AutoField(primary_key=True, serialize=False)),
40 ("history_date", models.DateTimeField(db_index=True)),
41 ("history_change_reason", models.CharField(max_length=100, null=True)),
42 (
43 "history_type",
44 models.CharField(
45 choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
46 max_length=1,
47 ),
48 ),
49 (
50 "created_by",
51 models.ForeignKey(
52 blank=True,
53 db_constraint=False,
54 null=True,
55 on_delete=django.db.models.deletion.DO_NOTHING,
56 related_name="+",
57 to=settings.AUTH_USER_MODEL,
58 ),
59 ),
60 (
61 "deleted_by",
62 models.ForeignKey(
63 blank=True,
64 db_constraint=False,
65 null=True,
66 on_delete=django.db.models.deletion.DO_NOTHING,
67 related_name="+",
68 to=settings.AUTH_USER_MODEL,
69 ),
70 ),
71 (
72 "history_user",
73 models.ForeignKey(
74 null=True,
75 on_delete=django.db.models.deletion.SET_NULL,
76 related_name="+",
77 to=settings.AUTH_USER_MODEL,
78 ),
79 ),
80 (
81 "updated_by",
82 models.ForeignKey(
83 blank=True,
84 db_constraint=False,
85 null=True,
86 on_delete=django.db.models.deletion.DO_NOTHING,
87 related_name="+",
88 to=settings.AUTH_USER_MODEL,
89 ),
90 ),
91 ],
92 options={
93 "verbose_name": "historical organization",
94 "verbose_name_plural": "historical organizations",
95 "ordering": ("-history_date", "-history_id"),
96 "get_latest_by": ("history_date", "history_id"),
97 },
98 bases=(simple_history.models.HistoricalChanges, models.Model),
99 ),
100 migrations.CreateModel(
101 name="Organization",
102 fields=[
103 (
104 "id",
105 models.BigAutoField(
106 auto_created=True,
107 primary_key=True,
108 serialize=False,
109 verbose_name="ID",
110 ),
111 ),
112 ("version", models.PositiveIntegerField(default=1, editable=False)),
113 ("created_at", models.DateTimeField(auto_now_add=True)),
114 ("updated_at", models.DateTimeField(auto_now=True)),
115 ("deleted_at", models.DateTimeField(blank=True, null=True)),
116 (
117 "guid",
118 models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True),
119 ),
120 ("name", models.CharField(max_length=200)),
121 ("slug", models.SlugField(max_length=200, unique=True)),
122 ("description", models.TextField(blank=True, default="")),
123 ("website", models.URLField(blank=True, default="")),
124 (
125 "created_by",
126 models.ForeignKey(
127 blank=True,
128 null=True,
129 on_delete=django.db.models.deletion.SET_NULL,
130 related_name="+",
131 to=settings.AUTH_USER_MODEL,
132 ),
133 ),
134 (
135 "deleted_by",
136 models.ForeignKey(
137 blank=True,
138 null=True,
139 on_delete=django.db.models.deletion.SET_NULL,
140 related_name="+",
141 to=settings.AUTH_USER_MODEL,
142 ),
143 ),
144 (
145 "groups",
146 models.ManyToManyField(blank=True, related_name="organizations", to="auth.group"),
147 ),
148 (
149 "updated_by",
150 models.ForeignKey(
151 blank=True,
152 null=True,
153 on_delete=django.db.models.deletion.SET_NULL,
154 related_name="+",
155 to=settings.AUTH_USER_MODEL,
156 ),
157 ),
158 ],
159 options={
160 "ordering": ["name"],
161 },
162 ),
163 migrations.CreateModel(
164 name="HistoricalOrganizationMember",
165 fields=[
166 (
167 "id",
168 models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"),
169 ),
170 ("version", models.PositiveIntegerField(default=1, editable=False)),
171 ("created_at", models.DateTimeField(blank=True, editable=False)),
172 ("updated_at", models.DateTimeField(blank=True, editable=False)),
173 ("deleted_at", models.DateTimeField(blank=True, null=True)),
174 ("is_active", models.BooleanField(default=True)),
175 ("history_id", models.AutoField(primary_key=True, serialize=False)),
176 ("history_date", models.DateTimeField(db_index=True)),
177 ("history_change_reason", models.CharField(max_length=100, null=True)),
178 (
179 "history_type",
180 models.CharField(
181 choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
182 max_length=1,
183 ),
184 ),
185 (
186 "created_by",
187 models.ForeignKey(
188 blank=True,
189 db_constraint=False,
190 null=True,
191 on_delete=django.db.models.deletion.DO_NOTHING,
192 related_name="+",
193 to=settings.AUTH_USER_MODEL,
194 ),
195 ),
196 (
197 "deleted_by",
198 models.ForeignKey(
199 blank=True,
200 db_constraint=False,
201 null=True,
202 on_delete=django.db.models.deletion.DO_NOTHING,
203 related_name="+",
204 to=settings.AUTH_USER_MODEL,
205 ),
206 ),
207 (
208 "history_user",
209 models.ForeignKey(
210 null=True,
211 on_delete=django.db.models.deletion.SET_NULL,
212 related_name="+",
213 to=settings.AUTH_USER_MODEL,
214 ),
215 ),
216 (
217 "member",
218 models.ForeignKey(
219 blank=True,
220 db_constraint=False,
221 null=True,
222 on_delete=django.db.models.deletion.DO_NOTHING,
223 related_name="+",
224 to=settings.AUTH_USER_MODEL,
225 ),
226 ),
227 (
228 "updated_by",
229 models.ForeignKey(
230 blank=True,
231 db_constraint=False,
232 null=True,
233 on_delete=django.db.models.deletion.DO_NOTHING,
234 related_name="+",
235 to=settings.AUTH_USER_MODEL,
236 ),
237 ),
238 (
239 "organization",
240 models.ForeignKey(
241 blank=True,
242 db_constraint=False,
243 null=True,
244 on_delete=django.db.models.deletion.DO_NOTHING,
245 related_name="+",
246 to="organization.organization",
247 ),
248 ),
249 ],
250 options={
251 "verbose_name": "historical organization member",
252 "verbose_name_plural": "historical organization members",
253 "ordering": ("-history_date", "-history_id"),
254 "get_latest_by": ("history_date", "history_id"),
255 },
256 bases=(simple_history.models.HistoricalChanges, models.Model),
257 ),
258 migrations.CreateModel(
259 name="OrganizationMember",
260 fields=[
261 (
262 "id",
263 models.BigAutoField(
264 auto_created=True,
265 primary_key=True,
266 serialize=False,
267 verbose_name="ID",
268 ),
269 ),
270 ("version", models.PositiveIntegerField(default=1, editable=False)),
271 ("created_at", models.DateTimeField(auto_now_add=True)),
272 ("updated_at", models.DateTimeField(auto_now=True)),
273 ("deleted_at", models.DateTimeField(blank=True, null=True)),
274 ("is_active", models.BooleanField(default=True)),
275 (
276 "created_by",
277 models.ForeignKey(
278 blank=True,
279 null=True,
280 on_delete=django.db.models.deletion.SET_NULL,
281 related_name="+",
282 to=settings.AUTH_USER_MODEL,
283 ),
284 ),
285 (
286 "deleted_by",
287 models.ForeignKey(
288 blank=True,
289 null=True,
290 on_delete=django.db.models.deletion.SET_NULL,
291 related_name="+",
292 to=settings.AUTH_USER_MODEL,
293 ),
294 ),
295 (
296 "groups",
297 models.ManyToManyField(blank=True, related_name="org_memberships", to="auth.group"),
298 ),
299 (
300 "member",
301 models.ForeignKey(
302 on_delete=django.db.models.deletion.CASCADE,
303 related_name="memberships",
304 to=settings.AUTH_USER_MODEL,
305 ),
306 ),
307 (
308 "organization",
309 models.ForeignKey(
310 on_delete=django.db.models.deletion.CASCADE,
311 related_name="members",
312 to="organization.organization",
313 ),
314 ),
315 (
316 "updated_by",
317 models.ForeignKey(
318 blank=True,
319 null=True,
320 on_delete=django.db.models.deletion.SET_NULL,
321 related_name="+",
322 to=settings.AUTH_USER_MODEL,
323 ),
324 ),
325 ],
326 options={
327 "unique_together": {("member", "organization")},
328 },
329 ),
330 ]
--- a/organization/models.py
+++ b/organization/models.py
@@ -0,0 +1,29 @@
1
+from django.contrib.auth.models import Group
2
+from django.db import models
3
+
4
+from core.models import ActiveManager, BaseCoreModel, Tracking
5
+
6
+
7
+class Organization(BaseCoreModel):
8
+ website = models.URLField(blank=True, default="")
9
+ groups = models.ManyToManyField(Group, blank=True, related_name="organizations")
10
+
11
+ objects = ActiveManager()
12
+ all_objects = models.Manager()
13
+
14
+ class Meta:
15
+ ordering = ["name"]
16
+
17
+
18
+class OrganizationMember(Tracking):
19
+ is_active = models.BooleanField(default=True)
20
+ member = models.ForeignKey("auth.User", on_delete=models.CASCADE, related_name="memberships")
21
+ organization = models.ForeignKey(Organization, on_delete=models.CASCADE, rfrom django.contrib.auth.models import Group
22
+from django.db import models
23
+
24
+from core.models import ActiveManager, BaseCoreModel, Tracking
25
+
26
+
27
+class Organization(BaseCoreModel):
28
+ website = models.URLField(blank=True, default="")
29
+ groups = models.ManyToManyField(Grotrib.auth.models import Gro
--- a/organization/models.py
+++ b/organization/models.py
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/organization/models.py
+++ b/organization/models.py
@@ -0,0 +1,29 @@
1 from django.contrib.auth.models import Group
2 from django.db import models
3
4 from core.models import ActiveManager, BaseCoreModel, Tracking
5
6
7 class Organization(BaseCoreModel):
8 website = models.URLField(blank=True, default="")
9 groups = models.ManyToManyField(Group, blank=True, related_name="organizations")
10
11 objects = ActiveManager()
12 all_objects = models.Manager()
13
14 class Meta:
15 ordering = ["name"]
16
17
18 class OrganizationMember(Tracking):
19 is_active = models.BooleanField(default=True)
20 member = models.ForeignKey("auth.User", on_delete=models.CASCADE, related_name="memberships")
21 organization = models.ForeignKey(Organization, on_delete=models.CASCADE, rfrom django.contrib.auth.models import Group
22 from django.db import models
23
24 from core.models import ActiveManager, BaseCoreModel, Tracking
25
26
27 class Organization(BaseCoreModel):
28 website = models.URLField(blank=True, default="")
29 groups = models.ManyToManyField(Grotrib.auth.models import Gro
--- a/organization/tests.py
+++ b/organization/tests.py
@@ -0,0 +1,35 @@
1
+import pytest
2
+from django.contrib.auth.models import User
3
+
4
+from .models import Organ
5
+
6
+
7
+@pytest.mark.djangango_db
8
+class TestTeamation:
9
+ def test_create_organization(self):
10
+ org = Organization.objects.create(name="Acme Corp")
11
+ assert org.slug == "acme-corp"
12
+ assert org.guid is not None
13
+
14
+ def test_soft_delete_excludes_from_default_manager(self):
15
+ user = User.objects.create_user(username="test", password="x")
16
+ org = Organization.objects.create(name="DeleteMe")
17
+ org.soft_delete(user=user)
18
+ assert Organization.objects.filter(slug="deleteme").count() == 0
19
+ assert Organization.all_objects.filter(slug="deleteme").count() == 1
20
+
21
+
22
+@pytest.mark.django_db
23
+class TestOrganizationMember:
24
+ def test_create_membership(self, admin_user, org):
25
+ assert OrganizationMember.objects.filter(member=admin_user, organization=org).exists()
26
+
27
+ def test_unique_membership(self, admin_user, org):
28
+ from django.db import IntegrityError
29
+
30
+ with pytest.raises(IntegrityError):
31
+ OrganizationMember.objects.create(member=admin_user, organization=org)
32
+
33
+ def test_str_representation(self, admin_user, org):
34
+ member = OrganizationMember.objects.get(member=admin_user, organization=org)
35
+ assert str(member
--- a/organization/tests.py
+++ b/organization/tests.py
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/organization/tests.py
+++ b/organization/tests.py
@@ -0,0 +1,35 @@
1 import pytest
2 from django.contrib.auth.models import User
3
4 from .models import Organ
5
6
7 @pytest.mark.djangango_db
8 class TestTeamation:
9 def test_create_organization(self):
10 org = Organization.objects.create(name="Acme Corp")
11 assert org.slug == "acme-corp"
12 assert org.guid is not None
13
14 def test_soft_delete_excludes_from_default_manager(self):
15 user = User.objects.create_user(username="test", password="x")
16 org = Organization.objects.create(name="DeleteMe")
17 org.soft_delete(user=user)
18 assert Organization.objects.filter(slug="deleteme").count() == 0
19 assert Organization.all_objects.filter(slug="deleteme").count() == 1
20
21
22 @pytest.mark.django_db
23 class TestOrganizationMember:
24 def test_create_membership(self, admin_user, org):
25 assert OrganizationMember.objects.filter(member=admin_user, organization=org).exists()
26
27 def test_unique_membership(self, admin_user, org):
28 from django.db import IntegrityError
29
30 with pytest.raises(IntegrityError):
31 OrganizationMember.objects.create(member=admin_user, organization=org)
32
33 def test_str_representation(self, admin_user, org):
34 member = OrganizationMember.objects.get(member=admin_user, organization=org)
35 assert str(member
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
1
+[project]
2
+name =1.0"
3
+descrip-django-htmxtion = "Omnielf-hosted Fossil SCM fFossilrepo Django + HTMX template — server-rendered with progressive enhancementand."
4
+license = "MIT"
5
+requires-python = ">=3.12"
6
+readme = "README.md"
7
+authors = [
8
+ { name = "CONFLICT LLC", email = "[email protected]" },
9
+]
10
+keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"]
11
+classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Web Environment",
14
+ "Framework :: Django",
15
+ "Framework :: Django :: 5.1",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operat]opg2-binary>=2.9",
20
+ "redis>=5.0",
21
+ "celery[redis]>=5.4",
22
+ "django-celery-results>=2.5",
23
+ "django-celery-beat>=2.7",
24
+ "django-import-export>=4.0",
25
+ "django-simple-history>=3.7",
26
+ "django-ratelimit>=4.1",
27
+ "django-health-check>=3.18",
28
+ "django-constance[database]>=4.1",
29
+ "django-storages[s3]>=1.14",
30
+ "django-ses>=4.1",
31
+ "django-cors-headers>=4.4",
32
+ "gunicorn>=23.0",
33
+ testdata "click>=8.1",
34
+ "rich>=13.0",
35
+ "markdown>=3.6",
36
+ "requests>=2.31",
37
+ "cryptography>=43.0",
38
+ "mcp>=1.0",
39
+]
40
+
41
+[project.urls]
42
+Homepage = "https://fossilrepo.dev"
43
+Documentation = "https://fossilrepo.dev"
44
+Repository = "https://github.com/ConflictHQ/fossilrepo"
45
+Issues = "https://github.com/ConflictHQ/fossilrepo/issues"
46
+ = "https://fossilrepo.io"
47
+
48
+[project.scripts]
49
+fossilrepo-ctl = "ctl.main:cli"
50
+fossilrepo-mcp = "mcp_server.__main__:run"
51
+
52
+[project.optional-dependredis>=5.0",
53
+ "cackends"
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
1 [project]
2 name =1.0"
3 descrip-django-htmxtion = "Omnielf-hosted Fossil SCM fFossilrepo Django + HTMX template — server-rendered with progressive enhancementand."
4 license = "MIT"
5 requires-python = ">=3.12"
6 readme = "README.md"
7 authors = [
8 { name = "CONFLICT LLC", email = "[email protected]" },
9 ]
10 keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"]
11 classifiers = [
12 "Development Status :: 3 - Alpha",
13 "Environment :: Web Environment",
14 "Framework :: Django",
15 "Framework :: Django :: 5.1",
16 "Intended Audience :: Developers",
17 "Intended Audience :: System Administrators",
18 "License :: OSI Approved :: MIT License",
19 "Operat]opg2-binary>=2.9",
20 "redis>=5.0",
21 "celery[redis]>=5.4",
22 "django-celery-results>=2.5",
23 "django-celery-beat>=2.7",
24 "django-import-export>=4.0",
25 "django-simple-history>=3.7",
26 "django-ratelimit>=4.1",
27 "django-health-check>=3.18",
28 "django-constance[database]>=4.1",
29 "django-storages[s3]>=1.14",
30 "django-ses>=4.1",
31 "django-cors-headers>=4.4",
32 "gunicorn>=23.0",
33 testdata "click>=8.1",
34 "rich>=13.0",
35 "markdown>=3.6",
36 "requests>=2.31",
37 "cryptography>=43.0",
38 "mcp>=1.0",
39 ]
40
41 [project.urls]
42 Homepage = "https://fossilrepo.dev"
43 Documentation = "https://fossilrepo.dev"
44 Repository = "https://github.com/ConflictHQ/fossilrepo"
45 Issues = "https://github.com/ConflictHQ/fossilrepo/issues"
46 = "https://fossilrepo.io"
47
48 [project.scripts]
49 fossilrepo-ctl = "ctl.main:cli"
50 fossilrepo-mcp = "mcp_server.__main__:run"
51
52 [project.optional-dependredis>=5.0",
53 "cackends"
A run.sh
+78
--- a/run.sh
+++ b/run.sh
@@ -0,0 +1,78 @@
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+# Fossilrepo — Django HTMX
5
+# Usage: ./run.sh [command]
6
+
7
+COMPOSE_FILE=""
8
+
9
+if [ -f "docker-compose.yml" ]; then
10
+ COMPOSE_FILE="docker-compose.yml"
11
+elif [ -f "docker-compose.yaml" ]; then
12
+ COMPOSE_FILE="docker-compose.yaml"
13
+fi
14
+
15
+compose() {
16
+ if [ -n "$COMPOSE_FILE" ]; then
17
+ docker compose -f "$COMPOSE_FILE" "$@"
18
+ else
19
+ echo "No docker-compose file found"
20
+ exit 1
21
+ fi
22
+}
23
+
24
+case "${1:-help}" in
25
+ up|start)
26
+ compose up -d --build
27
+ echo "Waiting for services..."
28
+ sleep 5
29
+ compose exec -T backend python manage.py migrate --noinput 2>&1 | tail -3
30
+ echo ""
31
+ echo "Services running. Check status with: ./run.sh status"
32
+ ;;
33
+ down|stop)
34
+ compose down
35
+ ;;
36
+ restart)
37
+ compose down
38
+ compose up -d --build
39
+ ;;
40
+ status|ps)
41
+ compose ps
42
+ ;;
43
+ logs)
44
+ compose logs -f "${2:-}"
45
+ ;;
46
+ seed)
47
+ compose exec -T backend python manage.py migrate --noinput 2>&1 | tail -3
48
+ compose exec -T backend python manage.py seed
49
+ ;;
50
+ test)
51
+ compose exec backend python -m pytest --cov --cov-report=term-missing -v
52
+ ;;
53
+ lint)
54
+ compose exec backend python -m ruff check . && compose exec backend python -m ruff format --check .
55
+ ;;
56
+ shell)
57
+ compose exec backend bash
58
+ ;;
59
+ migrate)
60
+ compose exec backend python manage.py migrate
61
+ ;;
62
+ help|*)
63
+ echo "Usage: ./run.sh <command>"
64
+ echo ""
65
+ echo "Commands:"
66
+ echo " up, start Start all services"
67
+ echo " down, stop Stop all services"
68
+ echo " restart Restart all services"
69
+ echo " status, ps Show service status"
70
+ echo " logs [svc] Tail logs (optionally for one service)"
71
+ echo " seed Seed the database"
72
+ echo " test Run tests"
73
+ echo " lint Run linters"
74
+ echo " shell Open a shell in the backend"
75
+ echo " migrate Run database migrations"
76
+ echo " help Show this help"
77
+ ;;
78
+esac
--- a/run.sh
+++ b/run.sh
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/run.sh
+++ b/run.sh
@@ -0,0 +1,78 @@
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 # Fossilrepo — Django HTMX
5 # Usage: ./run.sh [command]
6
7 COMPOSE_FILE=""
8
9 if [ -f "docker-compose.yml" ]; then
10 COMPOSE_FILE="docker-compose.yml"
11 elif [ -f "docker-compose.yaml" ]; then
12 COMPOSE_FILE="docker-compose.yaml"
13 fi
14
15 compose() {
16 if [ -n "$COMPOSE_FILE" ]; then
17 docker compose -f "$COMPOSE_FILE" "$@"
18 else
19 echo "No docker-compose file found"
20 exit 1
21 fi
22 }
23
24 case "${1:-help}" in
25 up|start)
26 compose up -d --build
27 echo "Waiting for services..."
28 sleep 5
29 compose exec -T backend python manage.py migrate --noinput 2>&1 | tail -3
30 echo ""
31 echo "Services running. Check status with: ./run.sh status"
32 ;;
33 down|stop)
34 compose down
35 ;;
36 restart)
37 compose down
38 compose up -d --build
39 ;;
40 status|ps)
41 compose ps
42 ;;
43 logs)
44 compose logs -f "${2:-}"
45 ;;
46 seed)
47 compose exec -T backend python manage.py migrate --noinput 2>&1 | tail -3
48 compose exec -T backend python manage.py seed
49 ;;
50 test)
51 compose exec backend python -m pytest --cov --cov-report=term-missing -v
52 ;;
53 lint)
54 compose exec backend python -m ruff check . && compose exec backend python -m ruff format --check .
55 ;;
56 shell)
57 compose exec backend bash
58 ;;
59 migrate)
60 compose exec backend python manage.py migrate
61 ;;
62 help|*)
63 echo "Usage: ./run.sh <command>"
64 echo ""
65 echo "Commands:"
66 echo " up, start Start all services"
67 echo " down, stop Stop all services"
68 echo " restart Restart all services"
69 echo " status, ps Show service status"
70 echo " logs [svc] Tail logs (optionally for one service)"
71 echo " seed Seed the database"
72 echo " test Run tests"
73 echo " lint Run linters"
74 echo " shell Open a shell in the backend"
75 echo " migrate Run database migrations"
76 echo " help Show this help"
77 ;;
78 esac
+19
--- a/startup.py
+++ b/startup.py
@@ -0,0 +1,19 @@
1
+"""Docker container startup script. Runs migrations and starts the dev server."""
2
+
3
+import os
4
+import subprocess
5
+import sys
6
+
7
+
8
+def main():
9
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
10
+
11
+ print("Running migrations...")
12
+ subprocess.run([sys.executable, "manage.py", "migrate", "--noinput"], check=True)
13
+
14
+ print("Starting development server...")
15
+ subprocess.run([sys.executable, "manage.py", "runserver", "0.0.0.0:8000"], check=False)
16
+
17
+
18
+if __name__ == "__main__":
19
+ main()
--- a/startup.py
+++ b/startup.py
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/startup.py
+++ b/startup.py
@@ -0,0 +1,19 @@
1 """Docker container startup script. Runs migrations and starts the dev server."""
2
3 import os
4 import subprocess
5 import sys
6
7
8 def main():
9 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
10
11 print("Running migrations...")
12 subprocess.run([sys.executable, "manage.py", "migrate", "--noinput"], check=True)
13
14 print("Starting development server...")
15 subprocess.run([sys.executable, "manage.py", "runserver", "0.0.0.0:8000"], check=False)
16
17
18 if __name__ == "__main__":
19 main()
--- a/static/admin/css/dark_theme.css
+++ b/static/admin/css/dark_theme.css
@@ -0,0 +1,627 @@
1
+/*
2
+ * Fossilrepo Admin Theme — supports dark, light, and system-auto modes
3
+ * Brand palette:
4
+ * #2B2D2C charcoal (dark body background)
5
+ * #DC394C red (primary brand accent)
6
+ * #8B3138 crimson (secondary / hover states)
7
+ * #FAFAFA near-white (foreground on dark)
8
+ */
9
+
10
+/* ── Shared brand accents + dark defaults ───────────────────────────────── */
11
+
12
+:root {
13
+ /* Brand accents — unchanged across all themes */
14
+ --primary: #DC394C;
15
+ --secondary: #8B3138;
16
+ --accent: #DC394C;
17
+ --primary-fg: #FAFAFA;
18
+
19
+ --button-fg: #FAFAFA;
20
+ --button-bg: #DC394C;
21
+ --button-hover-bg: #c42d3f;
22
+ --default-button-bg: #8B3138;
23
+ --default-button-hover-bg:#6a1921;
24
+ --close-button-bg: #4a4c4b;
25
+ --close-button-hover-bg: #5a5c5b;
26
+ --delete-button-bg: #8B3138;
27
+ --delete-button-hover-bg: #6a1921;
28
+
29
+ --object-tools-fg: #FAFAFA;
30
+ --object-tools-bg: #8B3138;
31
+ --object-tools-hover-bg: #6a1921;
32
+
33
+ --breadcrumbs-bg: #DC394C;
34
+ --breadcrumbs-fg: #FAFAFA;
35
+ --breadcrumbs-link-fg: #FAFAFA;
36
+ --link-selected-fg: #DC394C;
37
+
38
+ /* Header stays dark in both themes for brand consistency */
39
+ --header-color: #FAFAFA;
40
+ --header-branding-color: #FAFAFA;
41
+ --header-bg: #1f2120;
42
+ --header-link-color: #FAFAFA;
43
+
44
+ /* Dark theme defaults (applied when no explicit data-theme is set) */
45
+ --body-fg: #FAFAFA;
46
+ --body-bg: #2B2D2C;
47
+ --body-quiet-color: #a8aaa9;
48
+ --body-loud-color: #ffffff;
49
+
50
+ --link-fg: #e8677a;
51
+ --link-hover-color: #f0929f;
52
+
53
+ --hairline-color: #3d3f3e;
54
+ --border-color: #3d3f3e;
55
+
56
+ --error-fg: #ff7a7a;
57
+ --message-success-bg: #173317;
58
+ --message-success-color: #7ddf7d;
59
+ --message-success-border: #2d5a2d;
60
+ --message-warning-bg: #332e17;
61
+ --message-warning-color: #e6c87a;
62
+ --message-warning-border: #5a4d2d;
63
+ --message-error-bg: #331717;
64
+ --message-error-color: #ff7a7a;
65
+ --message-error-border: #5a2d2d;
66
+
67
+ --darkened-bg: #222423;
68
+ --selected-bg: #3d1e24;
69
+ --selected-row: #3d1e24;
70
+ --input-bg: #1f2120;
71
+}
72
+
73
+/* ── Explicit dark theme (beats Django's html[data-theme="dark"] specificity) */
74
+
75
+:root[data-theme="dark"] {
76
+ --primary: #DC394C;
77
+ --secondary: #8B3138;
78
+ --accent: #DC394C;
79
+ --primary-fg: #FAFAFA;
80
+
81
+ --button-fg: #FAFAFA;
82
+ --button-bg: #DC394C;
83
+ --button-hover-bg: #c42d3f;
84
+ --default-button-bg: #8B3138;
85
+ --default-button-hover-bg:#6a1921;
86
+ --close-button-bg: #4a4c4b;
87
+ --close-button-hover-bg: #5a5c5b;
88
+ --delete-button-bg: #8B3138;
89
+ --delete-button-hover-bg: #6a1921;
90
+
91
+ --object-tools-fg: #FAFAFA;
92
+ --object-tools-bg: #8B3138;
93
+ --object-tools-hover-bg: #6a1921;
94
+
95
+ --breadcrumbs-bg: #DC394C;
96
+ --breadcrumbs-fg: #FAFAFA;
97
+ --breadcrumbs-link-fg: #FAFAFA;
98
+ --link-selected-fg: #DC394C;
99
+
100
+ --header-bg: #1f2120;
101
+ --header-color: #FAFAFA;
102
+ --header-branding-color: #FAFAFA;
103
+ --header-link-color: #FAFAFA;
104
+
105
+ --body-fg: #FAFAFA;
106
+ --body-bg: #0d0d0d;
107
+ --body-quiet-color: #a8aaa9;
108
+ --body-loud-color: #ffffff;
109
+
110
+ --link-fg: #e8677a;
111
+ --link-hover-color: #f0929f;
112
+
113
+ --hairline-color: #2a2c2b;
114
+ --border-color: #2a2c2b;
115
+
116
+ --error-fg: #ff7a7a;
117
+ --message-success-bg: #0f1f0f;
118
+ --message-success-color: #7ddf7d;
119
+ --message-success-border: #1e3d1e;
120
+ --message-warning-bg: #1f1c0f;
121
+ --message-warning-color: #e6c87a;
122
+ --message-warning-border: #3d3519;
123
+ --message-error-bg: #1f0f0f;
124
+ --message-error-color: #ff7a7a;
125
+ --message-error-border: #3d1a1a;
126
+
127
+ --darkened-bg: #141514;
128
+ --selected-bg: #2d1219;
129
+ --selected-row: #2d1219;
130
+ --input-bg: #111211;
131
+}
132
+
133
+/* ── Light theme ─────────────────────────────────────────────────────────── */
134
+
135
+:root[data-theme="light"] {
136
+ /* Header — dark gray in light mode */
137
+ --header-bg: #3a3a3a;
138
+ --header-color: #FAFAFA;
139
+ --header-branding-color: #FAFAFA;
140
+ --header-link-color: #FAFAFA;
141
+
142
+ /* Brand accents — must repeat here to beat base.css html[data-theme="light"] specificity */
143
+ --primary: #DC394C;
144
+ --secondary: #8B3138;
145
+ --accent: #DC394C;
146
+ --primary-fg: #FAFAFA;
147
+
148
+ --button-fg: #FAFAFA;
149
+ --button-bg: #DC394C;
150
+ --button-hover-bg: #c42d3f;
151
+ --default-button-bg: #8B3138;
152
+ --default-button-hover-bg:#6a1921;
153
+ --delete-button-bg: #8B3138;
154
+ --delete-button-hover-bg: #6a1921;
155
+
156
+ --object-tools-fg: #FAFAFA;
157
+ --object-tools-bg: #8B3138;
158
+ --object-tools-hover-bg: #6a1921;
159
+
160
+ --breadcrumbs-bg: #DC394C;
161
+ --breadcrumbs-fg: #FAFAFA;
162
+ --breadcrumbs-link-fg: #FAFAFA;
163
+ --link-selected-fg: #DC394C;
164
+
165
+ --body-fg: #1a1a1a;
166
+ --body-bg: #f8f8f8;
167
+ --body-quiet-color: #666666;
168
+ --body-loud-color: #000000;
169
+
170
+ --link-fg: #DC394C;
171
+ --link-hover-color: #8B3138;
172
+
173
+ --hairline-color: #e0e0e0;
174
+ --border-color: #e0e0e0;
175
+
176
+ --error-fg: #c0392b;
177
+ --message-success-bg: #d4edda;
178
+ --message-success-color: #155724;
179
+ --message-success-border: #c3e6cb;
180
+ --message-warning-bg: #fff3cd;
181
+ --message-warning-color: #856404;
182
+ --message-warning-border: #ffeeba;
183
+ --message-error-bg: #f8d7da;
184
+ --message-error-color: #721c24;
185
+ --message-error-border: #f5c6cb;
186
+
187
+ --darkened-bg: #eeeeee;
188
+ --selected-bg: #fde8ea;
189
+ --selected-row: #fde8ea;
190
+ --input-bg: #ffffff;
191
+}
192
+
193
+/* ── System auto: respect prefers-color-scheme light ─────────────────────── */
194
+
195
+@media (prefers-color-scheme: light) {
196
+ :root:not([data-theme="dark"]) {
197
+ /* Header — dark gray in light mode */
198
+ --header-bg: #3a3a3a;
199
+ --header-color: #FAFAFA;
200
+ --header-branding-color: #FAFAFA;
201
+ --header-link-color: #FAFAFA;
202
+
203
+ /* Brand accents */
204
+ --primary: #DC394C;
205
+ --secondary: #8B3138;
206
+ --accent: #DC394C;
207
+ --primary-fg: #FAFAFA;
208
+
209
+ --button-fg: #FAFAFA;
210
+ --button-bg: #DC394C;
211
+ --button-hover-bg: #c42d3f;
212
+ --default-button-bg: #8B3138;
213
+ --default-button-hover-bg:#6a1921;
214
+ --delete-button-bg: #8B3138;
215
+ --delete-button-hover-bg: #6a1921;
216
+
217
+ --object-tools-fg: #FAFAFA;
218
+ --object-tools-bg: #8B3138;
219
+ --object-tools-hover-bg: #6a1921;
220
+
221
+ --breadcrumbs-bg: #DC394C;
222
+ --breadcrumbs-fg: #FAFAFA;
223
+ --breadcrumbs-link-fg: #FAFAFA;
224
+ --link-selected-fg: #DC394C;
225
+
226
+ --body-fg: #1a1a1a;
227
+ --body-bg: #f8f8f8;
228
+ --body-quiet-color: #666666;
229
+ --body-loud-color: #000000;
230
+
231
+ --link-fg: #DC394C;
232
+ --link-hover-color: #8B3138;
233
+
234
+ --hairline-color: #e0e0e0;
235
+ --border-color: #e0e0e0;
236
+
237
+ --error-fg: #c0392b;
238
+ --message-success-bg: #d4edda;
239
+ --message-success-color: #155724;
240
+ --message-success-border: #c3e6cb;
241
+ --message-warning-bg: #fff3cd;
242
+ --message-warning-color: #856404;
243
+ --message-warning-border: #ffeeba;
244
+ --message-error-bg: #f8d7da;
245
+ --message-error-color: #721c24;
246
+ --message-error-border: #f5c6cb;
247
+
248
+ --darkened-bg: #eeeeee;
249
+ --selected-bg: #fde8ea;
250
+ --selected-row: #fde8ea;
251
+ --input-bg: #ffffff;
252
+ }
253
+}
254
+
255
+
256
+/* ── Layout ─────────────────────────────────────────────────────────────── */
257
+
258
+/*
259
+ * Django's stock base.css gives `.module { background: var(--darkened-bg) }`.
260
+ * #changelist has class "module", so the 30px margin gap between the table and
261
+ * the filter also gets --darkened-bg — the same color as the filter itself.
262
+ * Result: no visible gap.
263
+ *
264
+ * clientcove fixes this by shipping a modified base.css with
265
+ * `.module { background: var(--body-bg) }`. We can't change Django's base.css,
266
+ * so we target #changelist specifically to make the gap show the body background.
267
+ */
268
+#changelist.module {
269
+ background: transparent;
270
+ border: none;
271
+}
272
+
273
+/* Prevent table from overflowing into the filter gap — layout owned by changelists.css */
274
+#changelist .changelist-form-container > div {
275
+ overflow-x: auto;
276
+}
277
+
278
+/* ── Header — always dark, hardcoded values intentional ─────────────────── */
279
+
280
+#header {
281
+ background: var(--header-bg);
282
+ border-bottom: 2px solid var(--primary);
283
+}
284
+
285
+#header a:link,
286
+#header a:visited {
287
+ color: var(--header-link-color);
288
+}
289
+
290
+#header #branding h1 {
291
+ line-height: 1;
292
+ margin: 0;
293
+}
294
+
295
+#header #branding .logo img {
296
+ pe="button"]:hoverblock;
297
+}
298
+
299
+#header #user-tools {
300
+ color: #a8aaa9;
301
+}
302
+
303
+#header #user-tools a {
304
+ color:
305
+ * Fossilrepo Admin Th�� supports dark, light, and system-auto modes
306
+ * Brand palette:
307
+ * #2B2D2C charcoal (dark body background)
308
+ * #DC394C red (primary brand accent)
309
+ * #8B3138 crimson (secondary / hover states)
310
+ * #FAFAFA near-white (foreground on dark)
311
+ */
312
+
313
+/* ── Shared brand accents + dark defaults ───────────────────────────────── */
314
+
315
+:root {
316
+ /* Brand accents — unchanged across all themes */
317
+ --primary: #DC394C;
318
+ --secondary: #8B3138;
319
+ --accent: #DC394C;
320
+ --primary-fg: #FAFAFA;
321
+
322
+ --button-fg: #FAFAFA;
323
+ --button-bg: #DC394C;
324
+ --button-hover-bg: #c42d3f;
325
+ --default-button-bg: #8B3138;
326
+ --default-button-hover-bg:#6a1921;
327
+ --close-button-bg: #4a4c4b;
328
+ --close-button-hover-bg: #5a5c5b;
329
+ --delete-button-bg: #8B3138;
330
+ --delete-button-hover-bg: #6a1921;
331
+
332
+ --object-tools-fg: #FAFAFA;
333
+ --object-tools-bg: #8B3138;
334
+ --object-tools-hover-bg: #6a1921;
335
+
336
+ --breadcrumbs-bg: #DC394C;
337
+ --breadcrumbs-fg: #FAFAFA;
338
+ --breadcrumbs-link-fg: #FAFAFA;
339
+ --link-selected-fg: #DC394C;
340
+
341
+ /* Header stays dark in both themes for brand consistency */
342
+ --header-color: #FAFAFA;
343
+ --header-branding-color: #FAFAFA;
344
+ --header-bg: #1f2120;
345
+ --header-link-color: #FAFAFA;
346
+
347
+ /* Dark theme defaults (applied when no explicit data-theme is set) */
348
+ --body-fg: #FAFAFA;
349
+ --body-bg: #2B2D2C;
350
+ --body-quiet-color: #a8aaa9;
351
+ --body-loud-color: #ffffff;
352
+
353
+ --link-fg: #e8677a;
354
+ --link-hover-color: #f0929f;
355
+
356
+ --hairline-color: #3d3f3e;
357
+ --border-color: #3d3f3e;
358
+
359
+ --error-fg: #ff7a7a;
360
+ --message-success-bg: #173317;
361
+ --message-success-color: #7ddf7d;
362
+ --message-success-border: #2d5a2d;
363
+ --message-warning-bg: #332e17;
364
+ --message-warning-color: #e6c87a;
365
+ --message-warning-border: #5a4d2d;
366
+ --message-error-bg: #331717;
367
+ --message-error-color: #ff7a7a;
368
+ --message-error-border: #5a2d2d;
369
+
370
+ --darkened-bg: #222423;
371
+ --selected-bg: #3d1e24;
372
+ --selected-row: #3d1e24;
373
+ --input-bg: #1f2120;
374
+}
375
+
376
+/* ── Explicit dark theme (beats Django's html[data-theme="dark"] specificity) */
377
+
378
+:root[data-theme="dark"] {
379
+ --primary: #DC394C;
380
+ --secondary: #8B3138;
381
+ --accent: #DC394C;
382
+ --primary-fg: #FAFAFA;
383
+
384
+ --button-fg: #FAFAFA;
385
+ --button-bg: #DC394C;
386
+ --button-hover-bg: #c42d3f;
387
+ --default-button-bg: #8B3138;
388
+ --default-button-hover-bg:#6a1921;
389
+ --close-button-bg: #4a4c4b;
390
+ --close-button-hover-bg: #5a5c5b;
391
+ --delete-button-bg: #8B3138;
392
+ --delete-button-hover-bg: #6a1921;
393
+
394
+ --object-tools-fg: #FAFAFA;
395
+ --object-tools-bg: #8B3138;
396
+ --object-tools-hover-bg: #6a1921;
397
+
398
+ --breadcrumbs-bg: #DC394C;
399
+ --breadcrumbs-fg: #FAFAFA;
400
+ --breadcrumbs-link-fg: #FAFAFA;
401
+ --link-selected-fg: #DC394C;
402
+
403
+ --header-bg: #1f2120;
404
+ --header-color: #FAFAFA;
405
+ --header-branding-color: #FAFAFA;
406
+ --header-link-color: #FAFAFA;
407
+
408
+ --body-fg: #FAFAFA;
409
+ --body-bg: #0d0d0d;
410
+ --body-quiet-color: #a8aaa9;
411
+ --body-loud-color: #ffffff;
412
+
413
+ --link-fg: #e8677a;
414
+ --link-hover-color: #f0929f;
415
+
416
+ --hairline-color: #2a2c2b;
417
+ --border-color: #2a2c2b;
418
+
419
+ --error-fg: #ff7a7a;
420
+ --message-success-bg: #0f1f0f;
421
+ --message-success-color: #7ddf7d;
422
+ --message-success-border: #1e3d1e;
423
+ --message-warning-bg: #1f1c0f;
424
+ --message-warning-color: #e6c87a;
425
+ --message-warning-border: #3d3519;
426
+ --message-error-bg: #1f0f0f;
427
+ --message-error-color: #ff7a7a;
428
+ --message-error-border: #3d1a1a;
429
+
430
+ --darkened-bg: #141514;
431
+ --selected-bg: #2d1219;
432
+ --selected-row: #2d1219;
433
+ --input-bg: #111211;
434
+}
435
+
436
+/* ── Light theme ─────────────────────────────────────────────────────────── */
437
+
438
+:root[data-theme="light"] {
439
+ /* Header — dark gray in light mode */
440
+ --header-bg: #3a3a3a;
441
+ --header-color: #FAFAFA;
442
+ --header-branding-color: #FAFAFA;
443
+ --header-link-color: #FAFAFA;
444
+
445
+ /* Brand accents — must repeat here to beat base.css html[data-theme="light"] specificity */
446
+ --primary: #DC394C;
447
+ --secondary: #8B3138;
448
+ --accent: #DC394C;
449
+ --primary-fg: #FAFAFA;
450
+
451
+ --button-fg: #FAFAFA;
452
+ --button-bg: #DC394C;
453
+ --button-hover-bg: #c42d3f;
454
+ --default-button-bg: #8B3138;
455
+ --default-button-hover-bg:#6a1921;
456
+ --delete-button-bg: #8B3138;
457
+ --delete-button-hover-bg: #6a1921;
458
+
459
+ --object-tools-fg: #FAFAFA;
460
+ --object-tools-bg: #8B3138;
461
+ --object-tools-hover-bg: #6a1921;
462
+
463
+ --breadcrumbs-bg: #DC394C;
464
+ --breadcrumbs-fg: #FAFAFA;
465
+ --breadcrumbs-link-fg: #FAFAFA;
466
+ --link-selected-fg: #DC394C;
467
+
468
+ --body-fg: #1a1a1a;
469
+ --body-bg: #f8f8f8;
470
+ --body-quiet-color: #666666;
471
+ --body-loud-color: #000000;
472
+
473
+ --link-fg: #DC394C;
474
+ --link-hover-color: #8B3138;
475
+
476
+ --hairline-color: #e0e0e0;
477
+ --border-color: #e0e0e0;
478
+
479
+ --error-fg: #c0392b;
480
+ --message-success-bg: #d4edda;
481
+ --message-success-color: #155724;
482
+ --message-success-border: #c3e6cb;
483
+ --message-warning-bg: #fff3cd;
484
+ --message-warning-color: #856404;
485
+ --message-warning-border: #ffeeba;
486
+ --message-error-bg: #f8d7da;
487
+ --message-error-color: #721c24;
488
+ --message-error-border: #f5c6cb;
489
+
490
+ --darkened-bg: #eeeeee;
491
+ --selected-bg: #fde8ea;
492
+ --selected-row: #fde8ea;
493
+ --input-bg: #ffffff;
494
+}
495
+
496
+/* ── System auto: respect prefers-color-scheme light ─────────────────────── */
497
+
498
+@media (prefers-color-scheme: light) {
499
+ :root:not([data-theme="dark"]) {
500
+ /* Header — dark gray in light mode */
501
+ --header-bg: #3a3a3a;
502
+ --header-color: #FAFAFA;
503
+ --header-branding-color: #FAFAFA;
504
+ --header-link-color: #FAFAFA;
505
+
506
+ /* Brand accents */
507
+ --primary: #DC394C;
508
+ --secondary: #8B3138;
509
+ --accent: #DC394C;
510
+ --primary-fg: #FAFAFA;
511
+
512
+ --button-fg: #FAFAFA;
513
+ --button-bg: #DC394C;
514
+ --button-hover-bg: #c42d3f;
515
+ --default-button-bg: #8B3138;
516
+ --default-button-hover-bg:#6a1921;
517
+ --delete-button-bg: #8B3138;
518
+ --delete-button-hover-bg: #6a1921;
519
+
520
+ --object-tools-fg: #FAFAFA;
521
+ --object-tools-bg: #8B3138;
522
+ --object-tools-hover-bg: #6a1921;
523
+
524
+ --breadcrumbs-bg: #DC394C;
525
+ --breadcrumbs-fg: #FAFAFA;
526
+ --breadcrumbs-link-fg: #FAFAFA;
527
+ --link-selected-fg: #DC394C;
528
+
529
+ --body-fg: #1a1a1a;
530
+ --body-bg: #f8f8f8;
531
+ --body-quiet-color: #666666;
532
+ --body-loud-color: #000000;
533
+
534
+ --link-fg: #DC394C;
535
+ --link-hover-color: #8B3138;
536
+
537
+ --hairline-color: #e0e0e0;
538
+ --border-color: #e0e0e0;
539
+
540
+ --error-fg: #c0392b;
541
+ --message-success-bg: #d4edda;
542
+ --message-success-color: #155724;
543
+ --message-success-border: #c3e6cb;
544
+ --message-warning-bg: #fff3cd;
545
+ --message-warning-color: #856404;
546
+ --message-warning-border: #ffeeba;
547
+ --message-error-bg: #f8d7da;
548
+ --message-error-color: #721c24;
549
+ --message-error-border: #f5c6cb;
550
+
551
+ --darkened-bg: #eeeeee;
552
+ --selected-bg: #fde8ea;
553
+ --selected-row: #fde8ea;
554
+ --input-bg: #ffffff;
555
+ }
556
+}
557
+
558
+
559
+/* ── Layout ─────────────────────────────────────────────────────────────── */
560
+
561
+/*
562
+ * Django's stock base.css gives `.module { background: var(--darkened-bg) }`.
563
+ * #changelist has class "module", so the 30px margin gap between the table and
564
+ * the filter also gets --darkened-bg — the same color as the filter itself.
565
+ * Result: no visible gap.
566
+ *
567
+ * clientcove fixes this by shipping a modified base.css with
568
+ * `.module { background: var(--body-bg) }`. We can't change Django's base.css,
569
+ * so we target #changelist specifically to make the gap show the body background.
570
+ */
571
+#changelist.module {
572
+ background: transparent;
573
+ border: none;
574
+}
575
+
576
+/* Prevent table from overflowing into the filter gap — layout owned by changelists.css */
577
+#changelist .changelist-form-container > div {
578
+ overflow-x: auto;
579
+}
580
+
581
+/* ── Header — always dark, hardcoded values intentional ─────────────────── */
582
+
583
+#header {
584
+ background: var(--header-bg);
585
+ border-bottom: 2px solid var(--primary);
586
+}
587
+
588
+#header a:link,
589
+#header a:visited {
590
+ color: var(--header-link-color);
591
+}
592
+
593
+#header #branding h1 {
594
+ line-height: 1;
595
+ margin: 0;
596
+}
597
+
598
+#header #branding .logo img {
599
+ height: 40px;
600
+ width: auto;
601
+ display: block;
602
+}
603
+
604
+#header #user-tools {
605
+ color: #a8aaa9;
606
+}
607
+
608
+#header #user-tools a {
609
+ color: #a8aaa9;
610
+}
611
+
612
+#header #user-tools a:hover {
613
+ color: #FAFAFA;
614
+}
615
+
616
+/* Quick-links group in header */
617
+.bw-links {
618
+ display: inline-flex;
619
+ gap: 2px;
620
+ margin-right: 8px;
621
+ border-right: 1px solid #3d3f3e;
622
+ padding-right: 10px;
623
+}
624
+
625
+.bw-links a {
626
+ display: inline-block;
627
+ pad
--- a/static/admin/css/dark_theme.css
+++ b/static/admin/css/dark_theme.css
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/static/admin/css/dark_theme.css
+++ b/static/admin/css/dark_theme.css
@@ -0,0 +1,627 @@
1 /*
2 * Fossilrepo Admin Theme — supports dark, light, and system-auto modes
3 * Brand palette:
4 * #2B2D2C charcoal (dark body background)
5 * #DC394C red (primary brand accent)
6 * #8B3138 crimson (secondary / hover states)
7 * #FAFAFA near-white (foreground on dark)
8 */
9
10 /* ── Shared brand accents + dark defaults ───────────────────────────────── */
11
12 :root {
13 /* Brand accents — unchanged across all themes */
14 --primary: #DC394C;
15 --secondary: #8B3138;
16 --accent: #DC394C;
17 --primary-fg: #FAFAFA;
18
19 --button-fg: #FAFAFA;
20 --button-bg: #DC394C;
21 --button-hover-bg: #c42d3f;
22 --default-button-bg: #8B3138;
23 --default-button-hover-bg:#6a1921;
24 --close-button-bg: #4a4c4b;
25 --close-button-hover-bg: #5a5c5b;
26 --delete-button-bg: #8B3138;
27 --delete-button-hover-bg: #6a1921;
28
29 --object-tools-fg: #FAFAFA;
30 --object-tools-bg: #8B3138;
31 --object-tools-hover-bg: #6a1921;
32
33 --breadcrumbs-bg: #DC394C;
34 --breadcrumbs-fg: #FAFAFA;
35 --breadcrumbs-link-fg: #FAFAFA;
36 --link-selected-fg: #DC394C;
37
38 /* Header stays dark in both themes for brand consistency */
39 --header-color: #FAFAFA;
40 --header-branding-color: #FAFAFA;
41 --header-bg: #1f2120;
42 --header-link-color: #FAFAFA;
43
44 /* Dark theme defaults (applied when no explicit data-theme is set) */
45 --body-fg: #FAFAFA;
46 --body-bg: #2B2D2C;
47 --body-quiet-color: #a8aaa9;
48 --body-loud-color: #ffffff;
49
50 --link-fg: #e8677a;
51 --link-hover-color: #f0929f;
52
53 --hairline-color: #3d3f3e;
54 --border-color: #3d3f3e;
55
56 --error-fg: #ff7a7a;
57 --message-success-bg: #173317;
58 --message-success-color: #7ddf7d;
59 --message-success-border: #2d5a2d;
60 --message-warning-bg: #332e17;
61 --message-warning-color: #e6c87a;
62 --message-warning-border: #5a4d2d;
63 --message-error-bg: #331717;
64 --message-error-color: #ff7a7a;
65 --message-error-border: #5a2d2d;
66
67 --darkened-bg: #222423;
68 --selected-bg: #3d1e24;
69 --selected-row: #3d1e24;
70 --input-bg: #1f2120;
71 }
72
73 /* ── Explicit dark theme (beats Django's html[data-theme="dark"] specificity) */
74
75 :root[data-theme="dark"] {
76 --primary: #DC394C;
77 --secondary: #8B3138;
78 --accent: #DC394C;
79 --primary-fg: #FAFAFA;
80
81 --button-fg: #FAFAFA;
82 --button-bg: #DC394C;
83 --button-hover-bg: #c42d3f;
84 --default-button-bg: #8B3138;
85 --default-button-hover-bg:#6a1921;
86 --close-button-bg: #4a4c4b;
87 --close-button-hover-bg: #5a5c5b;
88 --delete-button-bg: #8B3138;
89 --delete-button-hover-bg: #6a1921;
90
91 --object-tools-fg: #FAFAFA;
92 --object-tools-bg: #8B3138;
93 --object-tools-hover-bg: #6a1921;
94
95 --breadcrumbs-bg: #DC394C;
96 --breadcrumbs-fg: #FAFAFA;
97 --breadcrumbs-link-fg: #FAFAFA;
98 --link-selected-fg: #DC394C;
99
100 --header-bg: #1f2120;
101 --header-color: #FAFAFA;
102 --header-branding-color: #FAFAFA;
103 --header-link-color: #FAFAFA;
104
105 --body-fg: #FAFAFA;
106 --body-bg: #0d0d0d;
107 --body-quiet-color: #a8aaa9;
108 --body-loud-color: #ffffff;
109
110 --link-fg: #e8677a;
111 --link-hover-color: #f0929f;
112
113 --hairline-color: #2a2c2b;
114 --border-color: #2a2c2b;
115
116 --error-fg: #ff7a7a;
117 --message-success-bg: #0f1f0f;
118 --message-success-color: #7ddf7d;
119 --message-success-border: #1e3d1e;
120 --message-warning-bg: #1f1c0f;
121 --message-warning-color: #e6c87a;
122 --message-warning-border: #3d3519;
123 --message-error-bg: #1f0f0f;
124 --message-error-color: #ff7a7a;
125 --message-error-border: #3d1a1a;
126
127 --darkened-bg: #141514;
128 --selected-bg: #2d1219;
129 --selected-row: #2d1219;
130 --input-bg: #111211;
131 }
132
133 /* ── Light theme ─────────────────────────────────────────────────────────── */
134
135 :root[data-theme="light"] {
136 /* Header — dark gray in light mode */
137 --header-bg: #3a3a3a;
138 --header-color: #FAFAFA;
139 --header-branding-color: #FAFAFA;
140 --header-link-color: #FAFAFA;
141
142 /* Brand accents — must repeat here to beat base.css html[data-theme="light"] specificity */
143 --primary: #DC394C;
144 --secondary: #8B3138;
145 --accent: #DC394C;
146 --primary-fg: #FAFAFA;
147
148 --button-fg: #FAFAFA;
149 --button-bg: #DC394C;
150 --button-hover-bg: #c42d3f;
151 --default-button-bg: #8B3138;
152 --default-button-hover-bg:#6a1921;
153 --delete-button-bg: #8B3138;
154 --delete-button-hover-bg: #6a1921;
155
156 --object-tools-fg: #FAFAFA;
157 --object-tools-bg: #8B3138;
158 --object-tools-hover-bg: #6a1921;
159
160 --breadcrumbs-bg: #DC394C;
161 --breadcrumbs-fg: #FAFAFA;
162 --breadcrumbs-link-fg: #FAFAFA;
163 --link-selected-fg: #DC394C;
164
165 --body-fg: #1a1a1a;
166 --body-bg: #f8f8f8;
167 --body-quiet-color: #666666;
168 --body-loud-color: #000000;
169
170 --link-fg: #DC394C;
171 --link-hover-color: #8B3138;
172
173 --hairline-color: #e0e0e0;
174 --border-color: #e0e0e0;
175
176 --error-fg: #c0392b;
177 --message-success-bg: #d4edda;
178 --message-success-color: #155724;
179 --message-success-border: #c3e6cb;
180 --message-warning-bg: #fff3cd;
181 --message-warning-color: #856404;
182 --message-warning-border: #ffeeba;
183 --message-error-bg: #f8d7da;
184 --message-error-color: #721c24;
185 --message-error-border: #f5c6cb;
186
187 --darkened-bg: #eeeeee;
188 --selected-bg: #fde8ea;
189 --selected-row: #fde8ea;
190 --input-bg: #ffffff;
191 }
192
193 /* ── System auto: respect prefers-color-scheme light ─────────────────────── */
194
195 @media (prefers-color-scheme: light) {
196 :root:not([data-theme="dark"]) {
197 /* Header — dark gray in light mode */
198 --header-bg: #3a3a3a;
199 --header-color: #FAFAFA;
200 --header-branding-color: #FAFAFA;
201 --header-link-color: #FAFAFA;
202
203 /* Brand accents */
204 --primary: #DC394C;
205 --secondary: #8B3138;
206 --accent: #DC394C;
207 --primary-fg: #FAFAFA;
208
209 --button-fg: #FAFAFA;
210 --button-bg: #DC394C;
211 --button-hover-bg: #c42d3f;
212 --default-button-bg: #8B3138;
213 --default-button-hover-bg:#6a1921;
214 --delete-button-bg: #8B3138;
215 --delete-button-hover-bg: #6a1921;
216
217 --object-tools-fg: #FAFAFA;
218 --object-tools-bg: #8B3138;
219 --object-tools-hover-bg: #6a1921;
220
221 --breadcrumbs-bg: #DC394C;
222 --breadcrumbs-fg: #FAFAFA;
223 --breadcrumbs-link-fg: #FAFAFA;
224 --link-selected-fg: #DC394C;
225
226 --body-fg: #1a1a1a;
227 --body-bg: #f8f8f8;
228 --body-quiet-color: #666666;
229 --body-loud-color: #000000;
230
231 --link-fg: #DC394C;
232 --link-hover-color: #8B3138;
233
234 --hairline-color: #e0e0e0;
235 --border-color: #e0e0e0;
236
237 --error-fg: #c0392b;
238 --message-success-bg: #d4edda;
239 --message-success-color: #155724;
240 --message-success-border: #c3e6cb;
241 --message-warning-bg: #fff3cd;
242 --message-warning-color: #856404;
243 --message-warning-border: #ffeeba;
244 --message-error-bg: #f8d7da;
245 --message-error-color: #721c24;
246 --message-error-border: #f5c6cb;
247
248 --darkened-bg: #eeeeee;
249 --selected-bg: #fde8ea;
250 --selected-row: #fde8ea;
251 --input-bg: #ffffff;
252 }
253 }
254
255
256 /* ── Layout ─────────────────────────────────────────────────────────────── */
257
258 /*
259 * Django's stock base.css gives `.module { background: var(--darkened-bg) }`.
260 * #changelist has class "module", so the 30px margin gap between the table and
261 * the filter also gets --darkened-bg — the same color as the filter itself.
262 * Result: no visible gap.
263 *
264 * clientcove fixes this by shipping a modified base.css with
265 * `.module { background: var(--body-bg) }`. We can't change Django's base.css,
266 * so we target #changelist specifically to make the gap show the body background.
267 */
268 #changelist.module {
269 background: transparent;
270 border: none;
271 }
272
273 /* Prevent table from overflowing into the filter gap — layout owned by changelists.css */
274 #changelist .changelist-form-container > div {
275 overflow-x: auto;
276 }
277
278 /* ── Header — always dark, hardcoded values intentional ─────────────────── */
279
280 #header {
281 background: var(--header-bg);
282 border-bottom: 2px solid var(--primary);
283 }
284
285 #header a:link,
286 #header a:visited {
287 color: var(--header-link-color);
288 }
289
290 #header #branding h1 {
291 line-height: 1;
292 margin: 0;
293 }
294
295 #header #branding .logo img {
296 pe="button"]:hoverblock;
297 }
298
299 #header #user-tools {
300 color: #a8aaa9;
301 }
302
303 #header #user-tools a {
304 color:
305 * Fossilrepo Admin Th�� supports dark, light, and system-auto modes
306 * Brand palette:
307 * #2B2D2C charcoal (dark body background)
308 * #DC394C red (primary brand accent)
309 * #8B3138 crimson (secondary / hover states)
310 * #FAFAFA near-white (foreground on dark)
311 */
312
313 /* ── Shared brand accents + dark defaults ───────────────────────────────── */
314
315 :root {
316 /* Brand accents — unchanged across all themes */
317 --primary: #DC394C;
318 --secondary: #8B3138;
319 --accent: #DC394C;
320 --primary-fg: #FAFAFA;
321
322 --button-fg: #FAFAFA;
323 --button-bg: #DC394C;
324 --button-hover-bg: #c42d3f;
325 --default-button-bg: #8B3138;
326 --default-button-hover-bg:#6a1921;
327 --close-button-bg: #4a4c4b;
328 --close-button-hover-bg: #5a5c5b;
329 --delete-button-bg: #8B3138;
330 --delete-button-hover-bg: #6a1921;
331
332 --object-tools-fg: #FAFAFA;
333 --object-tools-bg: #8B3138;
334 --object-tools-hover-bg: #6a1921;
335
336 --breadcrumbs-bg: #DC394C;
337 --breadcrumbs-fg: #FAFAFA;
338 --breadcrumbs-link-fg: #FAFAFA;
339 --link-selected-fg: #DC394C;
340
341 /* Header stays dark in both themes for brand consistency */
342 --header-color: #FAFAFA;
343 --header-branding-color: #FAFAFA;
344 --header-bg: #1f2120;
345 --header-link-color: #FAFAFA;
346
347 /* Dark theme defaults (applied when no explicit data-theme is set) */
348 --body-fg: #FAFAFA;
349 --body-bg: #2B2D2C;
350 --body-quiet-color: #a8aaa9;
351 --body-loud-color: #ffffff;
352
353 --link-fg: #e8677a;
354 --link-hover-color: #f0929f;
355
356 --hairline-color: #3d3f3e;
357 --border-color: #3d3f3e;
358
359 --error-fg: #ff7a7a;
360 --message-success-bg: #173317;
361 --message-success-color: #7ddf7d;
362 --message-success-border: #2d5a2d;
363 --message-warning-bg: #332e17;
364 --message-warning-color: #e6c87a;
365 --message-warning-border: #5a4d2d;
366 --message-error-bg: #331717;
367 --message-error-color: #ff7a7a;
368 --message-error-border: #5a2d2d;
369
370 --darkened-bg: #222423;
371 --selected-bg: #3d1e24;
372 --selected-row: #3d1e24;
373 --input-bg: #1f2120;
374 }
375
376 /* ── Explicit dark theme (beats Django's html[data-theme="dark"] specificity) */
377
378 :root[data-theme="dark"] {
379 --primary: #DC394C;
380 --secondary: #8B3138;
381 --accent: #DC394C;
382 --primary-fg: #FAFAFA;
383
384 --button-fg: #FAFAFA;
385 --button-bg: #DC394C;
386 --button-hover-bg: #c42d3f;
387 --default-button-bg: #8B3138;
388 --default-button-hover-bg:#6a1921;
389 --close-button-bg: #4a4c4b;
390 --close-button-hover-bg: #5a5c5b;
391 --delete-button-bg: #8B3138;
392 --delete-button-hover-bg: #6a1921;
393
394 --object-tools-fg: #FAFAFA;
395 --object-tools-bg: #8B3138;
396 --object-tools-hover-bg: #6a1921;
397
398 --breadcrumbs-bg: #DC394C;
399 --breadcrumbs-fg: #FAFAFA;
400 --breadcrumbs-link-fg: #FAFAFA;
401 --link-selected-fg: #DC394C;
402
403 --header-bg: #1f2120;
404 --header-color: #FAFAFA;
405 --header-branding-color: #FAFAFA;
406 --header-link-color: #FAFAFA;
407
408 --body-fg: #FAFAFA;
409 --body-bg: #0d0d0d;
410 --body-quiet-color: #a8aaa9;
411 --body-loud-color: #ffffff;
412
413 --link-fg: #e8677a;
414 --link-hover-color: #f0929f;
415
416 --hairline-color: #2a2c2b;
417 --border-color: #2a2c2b;
418
419 --error-fg: #ff7a7a;
420 --message-success-bg: #0f1f0f;
421 --message-success-color: #7ddf7d;
422 --message-success-border: #1e3d1e;
423 --message-warning-bg: #1f1c0f;
424 --message-warning-color: #e6c87a;
425 --message-warning-border: #3d3519;
426 --message-error-bg: #1f0f0f;
427 --message-error-color: #ff7a7a;
428 --message-error-border: #3d1a1a;
429
430 --darkened-bg: #141514;
431 --selected-bg: #2d1219;
432 --selected-row: #2d1219;
433 --input-bg: #111211;
434 }
435
436 /* ── Light theme ─────────────────────────────────────────────────────────── */
437
438 :root[data-theme="light"] {
439 /* Header — dark gray in light mode */
440 --header-bg: #3a3a3a;
441 --header-color: #FAFAFA;
442 --header-branding-color: #FAFAFA;
443 --header-link-color: #FAFAFA;
444
445 /* Brand accents — must repeat here to beat base.css html[data-theme="light"] specificity */
446 --primary: #DC394C;
447 --secondary: #8B3138;
448 --accent: #DC394C;
449 --primary-fg: #FAFAFA;
450
451 --button-fg: #FAFAFA;
452 --button-bg: #DC394C;
453 --button-hover-bg: #c42d3f;
454 --default-button-bg: #8B3138;
455 --default-button-hover-bg:#6a1921;
456 --delete-button-bg: #8B3138;
457 --delete-button-hover-bg: #6a1921;
458
459 --object-tools-fg: #FAFAFA;
460 --object-tools-bg: #8B3138;
461 --object-tools-hover-bg: #6a1921;
462
463 --breadcrumbs-bg: #DC394C;
464 --breadcrumbs-fg: #FAFAFA;
465 --breadcrumbs-link-fg: #FAFAFA;
466 --link-selected-fg: #DC394C;
467
468 --body-fg: #1a1a1a;
469 --body-bg: #f8f8f8;
470 --body-quiet-color: #666666;
471 --body-loud-color: #000000;
472
473 --link-fg: #DC394C;
474 --link-hover-color: #8B3138;
475
476 --hairline-color: #e0e0e0;
477 --border-color: #e0e0e0;
478
479 --error-fg: #c0392b;
480 --message-success-bg: #d4edda;
481 --message-success-color: #155724;
482 --message-success-border: #c3e6cb;
483 --message-warning-bg: #fff3cd;
484 --message-warning-color: #856404;
485 --message-warning-border: #ffeeba;
486 --message-error-bg: #f8d7da;
487 --message-error-color: #721c24;
488 --message-error-border: #f5c6cb;
489
490 --darkened-bg: #eeeeee;
491 --selected-bg: #fde8ea;
492 --selected-row: #fde8ea;
493 --input-bg: #ffffff;
494 }
495
496 /* ── System auto: respect prefers-color-scheme light ─────────────────────── */
497
498 @media (prefers-color-scheme: light) {
499 :root:not([data-theme="dark"]) {
500 /* Header — dark gray in light mode */
501 --header-bg: #3a3a3a;
502 --header-color: #FAFAFA;
503 --header-branding-color: #FAFAFA;
504 --header-link-color: #FAFAFA;
505
506 /* Brand accents */
507 --primary: #DC394C;
508 --secondary: #8B3138;
509 --accent: #DC394C;
510 --primary-fg: #FAFAFA;
511
512 --button-fg: #FAFAFA;
513 --button-bg: #DC394C;
514 --button-hover-bg: #c42d3f;
515 --default-button-bg: #8B3138;
516 --default-button-hover-bg:#6a1921;
517 --delete-button-bg: #8B3138;
518 --delete-button-hover-bg: #6a1921;
519
520 --object-tools-fg: #FAFAFA;
521 --object-tools-bg: #8B3138;
522 --object-tools-hover-bg: #6a1921;
523
524 --breadcrumbs-bg: #DC394C;
525 --breadcrumbs-fg: #FAFAFA;
526 --breadcrumbs-link-fg: #FAFAFA;
527 --link-selected-fg: #DC394C;
528
529 --body-fg: #1a1a1a;
530 --body-bg: #f8f8f8;
531 --body-quiet-color: #666666;
532 --body-loud-color: #000000;
533
534 --link-fg: #DC394C;
535 --link-hover-color: #8B3138;
536
537 --hairline-color: #e0e0e0;
538 --border-color: #e0e0e0;
539
540 --error-fg: #c0392b;
541 --message-success-bg: #d4edda;
542 --message-success-color: #155724;
543 --message-success-border: #c3e6cb;
544 --message-warning-bg: #fff3cd;
545 --message-warning-color: #856404;
546 --message-warning-border: #ffeeba;
547 --message-error-bg: #f8d7da;
548 --message-error-color: #721c24;
549 --message-error-border: #f5c6cb;
550
551 --darkened-bg: #eeeeee;
552 --selected-bg: #fde8ea;
553 --selected-row: #fde8ea;
554 --input-bg: #ffffff;
555 }
556 }
557
558
559 /* ── Layout ─────────────────────────────────────────────────────────────── */
560
561 /*
562 * Django's stock base.css gives `.module { background: var(--darkened-bg) }`.
563 * #changelist has class "module", so the 30px margin gap between the table and
564 * the filter also gets --darkened-bg — the same color as the filter itself.
565 * Result: no visible gap.
566 *
567 * clientcove fixes this by shipping a modified base.css with
568 * `.module { background: var(--body-bg) }`. We can't change Django's base.css,
569 * so we target #changelist specifically to make the gap show the body background.
570 */
571 #changelist.module {
572 background: transparent;
573 border: none;
574 }
575
576 /* Prevent table from overflowing into the filter gap — layout owned by changelists.css */
577 #changelist .changelist-form-container > div {
578 overflow-x: auto;
579 }
580
581 /* ── Header — always dark, hardcoded values intentional ─────────────────── */
582
583 #header {
584 background: var(--header-bg);
585 border-bottom: 2px solid var(--primary);
586 }
587
588 #header a:link,
589 #header a:visited {
590 color: var(--header-link-color);
591 }
592
593 #header #branding h1 {
594 line-height: 1;
595 margin: 0;
596 }
597
598 #header #branding .logo img {
599 height: 40px;
600 width: auto;
601 display: block;
602 }
603
604 #header #user-tools {
605 color: #a8aaa9;
606 }
607
608 #header #user-tools a {
609 color: #a8aaa9;
610 }
611
612 #header #user-tools a:hover {
613 color: #FAFAFA;
614 }
615
616 /* Quick-links group in header */
617 .bw-links {
618 display: inline-flex;
619 gap: 2px;
620 margin-right: 8px;
621 border-right: 1px solid #3d3f3e;
622 padding-right: 10px;
623 }
624
625 .bw-links a {
626 display: inline-block;
627 pad
--- a/static/admin/img/logo-dark.svg
+++ b/static/admin/img/logo-dark.svg
@@ -0,0 +1,110 @@
1
+<svg width="1033" height="213" viewBox="0 0 1033 213" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<g filter="url(#filter0_d_303_614)">
3
+<rect x="4" width="1025" height="205" fill="#2B2D2C"/>
4
+<path d="M491.56 167H500.064V163.116H495.59V132.884H500.138V129H491.56V167Z" fill="#DB394C"/>
5
+<path d="M506.018 129V132.884H510.529V163.116H505.981V167H514.56V129H506.018Z" fill="#DB394C"/>
6
+<path d="M505.093 139.932H501.026V155.788H505.093V139.932Z" fill="#DB394C"/>
7
+<path d="M148.624 101.062C168.554 83.5327 166.323 44.969 144.11 30.1725C134.17 22.9805 119.767 19.0365 107.519 19.1396C101.434 19.6293 95.7028 22.4134 90.7074 26.048C84.6217 30.5592 79.7024 35.9209 72.9573 40.2774C65.1219 44.969 54.1422 50.4597 44.0754 49.2223C37.3303 47.3921 37.1782 40.8703 37.4825 34.9929C37.4825 34.9414 37.4825 34.8641 37.4825 34.8125C37.2796 34.8125 37.1275 34.8641 36.9246 34.8641C34.4903 34.8641 32.4871 34.1681 30.8896 33.0854C30.8135 33.7041 30.7374 34.3485 30.7374 34.9929C29.6724 46.129 34.2367 55.873 46.2814 56.3628C61.9776 56.5175 77.0145 46.7734 88.0956 36.9005C97.5793 27.9556 106.125 23.3156 119.006 26.7956C132.522 29.1929 144.871 36.1014 151.311 48.6294C160.034 65.849 155.216 92.9158 136.477 101.01C155.216 109.079 160.034 136.171 151.311 153.391C144.871 165.919 132.395 172.518 118.879 174.915C105.947 178.395 97.63 174.09 88.0956 165.12C77.0145 155.247 61.9523 145.503 46.2814 145.657C34.2367 146.199 29.6724 155.865 30.7374 167.053C30.7374 173.523 34.5917 179.117 40.0943 181.54C40.0182 180.354 40.0943 179.194 40.3986 178.112C40.7536 176.849 41.2861 175.766 41.9454 174.812C39.2575 173.265 37.4317 170.378 37.4317 167.053C37.1275 161.201 37.2542 154.654 44.0246 152.824C54.0661 151.586 65.0459 157.077 72.9066 161.768C79.6517 166.125 84.571 171.513 90.6567 175.998C95.6521 179.633 101.383 182.417 107.469 182.906C119.716 183.009 134.119 179.065 144.059 171.873C166.272 157.103 168.529 118.539 148.573 100.984L148.624 101.062Z" fill="#DC394C"/>
8
+<path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="#DC394C"/>
9
+<g opacity="0.2">
10
+<path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="url(#paint0_linear_303_614)"/>
11
+</g>
12
+<path d="M37.7352 41.4374C37.4056 39.3236 37.4563 37.0294 37.5577 34.8382C37.5577 29.8889 41.5135 25.8676 46.3821 25.8676H79.5241C80.158 25.8676 80.7412 25.6356 81.223 25.2231C83.6827 23.1093 86.8523 20.7893 89.8445 19.0364H46.4074C37.8367 19.0364 30.8634 26.1253 30.8634 34.8382C30.483 38.7823 30.8381 42.5716 31.9538 45.8196C33.3991 43.7574 35.4277 42.1076 37.7606 41.4632L37.7352 41.4374Z" fill="url(#paint1_linear_303_614)"/>
13
+<path d="M37.7352 160.402C37.4056 162.516 37.4563 164.81 37.5577 167.001C37.5577 171.951 41.5135 175.972 46.3821 175.972H79.5241C80.158 175.972 80.7412 176.204 81.223 176.617C83.6827 178.73 86.8523 181.05 89.8445 182.803H46.4074C37.8367 182.803 30.8634 175.714 30.8634 167.001C30.483 163.057 30.8381 159.268 31.9538 156.02C33.3991 158.082 35.4277 159.732 37.7606 160.376L37.7352 160.402Z" fill="url(#paint2_linear_303_614)"/>
14
+<path d="M102.042 111.167C102.042 111.167 35.8849 111.167 30.5852 111.167V117.998H102.042C107.494 117.998 111.779 119.183 114.848 121.555C119.615 124.88 120.604 139.11 114.366 142.719C106.48 148.209 82.0859 144.781 72.8051 145.683C76.5833 147.436 80.3616 149.292 83.6327 151.483C84.5962 152.128 85.712 152.488 86.853 152.463C103.437 152.179 125.929 157.129 125.396 132.665C125.599 117.276 115.938 111.321 102.042 111.167Z" fill="url(#paint3_linear_303_614)"/>
15
+<path d="M87.7147 49.2997C86.3454 49.2997 84.9761 49.6606 83.8097 50.4339C80.6146 52.5735 76.9125 54.3779 73.2103 56.0793C82.77 56.9815 106.226 53.5273 114.34 59.0437C120.578 62.6526 119.563 76.9078 114.822 80.2074C111.753 82.5531 107.468 83.7647 102.016 83.7647H30.5593V90.5958H102.016C115.912 90.4669 125.573 84.4865 125.37 69.0971C125.928 44.8401 104.324 49.5059 87.6893 49.2997H87.7147Z" fill="url(#paint4_linear_303_614)"/>
16
+<path d="M121.263 126.427C120.198 126.608 119.133 126.814 118.068 127.072C119.539 132.614 118.702 140.218 114.391 142.744C106.505 148.235 82.1114 144.807 72.8306 145.709C76.6088 147.462 80.3871 149.318 83.6582 151.509C84.6217 152.153 85.7375 152.514 86.8785 152.489C103.462 152.205 125.954 157.154 125.422 132.691C125.447 130.062 125.168 127.768 124.661 125.654C123.596 126.04 122.48 126.298 121.288 126.427H121.263Z" fill="url(#paint5_linear_303_614)"/>
17
+<g filter="url(#filter1_d_303_614)">
18
+<rect x="195.06" y="32.5" width="803.841" height="76.1916" stroke="#2B2D2C" shape-rendering="crispEdges"/>
19
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.696 108.274V32.9176H582.786C588.268 32.9176 593.264 33.9167 597.661 36.0512C602.078 38.1951 605.594 41.3351 608.098 45.4472C610.637 49.6163 611.813 54.4753 611.813 59.8467C611.813 65.2685 610.6 70.1409 607.941 74.2414C606.304 76.7879 604.248 78.9243 601.812 80.6522L616.857 108.274H587.652L576.157 86.6632V108.274H549.696ZM590.127 104.154H609.921L596.291 79.1301C597.57 78.5163 598.748 77.8075 599.824 77.0036C601.677 75.6192 603.23 73.9531 604.481 72.005C606.622 68.706 607.693 64.6532 607.693 59.8467C607.693 55.0838 606.655 50.9983 604.579 47.59C602.504 44.1817 599.598 41.5709 595.862 39.7576C592.126 37.9442 587.768 37.0375 582.786 37.0375H553.816V104.154H572.037V82.0004H578.343L590.127 104.154ZM572.037 67.843H578.46C580.601 67.843 582.426 67.5808 583.933 67.0565C585.463 66.5103 586.631 65.6473 587.44 64.4675C588.27 63.2877 588.685 61.7474 588.685 59.8467C588.685 57.9241 588.27 56.3619 587.44 55.1603C586.631 53.9368 585.463 53.0411 583.933 52.473C582.426 51.8831 580.601 51.5882 578.46 51.5882H572.037V67.843ZM576.157 55.7081V63.7231H578.46C580.277 63.7231 581.612 63.499 582.563 63.171C583.38 62.8765 583.79 62.5051 584.041 62.1388L584.056 62.1176L584.071 62.0965C584.278 61.8011 584.565 61.1655 584.565 59.8467C584.565 58.4992 584.275 57.8278 584.05 57.5022L584.026 57.467L584.002 57.4314C583.73 57.019 583.305 56.6347 582.499 56.3351L582.465 56.3227L582.432 56.3096C581.53 55.9566 580.244 55.7081 578.46 55.7081H576.157Z" fill="#FAFAFA"/>
20
+<path fill-rule="evenodd" clip-rule="evenodd" d="M475.677 108.274V32.9176H532.288V55.8391H502.138V59.1351H529.797V82.0566H502.138V85.3525H532.157V108.274H475.677ZM498.018 89.4724V77.9367H525.677V63.2549H498.018V51.7193H528.168V37.0375H479.797V104.154H528.037V89.4724H498.018Z" fill="#FAFAFA"/>
21
+<path fill-rule="evenodd" clip-rule="evenodd" d="M406.344 108.274V32.9176H432.805V85.3525H459.94V108.274H406.344ZM428.685 89.4724V37.0375H410.464V104.154H455.82V89.4724H428.685Z" fill="#FAFAFA"/>
22
+<path fill-rule="evenodd" clip-rule="evenodd" d="M389.337 32.9176V108.274H362.876V32.9176H389.337ZM385.217 37.0375H366.996V104.154H385.217V37.0375Z" fill="#FAFAFA"/>
23
+<path fill-rule="evenodd" clip-rule="evenodd" d="M341.181 91.5344L341.177 91.5409C337.907 97.2938 333.433 101.742 327.775 104.771L327.768 104.775L327.762 104.778C322.179 107.742 316.001 109.192 309.313 109.192C302.581 109.192 296.376 107.731 290.786 104.738L290.776 104.733L290.767 104.728C285.138 101.676 280.682 97.2211 277.416 91.4754L277.411 91.4663L277.406 91.4573C274.097 85.5658 272.552 78.5492 272.552 70.5958C272.552 62.6044 274.095 55.5718 277.41 49.6951L277.413 49.69C280.679 43.9181 285.143 39.4637 290.789 36.4517C296.378 33.46 302.582 32 309.313 32C316.004 32 322.185 33.4617 327.769 36.4501C333.435 39.4604 337.911 43.9139 341.18 49.6886C344.519 55.5668 346.074 62.602 346.074 70.5958C346.074 78.5928 344.518 85.6372 341.181 91.5344ZM325.83 40.0853C320.892 37.4417 315.387 36.1199 309.313 36.1199C303.196 36.1199 297.668 37.4417 292.731 40.0853C287.815 42.707 283.904 46.585 280.998 51.7193C278.114 56.8317 276.672 63.1239 276.672 70.5958C276.672 78.0241 278.114 84.3054 280.998 89.4396C283.904 94.552 287.815 98.4409 292.731 101.106C297.668 103.75 303.196 105.072 309.313 105.072C315.387 105.072 320.892 103.761 325.83 101.139C330.768 98.4956 334.689 94.6176 337.595 89.5052C340.501 84.3709 341.954 78.0678 341.954 70.5958C341.954 63.1239 340.501 56.8317 337.595 51.7193C334.689 46.585 330.768 42.707 325.83 40.0853ZM317.756 61.9115L317.749 61.8931C316.943 59.7622 315.851 58.4077 314.59 57.5445L314.569 57.5305L314.549 57.5161C313.319 56.6524 311.655 56.1013 309.313 56.1013C306.973 56.1013 305.289 56.6519 304.028 57.5279L304.02 57.533C302.77 58.3979 301.668 59.7627 300.838 61.9119C300.021 64.0587 299.538 66.9113 299.538 70.5958C299.538 74.2732 300.019 77.1389 300.838 79.3129C301.662 81.4232 302.759 82.7991 304.024 83.6938C305.285 84.5503 306.973 85.0903 309.313 85.0903C311.665 85.0903 313.337 84.545 314.569 83.6941C315.854 82.8 316.95 81.4286 317.753 79.3212L317.756 79.313L317.759 79.3048C318.595 77.1376 319.088 74.2759 319.088 70.5958C319.088 66.9141 318.594 64.0686 317.763 61.9298L317.756 61.9115ZM316.916 87.08C314.862 88.5002 312.328 89.2102 309.313 89.2102C306.298 89.2102 303.753 88.5002 301.677 87.08C299.624 85.6381 298.061 83.5407 296.991 80.7879C295.942 78.0132 295.418 74.6158 295.418 70.5958C295.418 66.5758 295.942 63.1894 296.991 60.4366C298.061 57.6619 299.624 55.5645 301.677 54.1444C303.753 52.7024 306.298 51.9814 309.313 51.9814C312.328 51.9814 314.862 52.7024 316.916 54.1444C318.992 55.5645 320.554 57.6619 321.602 60.4366C322.673 63.1894 323.208 66.5758 323.208 70.5958C323.208 74.6158 322.673 78.0132 321.602 80.7879C320.554 83.5407 318.992 85.6381 316.916 87.08Z" fill="#FAFAFA"/>
24
+<path fill-rule="evenodd" clip-rule="evenodd" d="M194.56 108.274V32.9176H227.781C233.232 32.9176 238.075 33.6472 242.173 35.2702C246.242 36.8731 249.59 39.253 251.916 42.5486C254.255 45.8309 255.365 49.6469 255.365 53.8167C255.365 56.9112 254.675 59.8338 253.244 62.5031C252.104 64.6623 250.581 66.5265 248.711 68.0855C251.447 69.7483 253.658 71.9966 255.309 74.7798C257.146 77.8531 257.987 81.3596 257.987 85.1465C257.987 89.576 256.814 93.6497 254.409 97.2336C252.037 100.802 248.706 103.516 244.595 105.431C240.412 107.379 235.595 108.274 230.271 108.274H194.56ZM244.445 66.1032C244.662 65.967 244.875 65.8259 245.084 65.68C247.029 64.3036 248.536 62.5995 249.607 60.5676C250.699 58.5358 251.245 56.2855 251.245 53.8167C251.245 50.4084 250.35 47.448 248.558 44.9355C246.788 42.423 244.156 40.4785 240.66 39.1021C237.186 37.7257 232.893 37.0375 227.781 37.0375H198.679V104.154H230.271C235.143 104.154 239.338 103.335 242.856 101.696C246.373 100.058 249.082 97.8073 250.983 94.9453C252.906 92.0832 253.867 88.8169 253.867 85.1465C253.867 81.9786 253.168 79.2257 251.77 76.888C250.371 74.5284 248.493 72.6823 246.133 71.3496C245.682 71.0907 245.222 70.8546 244.752 70.6413C242.8 69.7553 240.683 69.2613 238.401 69.1594C238.357 69.1574 238.312 69.1556 238.268 69.1539V68.4984C238.276 68.4967 238.284 68.4951 238.293 68.4934C240.602 68.0248 242.653 67.2281 244.445 66.1032ZM228.809 80.0833L228.797 80.0783C228.133 79.7907 227.138 79.5659 225.683 79.5659H221.02V85.4836H225.421C228.065 85.4836 229.319 84.9764 229.832 84.6027C230.143 84.3635 230.477 83.9794 230.477 82.7869C230.477 81.8557 230.271 81.3515 230.077 81.0582C229.849 80.7117 229.491 80.3743 228.82 80.0881L228.809 80.0833ZM232.303 87.8993C230.774 89.0354 228.48 89.6035 225.421 89.6035H216.901V75.446H225.683C227.54 75.446 229.124 75.7301 230.435 76.2981C231.768 76.8662 232.795 77.6964 233.516 78.7888C234.237 79.8812 234.597 81.2139 234.597 82.7869C234.597 85.0373 233.833 86.7414 232.303 87.8993ZM228.114 58.8396L228.119 58.8327C228.228 58.6826 228.38 58.3975 228.38 57.7493C228.38 56.7949 228.117 56.5269 227.831 56.3166L227.815 56.305L227.799 56.2933C227.174 55.8244 226.219 55.4459 224.635 55.4459H221.02V60.0527H224.372C225.538 60.0527 226.399 59.869 227.035 59.6078C227.65 59.3551 227.943 59.0772 228.109 58.8465L228.114 58.8396ZM224.372 64.1726C225.967 64.1726 227.377 63.9213 228.6 63.4188C229.824 62.9163 230.774 62.1953 231.451 61.2559C232.15 60.2946 232.5 59.1257 232.5 57.7493C232.5 55.6737 231.757 54.0898 230.271 52.9974C228.786 51.8831 226.907 51.326 224.635 51.326H216.901V64.1726H224.372Z" fill="#FAFAFA"/>
25
+<path d="M979.577 55.6468C979.388 53.2869 978.503 51.4461 976.922 50.1245C975.365 48.8029 972.993 48.1421 969.807 48.1421C967.777 48.1421 966.114 48.3899 964.816 48.8855C963.541 49.3575 962.597 50.0065 961.984 50.8325C961.37 51.6585 961.051 52.6025 961.028 53.6645C960.981 54.5377 961.134 55.3282 961.488 56.0362C961.866 56.7206 962.456 57.346 963.258 57.9124C964.06 58.4552 965.087 58.9508 966.338 59.3992C967.589 59.8476 969.075 60.2488 970.798 60.6028L976.745 61.8771C980.757 62.7267 984.191 63.8477 987.046 65.2401C989.902 66.6325 992.238 68.2726 994.056 70.1606C995.873 72.025 997.206 74.1253 998.056 76.4617C998.929 78.7981 999.377 81.3468 999.401 84.108C999.377 88.8751 998.185 92.9107 995.826 96.2146C993.466 99.5185 990.091 102.032 985.701 103.755C981.335 105.477 976.084 106.339 969.948 106.339C963.647 106.339 958.149 105.407 953.452 103.542C948.78 101.678 945.145 98.8106 942.549 94.9402C939.977 91.0463 938.679 86.0668 938.655 80.0017H957.346C957.464 82.22 958.019 84.0844 959.01 85.5948C960.001 87.1051 961.394 88.2497 963.187 89.0285C965.004 89.8073 967.164 90.1967 969.665 90.1967C971.766 90.1967 973.524 89.9371 974.94 89.4179C976.356 88.8987 977.43 88.1789 978.161 87.2585C978.893 86.3382 979.27 85.288 979.294 84.108C979.27 82.9988 978.905 82.0312 978.197 81.2052C977.512 80.3557 976.379 79.6005 974.798 78.9397C973.217 78.2553 971.081 77.6181 968.391 77.0281L961.169 75.4705C954.75 74.0781 949.688 71.7536 945.983 68.4968C942.302 65.2165 940.473 60.7444 940.496 55.0804C940.473 50.4785 941.7 46.4548 944.178 43.0092C946.679 39.5401 950.137 36.8379 954.55 34.9028C958.986 32.9676 964.072 32 969.807 32C975.66 32 980.722 32.9794 984.993 34.9382C989.265 36.8969 992.557 39.6581 994.87 43.2216C997.206 46.7616 998.386 50.9033 998.41 55.6468H979.577Z" fill="#FAFAFA"/>
26
+<path d="M870.211 105.489V32.9912H889.893V62.3019H890.884L912.69 32.9912H935.629L911.132 65.2755L936.195 105.489H912.69L896.406 78.3025L889.893 86.7983V105.489H870.211Z" fill="#FAFAFA"/>
27
+<path d="M803.624 105.489V32.9912H834.917C840.298 32.9912 845.006 33.9706 849.042 35.9293C853.077 37.8881 856.216 40.7083 858.458 44.3898C860.7 48.0714 861.821 52.4845 861.821 57.6292C861.821 62.8211 860.664 67.1988 858.352 70.7624C856.063 74.3259 852.841 77.0163 848.688 78.8335C844.558 80.6506 839.732 81.5592 834.209 81.5592H815.518V66.2667H830.245C832.557 66.2667 834.528 65.9835 836.156 65.4171C837.808 64.8271 839.071 63.8949 839.944 62.6205C840.841 61.3461 841.289 59.6824 841.289 57.6292C841.289 55.5524 840.841 53.8651 839.944 52.5671C839.071 51.2455 837.808 50.2779 836.156 49.6643C834.528 49.0271 832.557 48.7085 830.245 48.7085H823.306V105.489H803.624ZM846.103 72.2138L864.228 105.489H842.847L825.147 72.2138H846.103Z" fill="#FAFAFA"/>
28
+<path d="M794.518 69.2402C794.518 77.3113 792.948 84.1198 789.81 89.6657C786.671 95.188 782.435 99.377 777.101 102.233C771.768 105.064 765.82 106.48 759.26 106.48C752.652 106.48 746.681 105.053 741.348 102.197C736.038 99.318 731.813 95.1172 728.675 89.5949C725.56 84.049 724.002 77.2641 724.002 69.2402C724.002 61.1691 725.56 54.3725 728.675 48.8501C731.813 43.3042 736.038 39.1153 741.348 36.2833C746.681 33.4278 752.652 32 759.26 32C765.82 32 771.768 33.4278 777.101 36.2833C782.435 39.1153 786.671 43.3042 789.81 48.8501C792.948 54.3725 794.518 61.1691 794.518 69.2402ZM774.269 69.2402C774.269 64.8979 773.691 61.2399 772.535 58.2664C771.402 55.2692 769.714 53.0037 767.472 51.4697C765.254 49.9121 762.517 49.1333 759.26 49.1333C756.003 49.1333 753.254 49.9121 751.012 51.4697C748.793 53.0037 747.106 55.2692 745.95 58.2664C744.817 61.2399 744.25 64.8979 744.25 69.2402C744.25 73.5826 744.817 77.2523 745.95 80.2495C747.106 83.223 748.793 85.4886 751.012 87.0461C753.254 88.5801 756.003 89.3471 759.26 89.3471C762.517 89.3471 765.254 88.5801 767.472 87.0461C769.714 85.4886 771.402 83.223 772.535 80.2495C773.691 77.2523 774.269 73.5826 774.269 69.2402Z" fill="#FAFAFA"/>
29
+<path d="M638.238 105.489L616.857 32.9912H638.804L648.716 77.5945H649.283L661.035 32.9912H678.31L690.063 77.7361H690.629L700.541 32.9912H722.489L701.107 105.489H682.275L669.956 64.9923H669.389L657.07 105.489H638.238Z" fill="#FAFAFA"/>
30
+</g>
31
+<path d="M649.525 133.05H642.505V164.911H638.725V133.05H631.704V129.81H649.525V133.05Z" fill="#FAFAFA"/>
32
+<path d="M622.242 155.191H626.022V158.161C626.022 160.501 625.302 162.355 623.862 163.723C622.422 165.055 620.19 165.721 617.165 165.721C614.141 165.721 611.891 165.055 610.415 163.723C608.939 162.355 608.201 160.501 608.201 158.161V136.56C608.201 134.22 608.921 132.384 610.361 131.052C611.801 129.684 614.033 129 617.057 129C620.082 129 622.332 129.684 623.808 131.052C625.284 132.384 626.022 134.22 626.022 136.56V141.69H622.242V136.56C622.242 135.192 621.81 134.13 620.946 133.374C620.117 132.618 618.821 132.24 617.057 132.24C615.329 132.24 614.051 132.618 613.223 133.374C612.395 134.13 611.981 135.192 611.981 136.56V158.161C611.981 159.493 612.395 160.555 613.223 161.347C614.087 162.103 615.401 162.481 617.165 162.481C618.893 162.481 620.171 162.103 621 161.347C621.828 160.555 622.242 159.493 622.242 158.161V155.191Z" fill="#FAFAFA"/>
33
+<path d="M594.456 164.911V129.81H598.236V164.911H594.456Z" fill="#FAFAFA"/>
34
+<path d="M551.898 164.911V129.81H566.749V133.05H555.678V145.471H564.589V148.711H555.678V164.911H551.898ZM574.309 129.81H578.089V161.671H587.971V164.911H574.309V129.81Z" fill="#FAFAFA"/>
35
+<path d="M521.839 164.911V129.81H525.457L537.337 156.811V129.81H541.117V164.911H537.499L525.619 137.91V164.911H521.839Z" fill="#FAFAFA"/>
36
+<path d="M481.068 155.191H484.848V158.161C484.848 160.501 484.128 162.355 482.688 163.723C481.248 165.055 479.015 165.721 475.991 165.721C472.967 165.721 470.717 165.055 469.241 163.723C467.765 162.355 467.027 160.501 467.027 158.161V136.56C467.027 134.22 467.747 132.384 469.187 131.052C470.627 129.684 472.859 129 475.883 129C478.907 129 481.158 129.684 482.634 131.052C484.11 132.384 484.848 134.22 484.848 136.56V141.69H481.068V136.56C481.068 135.192 480.635 134.13 479.771 133.374C478.943 132.618 477.647 132.24 475.883 132.24C474.155 132.24 472.877 132.618 472.049 133.374C471.221 134.13 470.807 135.192 470.807 136.56V158.161C470.807 159.493 471.221 160.555 472.049 161.347C472.913 162.103 474.227 162.481 475.991 162.481C477.719 162.481 478.997 162.103 479.825 161.347C480.653 160.555 481.068 159.493 481.068 158.161V155.191Z" fill="#FAFAFA"/>
37
+<path d="M435.94 164.911V152.329L427.03 129.81H431.026L437.83 147.901L444.635 129.81H448.631L439.72 152.383V164.911H435.94Z" fill="#FAFAFA"/>
38
+<path d="M411.97 129.81C414.85 129.81 416.992 130.494 418.396 131.862C419.836 133.194 420.556 135.03 420.556 137.37V140.61C420.556 143.67 419.278 145.633 416.722 146.497C419.638 147.289 421.096 149.287 421.096 152.491V157.351C421.096 159.691 420.394 161.545 418.99 162.913C417.586 164.245 415.426 164.911 412.51 164.911H403.816V129.81H411.97ZM416.776 137.37C416.776 136.038 416.362 134.994 415.534 134.238C414.742 133.446 413.554 133.05 411.97 133.05H407.596V145.201H411.97C413.59 145.201 414.796 144.768 415.588 143.904C416.38 143.04 416.776 141.942 416.776 140.61V137.37ZM417.316 152.491C417.316 151.159 416.902 150.061 416.074 149.197C415.282 148.333 414.094 147.901 412.51 147.901H407.596V161.671H412.51C414.13 161.671 415.336 161.293 416.128 160.537C416.92 159.745 417.316 158.683 417.316 157.351V152.491Z" fill="#FAFAFA"/>
39
+<path d="M364.106 164.911V129.81H372.8C375.68 129.81 377.822 130.494 379.226 131.862C380.666 133.194 381.386 135.03 381.386 137.37V157.351C381.386 159.691 380.684 161.545 379.28 162.913C377.876 164.245 375.716 164.911 372.8 164.911H364.106ZM377.606 137.37C377.606 136.038 377.192 134.994 376.364 134.238C375.572 133.446 374.384 133.05 372.8 133.05H367.886V161.671H372.8C374.42 161.671 375.626 161.293 376.418 160.537C377.21 159.745 377.606 158.683 377.606 157.351V137.37Z" fill="#FAFAFA"/>
40
+<path d="M350.335 155.191H354.115V158.161C354.115 160.501 353.395 162.355 351.955 163.723C350.515 165.055 348.283 165.721 345.259 165.721C342.235 165.721 339.985 165.055 338.509 163.723C337.033 162.355 336.295 160.501 336.295 158.161V136.56C336.295 134.22 337.015 132.384 338.455 131.052C339.895 129.684 342.127 129 345.151 129C348.175 129 350.425 129.684 351.901 131.052C353.377 132.384 354.115 134.22 354.115 136.56V148.441H340.075V158.161C340.075 159.493 340.489 160.555 341.317 161.347C342.181 162.103 343.495 162.481 345.259 162.481C346.987 162.481 348.265 162.103 349.093 161.347C349.921 160.555 350.335 159.493 350.335 158.161V155.191ZM340.075 145.2H350.335V136.56C350.335 135.192 349.903 134.13 349.039 133.374C348.211 132.618 346.915 132.24 345.151 132.24C343.423 132.24 342.145 132.618 341.317 133.374C340.489 134.13 340.075 135.192 340.075 136.56V145.2Z" fill="#FAFAFA"/>
41
+<path d="M315.483 164.911V129.81H317.103L318.669 133.806C319.173 132.438 319.857 131.304 320.721 130.404C321.585 129.468 322.737 129 324.177 129C326.265 129 327.849 129.594 328.929 130.782C330.045 131.934 330.603 133.68 330.603 136.02V139.53H326.985V135.804C326.985 134.76 326.751 133.914 326.283 133.266C325.815 132.582 325.059 132.24 324.015 132.24C323.331 132.24 322.611 132.51 321.855 133.05C321.135 133.59 320.523 134.454 320.019 135.642C319.515 136.794 319.263 138.324 319.263 140.232V164.911H315.483Z" fill="#FAFAFA"/>
42
+<path d="M301.713 155.191H305.493V158.161C305.493 160.501 304.773 162.355 303.333 163.723C301.893 165.055 299.661 165.721 296.636 165.721C293.612 165.721 291.362 165.055 289.886 163.723C288.41 162.355 287.672 160.501 287.672 158.161V136.56C287.672 134.22 288.392 132.384 289.832 131.052C291.272 129.684 293.504 129 296.528 129C299.553 129 301.803 129.684 303.279 131.052C304.755 132.384 305.493 134.22 305.493 136.56V148.441H291.452V158.161C291.452 159.493 291.866 160.555 292.694 161.347C293.558 162.103 294.872 162.481 296.636 162.481C298.365 162.481 299.643 162.103 300.471 161.347C301.299 160.555 301.713 159.493 301.713 158.161V155.191ZM291.452 145.2H301.713V136.56C301.713 135.192 301.281 134.13 300.417 133.374C299.589 132.618 298.293 132.24 296.528 132.24C294.8 132.24 293.522 132.618 292.694 133.374C291.866 134.13 291.452 135.192 291.452 136.56V145.2Z" fill="#FAFAFA"/>
43
+<path d="M260.684 129.81H264.464V158.161C264.464 159.493 264.878 160.555 265.706 161.347C266.534 162.103 267.776 162.481 269.432 162.481C271.124 162.481 272.384 162.103 273.212 161.347C274.04 160.555 274.454 159.493 274.454 158.161V129.81H278.235V158.161C278.235 160.501 277.479 162.355 275.966 163.723C274.49 165.055 272.312 165.721 269.432 165.721C267.776 165.721 266.39 165.433 265.274 164.857C264.158 164.281 263.258 163.525 262.574 162.589C261.926 163.561 261.026 164.335 259.874 164.911C258.722 165.451 257.336 165.721 255.716 165.721C252.836 165.721 250.64 165.055 249.128 163.723C247.652 162.355 246.914 160.501 246.914 158.161V129.81H250.694V158.161C250.694 159.493 251.108 160.555 251.936 161.347C252.8 162.103 254.06 162.481 255.716 162.481C257.408 162.481 258.65 162.103 259.442 161.347C260.27 160.555 260.684 159.493 260.684 158.161V129.81Z" fill="#FAFAFA"/>
44
+<path d="M237.476 158.161C237.476 160.501 236.72 162.355 235.208 163.723C233.732 165.055 231.446 165.721 228.35 165.721C225.254 165.721 222.932 165.055 221.383 163.723C219.871 162.355 219.115 160.501 219.115 158.161V136.56C219.115 134.22 219.853 132.384 221.329 131.052C222.842 129.684 225.146 129 228.242 129C231.338 129 233.642 129.684 235.154 131.052C236.702 132.384 237.476 134.22 237.476 136.56V158.161ZM233.696 136.56C233.696 135.192 233.246 134.13 232.346 133.374C231.446 132.618 230.078 132.24 228.242 132.24C226.442 132.24 225.092 132.618 224.192 133.374C223.328 134.13 222.896 135.192 222.896 136.56V158.161C222.896 159.493 223.346 160.555 224.246 161.347C225.146 162.103 226.514 162.481 228.35 162.481C230.15 162.481 231.482 162.103 232.346 161.347C233.246 160.555 233.696 159.493 233.696 158.161V136.56Z" fill="#FAFAFA"/>
45
+<path d="M194.56 164.911V129.81H202.714C205.594 129.81 207.736 130.494 209.14 131.862C210.58 133.194 211.3 135.03 211.3 137.37V146.551C211.3 148.891 210.598 150.745 209.194 152.113C207.79 153.445 205.63 154.111 202.714 154.111H198.34V164.911H194.56ZM207.52 137.37C207.52 136.038 207.106 134.994 206.278 134.238C205.486 133.446 204.298 133.05 202.714 133.05H198.34V150.871H202.714C204.334 150.871 205.54 150.493 206.332 149.737C207.124 148.945 207.52 147.883 207.52 146.551V137.37Z" fill="#FAFAFA"/>
46
+</g>
47
+<defs>
48
+<filter id="filter0_d_303_614" x="0" y="0" width="1033" height="213" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
49
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
50
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
51
+<feOffset dy="4"/>
52
+<feGaussianBlur stdDeviation="2"/>
53
+<feComposite in2="hardAlpha" operator="out"/>
54
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
55
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
56
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
57
+</filter>
58
+<filter id="filter1_d_303_614" x="190.56" y="32" width="812.841" height="85.1917" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
59
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
60
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
61
+<feOffset dy="4"/>
62
+<feGaussianBlur stdDeviation="2"/>
63
+<feComposite in2="hardAlpha" operator="out"/>
64
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
65
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
66
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
67
+</filter>
68
+<linearGradient id="paint0_linear_303_614" x1="48.4373" y1="83.8162" x2="111.888" y2="65.7367" gradientUnits="userSpaceOnUse">
69
+<stop stop-color="#DD4652"/>
70
+<stop offset="0.15" stop-color="#DD4854"/>
71
+<stop offset="0.23" stop-color="#DE4F5B"/>
72
+<stop offset="0.29" stop-color="#E15C67"/>
73
+<stop offset="0.34" stop-color="#E46F78"/>
74
+<stop offset="0.38" stop-color="#E9878F"/>
75
+<stop offset="0.42" stop-color="#EEA5AB"/>
76
+<stop offset="0.46" stop-color="#F5C8CC"/>
77
+<stop offset="0.49" stop-color="#FCF0F1"/>
78
+<stop offset="0.5" stop-color="#FEFDFD"/>
79
+<stop offset="0.52" stop-color="#F7D4D8"/>
80
+<stop offset="0.55" stop-color="#F1B0B7"/>
81
+<stop offset="0.58" stop-color="#EB8F9A"/>
82
+<stop offset="0.61" stop-color="#E67481"/>
83
+<stop offset="0.64" stop-color="#E25E6D"/>
84
+<stop offset="0.68" stop-color="#DF4D5E"/>
85
+<stop offset="0.73" stop-color="#DD4153"/>
86
+<stop offset="0.8" stop-color="#DC3A4D"/>
87
+<stop offset="1" stop-color="#DC394C"/>
88
+</linearGradient>
89
+<linearGradient id="paint1_linear_303_614" x1="107.569" y1="12.2053" x2="11.6611" y2="39.2106" gradientUnits="userSpaceOnUse">
90
+<stop stop-color="#8B3138" stop-opacity="0"/>
91
+<stop offset="0.65" stop-color="#DC394C"/>
92
+</linearGradient>
93
+<linearGradient id="paint2_linear_303_614" x1="107.569" y1="189.634" x2="11.6864" y2="162.629" gradientUnits="userSpaceOnUse">
94
+<stop stop-color="#8B3138" stop-opacity="0"/>
95
+<stop offset="0.65" stop-color="#DC394C"/>
96
+</linearGradient>
97
+<linearGradient id="paint3_linear_303_614" x1="30.5598" y1="131.944" x2="125.396" y2="131.944" gradientUnits="userSpaceOnUse">
98
+<stop stop-color="#8B3138" stop-opacity="0"/>
99
+<stop offset="0.65" stop-color="#DC394C"/>
100
+</linearGradient>
101
+<linearGradient id="paint4_linear_303_614" x1="30.5847" y1="69.8189" x2="125.396" y2="69.8189" gradientUnits="userSpaceOnUse">
102
+<stop stop-color="#8B3138" stop-opacity="0"/>
103
+<stop offset="0.65" stop-color="#DC394C"/>
104
+</linearGradient>
105
+<linearGradient id="paint5_linear_303_614" x1="-48.1491" y1="140.528" x2="123.546" y2="139.131" gradientUnits="userSpaceOnUse">
106
+<stop stop-color="#6A1921"/>
107
+<stop offset="1" stop-color="#DC394C"/>
108
+</linearGradient>
109
+</defs>
110
+</svg>
--- a/static/admin/img/logo-dark.svg
+++ b/static/admin/img/logo-dark.svg
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/static/admin/img/logo-dark.svg
+++ b/static/admin/img/logo-dark.svg
@@ -0,0 +1,110 @@
1 <svg width="1033" height="213" viewBox="0 0 1033 213" fill="none" xmlns="http://www.w3.org/2000/svg">
2 <g filter="url(#filter0_d_303_614)">
3 <rect x="4" width="1025" height="205" fill="#2B2D2C"/>
4 <path d="M491.56 167H500.064V163.116H495.59V132.884H500.138V129H491.56V167Z" fill="#DB394C"/>
5 <path d="M506.018 129V132.884H510.529V163.116H505.981V167H514.56V129H506.018Z" fill="#DB394C"/>
6 <path d="M505.093 139.932H501.026V155.788H505.093V139.932Z" fill="#DB394C"/>
7 <path d="M148.624 101.062C168.554 83.5327 166.323 44.969 144.11 30.1725C134.17 22.9805 119.767 19.0365 107.519 19.1396C101.434 19.6293 95.7028 22.4134 90.7074 26.048C84.6217 30.5592 79.7024 35.9209 72.9573 40.2774C65.1219 44.969 54.1422 50.4597 44.0754 49.2223C37.3303 47.3921 37.1782 40.8703 37.4825 34.9929C37.4825 34.9414 37.4825 34.8641 37.4825 34.8125C37.2796 34.8125 37.1275 34.8641 36.9246 34.8641C34.4903 34.8641 32.4871 34.1681 30.8896 33.0854C30.8135 33.7041 30.7374 34.3485 30.7374 34.9929C29.6724 46.129 34.2367 55.873 46.2814 56.3628C61.9776 56.5175 77.0145 46.7734 88.0956 36.9005C97.5793 27.9556 106.125 23.3156 119.006 26.7956C132.522 29.1929 144.871 36.1014 151.311 48.6294C160.034 65.849 155.216 92.9158 136.477 101.01C155.216 109.079 160.034 136.171 151.311 153.391C144.871 165.919 132.395 172.518 118.879 174.915C105.947 178.395 97.63 174.09 88.0956 165.12C77.0145 155.247 61.9523 145.503 46.2814 145.657C34.2367 146.199 29.6724 155.865 30.7374 167.053C30.7374 173.523 34.5917 179.117 40.0943 181.54C40.0182 180.354 40.0943 179.194 40.3986 178.112C40.7536 176.849 41.2861 175.766 41.9454 174.812C39.2575 173.265 37.4317 170.378 37.4317 167.053C37.1275 161.201 37.2542 154.654 44.0246 152.824C54.0661 151.586 65.0459 157.077 72.9066 161.768C79.6517 166.125 84.571 171.513 90.6567 175.998C95.6521 179.633 101.383 182.417 107.469 182.906C119.716 183.009 134.119 179.065 144.059 171.873C166.272 157.103 168.529 118.539 148.573 100.984L148.624 101.062Z" fill="#DC394C"/>
8 <path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="#DC394C"/>
9 <g opacity="0.2">
10 <path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="url(#paint0_linear_303_614)"/>
11 </g>
12 <path d="M37.7352 41.4374C37.4056 39.3236 37.4563 37.0294 37.5577 34.8382C37.5577 29.8889 41.5135 25.8676 46.3821 25.8676H79.5241C80.158 25.8676 80.7412 25.6356 81.223 25.2231C83.6827 23.1093 86.8523 20.7893 89.8445 19.0364H46.4074C37.8367 19.0364 30.8634 26.1253 30.8634 34.8382C30.483 38.7823 30.8381 42.5716 31.9538 45.8196C33.3991 43.7574 35.4277 42.1076 37.7606 41.4632L37.7352 41.4374Z" fill="url(#paint1_linear_303_614)"/>
13 <path d="M37.7352 160.402C37.4056 162.516 37.4563 164.81 37.5577 167.001C37.5577 171.951 41.5135 175.972 46.3821 175.972H79.5241C80.158 175.972 80.7412 176.204 81.223 176.617C83.6827 178.73 86.8523 181.05 89.8445 182.803H46.4074C37.8367 182.803 30.8634 175.714 30.8634 167.001C30.483 163.057 30.8381 159.268 31.9538 156.02C33.3991 158.082 35.4277 159.732 37.7606 160.376L37.7352 160.402Z" fill="url(#paint2_linear_303_614)"/>
14 <path d="M102.042 111.167C102.042 111.167 35.8849 111.167 30.5852 111.167V117.998H102.042C107.494 117.998 111.779 119.183 114.848 121.555C119.615 124.88 120.604 139.11 114.366 142.719C106.48 148.209 82.0859 144.781 72.8051 145.683C76.5833 147.436 80.3616 149.292 83.6327 151.483C84.5962 152.128 85.712 152.488 86.853 152.463C103.437 152.179 125.929 157.129 125.396 132.665C125.599 117.276 115.938 111.321 102.042 111.167Z" fill="url(#paint3_linear_303_614)"/>
15 <path d="M87.7147 49.2997C86.3454 49.2997 84.9761 49.6606 83.8097 50.4339C80.6146 52.5735 76.9125 54.3779 73.2103 56.0793C82.77 56.9815 106.226 53.5273 114.34 59.0437C120.578 62.6526 119.563 76.9078 114.822 80.2074C111.753 82.5531 107.468 83.7647 102.016 83.7647H30.5593V90.5958H102.016C115.912 90.4669 125.573 84.4865 125.37 69.0971C125.928 44.8401 104.324 49.5059 87.6893 49.2997H87.7147Z" fill="url(#paint4_linear_303_614)"/>
16 <path d="M121.263 126.427C120.198 126.608 119.133 126.814 118.068 127.072C119.539 132.614 118.702 140.218 114.391 142.744C106.505 148.235 82.1114 144.807 72.8306 145.709C76.6088 147.462 80.3871 149.318 83.6582 151.509C84.6217 152.153 85.7375 152.514 86.8785 152.489C103.462 152.205 125.954 157.154 125.422 132.691C125.447 130.062 125.168 127.768 124.661 125.654C123.596 126.04 122.48 126.298 121.288 126.427H121.263Z" fill="url(#paint5_linear_303_614)"/>
17 <g filter="url(#filter1_d_303_614)">
18 <rect x="195.06" y="32.5" width="803.841" height="76.1916" stroke="#2B2D2C" shape-rendering="crispEdges"/>
19 <path fill-rule="evenodd" clip-rule="evenodd" d="M549.696 108.274V32.9176H582.786C588.268 32.9176 593.264 33.9167 597.661 36.0512C602.078 38.1951 605.594 41.3351 608.098 45.4472C610.637 49.6163 611.813 54.4753 611.813 59.8467C611.813 65.2685 610.6 70.1409 607.941 74.2414C606.304 76.7879 604.248 78.9243 601.812 80.6522L616.857 108.274H587.652L576.157 86.6632V108.274H549.696ZM590.127 104.154H609.921L596.291 79.1301C597.57 78.5163 598.748 77.8075 599.824 77.0036C601.677 75.6192 603.23 73.9531 604.481 72.005C606.622 68.706 607.693 64.6532 607.693 59.8467C607.693 55.0838 606.655 50.9983 604.579 47.59C602.504 44.1817 599.598 41.5709 595.862 39.7576C592.126 37.9442 587.768 37.0375 582.786 37.0375H553.816V104.154H572.037V82.0004H578.343L590.127 104.154ZM572.037 67.843H578.46C580.601 67.843 582.426 67.5808 583.933 67.0565C585.463 66.5103 586.631 65.6473 587.44 64.4675C588.27 63.2877 588.685 61.7474 588.685 59.8467C588.685 57.9241 588.27 56.3619 587.44 55.1603C586.631 53.9368 585.463 53.0411 583.933 52.473C582.426 51.8831 580.601 51.5882 578.46 51.5882H572.037V67.843ZM576.157 55.7081V63.7231H578.46C580.277 63.7231 581.612 63.499 582.563 63.171C583.38 62.8765 583.79 62.5051 584.041 62.1388L584.056 62.1176L584.071 62.0965C584.278 61.8011 584.565 61.1655 584.565 59.8467C584.565 58.4992 584.275 57.8278 584.05 57.5022L584.026 57.467L584.002 57.4314C583.73 57.019 583.305 56.6347 582.499 56.3351L582.465 56.3227L582.432 56.3096C581.53 55.9566 580.244 55.7081 578.46 55.7081H576.157Z" fill="#FAFAFA"/>
20 <path fill-rule="evenodd" clip-rule="evenodd" d="M475.677 108.274V32.9176H532.288V55.8391H502.138V59.1351H529.797V82.0566H502.138V85.3525H532.157V108.274H475.677ZM498.018 89.4724V77.9367H525.677V63.2549H498.018V51.7193H528.168V37.0375H479.797V104.154H528.037V89.4724H498.018Z" fill="#FAFAFA"/>
21 <path fill-rule="evenodd" clip-rule="evenodd" d="M406.344 108.274V32.9176H432.805V85.3525H459.94V108.274H406.344ZM428.685 89.4724V37.0375H410.464V104.154H455.82V89.4724H428.685Z" fill="#FAFAFA"/>
22 <path fill-rule="evenodd" clip-rule="evenodd" d="M389.337 32.9176V108.274H362.876V32.9176H389.337ZM385.217 37.0375H366.996V104.154H385.217V37.0375Z" fill="#FAFAFA"/>
23 <path fill-rule="evenodd" clip-rule="evenodd" d="M341.181 91.5344L341.177 91.5409C337.907 97.2938 333.433 101.742 327.775 104.771L327.768 104.775L327.762 104.778C322.179 107.742 316.001 109.192 309.313 109.192C302.581 109.192 296.376 107.731 290.786 104.738L290.776 104.733L290.767 104.728C285.138 101.676 280.682 97.2211 277.416 91.4754L277.411 91.4663L277.406 91.4573C274.097 85.5658 272.552 78.5492 272.552 70.5958C272.552 62.6044 274.095 55.5718 277.41 49.6951L277.413 49.69C280.679 43.9181 285.143 39.4637 290.789 36.4517C296.378 33.46 302.582 32 309.313 32C316.004 32 322.185 33.4617 327.769 36.4501C333.435 39.4604 337.911 43.9139 341.18 49.6886C344.519 55.5668 346.074 62.602 346.074 70.5958C346.074 78.5928 344.518 85.6372 341.181 91.5344ZM325.83 40.0853C320.892 37.4417 315.387 36.1199 309.313 36.1199C303.196 36.1199 297.668 37.4417 292.731 40.0853C287.815 42.707 283.904 46.585 280.998 51.7193C278.114 56.8317 276.672 63.1239 276.672 70.5958C276.672 78.0241 278.114 84.3054 280.998 89.4396C283.904 94.552 287.815 98.4409 292.731 101.106C297.668 103.75 303.196 105.072 309.313 105.072C315.387 105.072 320.892 103.761 325.83 101.139C330.768 98.4956 334.689 94.6176 337.595 89.5052C340.501 84.3709 341.954 78.0678 341.954 70.5958C341.954 63.1239 340.501 56.8317 337.595 51.7193C334.689 46.585 330.768 42.707 325.83 40.0853ZM317.756 61.9115L317.749 61.8931C316.943 59.7622 315.851 58.4077 314.59 57.5445L314.569 57.5305L314.549 57.5161C313.319 56.6524 311.655 56.1013 309.313 56.1013C306.973 56.1013 305.289 56.6519 304.028 57.5279L304.02 57.533C302.77 58.3979 301.668 59.7627 300.838 61.9119C300.021 64.0587 299.538 66.9113 299.538 70.5958C299.538 74.2732 300.019 77.1389 300.838 79.3129C301.662 81.4232 302.759 82.7991 304.024 83.6938C305.285 84.5503 306.973 85.0903 309.313 85.0903C311.665 85.0903 313.337 84.545 314.569 83.6941C315.854 82.8 316.95 81.4286 317.753 79.3212L317.756 79.313L317.759 79.3048C318.595 77.1376 319.088 74.2759 319.088 70.5958C319.088 66.9141 318.594 64.0686 317.763 61.9298L317.756 61.9115ZM316.916 87.08C314.862 88.5002 312.328 89.2102 309.313 89.2102C306.298 89.2102 303.753 88.5002 301.677 87.08C299.624 85.6381 298.061 83.5407 296.991 80.7879C295.942 78.0132 295.418 74.6158 295.418 70.5958C295.418 66.5758 295.942 63.1894 296.991 60.4366C298.061 57.6619 299.624 55.5645 301.677 54.1444C303.753 52.7024 306.298 51.9814 309.313 51.9814C312.328 51.9814 314.862 52.7024 316.916 54.1444C318.992 55.5645 320.554 57.6619 321.602 60.4366C322.673 63.1894 323.208 66.5758 323.208 70.5958C323.208 74.6158 322.673 78.0132 321.602 80.7879C320.554 83.5407 318.992 85.6381 316.916 87.08Z" fill="#FAFAFA"/>
24 <path fill-rule="evenodd" clip-rule="evenodd" d="M194.56 108.274V32.9176H227.781C233.232 32.9176 238.075 33.6472 242.173 35.2702C246.242 36.8731 249.59 39.253 251.916 42.5486C254.255 45.8309 255.365 49.6469 255.365 53.8167C255.365 56.9112 254.675 59.8338 253.244 62.5031C252.104 64.6623 250.581 66.5265 248.711 68.0855C251.447 69.7483 253.658 71.9966 255.309 74.7798C257.146 77.8531 257.987 81.3596 257.987 85.1465C257.987 89.576 256.814 93.6497 254.409 97.2336C252.037 100.802 248.706 103.516 244.595 105.431C240.412 107.379 235.595 108.274 230.271 108.274H194.56ZM244.445 66.1032C244.662 65.967 244.875 65.8259 245.084 65.68C247.029 64.3036 248.536 62.5995 249.607 60.5676C250.699 58.5358 251.245 56.2855 251.245 53.8167C251.245 50.4084 250.35 47.448 248.558 44.9355C246.788 42.423 244.156 40.4785 240.66 39.1021C237.186 37.7257 232.893 37.0375 227.781 37.0375H198.679V104.154H230.271C235.143 104.154 239.338 103.335 242.856 101.696C246.373 100.058 249.082 97.8073 250.983 94.9453C252.906 92.0832 253.867 88.8169 253.867 85.1465C253.867 81.9786 253.168 79.2257 251.77 76.888C250.371 74.5284 248.493 72.6823 246.133 71.3496C245.682 71.0907 245.222 70.8546 244.752 70.6413C242.8 69.7553 240.683 69.2613 238.401 69.1594C238.357 69.1574 238.312 69.1556 238.268 69.1539V68.4984C238.276 68.4967 238.284 68.4951 238.293 68.4934C240.602 68.0248 242.653 67.2281 244.445 66.1032ZM228.809 80.0833L228.797 80.0783C228.133 79.7907 227.138 79.5659 225.683 79.5659H221.02V85.4836H225.421C228.065 85.4836 229.319 84.9764 229.832 84.6027C230.143 84.3635 230.477 83.9794 230.477 82.7869C230.477 81.8557 230.271 81.3515 230.077 81.0582C229.849 80.7117 229.491 80.3743 228.82 80.0881L228.809 80.0833ZM232.303 87.8993C230.774 89.0354 228.48 89.6035 225.421 89.6035H216.901V75.446H225.683C227.54 75.446 229.124 75.7301 230.435 76.2981C231.768 76.8662 232.795 77.6964 233.516 78.7888C234.237 79.8812 234.597 81.2139 234.597 82.7869C234.597 85.0373 233.833 86.7414 232.303 87.8993ZM228.114 58.8396L228.119 58.8327C228.228 58.6826 228.38 58.3975 228.38 57.7493C228.38 56.7949 228.117 56.5269 227.831 56.3166L227.815 56.305L227.799 56.2933C227.174 55.8244 226.219 55.4459 224.635 55.4459H221.02V60.0527H224.372C225.538 60.0527 226.399 59.869 227.035 59.6078C227.65 59.3551 227.943 59.0772 228.109 58.8465L228.114 58.8396ZM224.372 64.1726C225.967 64.1726 227.377 63.9213 228.6 63.4188C229.824 62.9163 230.774 62.1953 231.451 61.2559C232.15 60.2946 232.5 59.1257 232.5 57.7493C232.5 55.6737 231.757 54.0898 230.271 52.9974C228.786 51.8831 226.907 51.326 224.635 51.326H216.901V64.1726H224.372Z" fill="#FAFAFA"/>
25 <path d="M979.577 55.6468C979.388 53.2869 978.503 51.4461 976.922 50.1245C975.365 48.8029 972.993 48.1421 969.807 48.1421C967.777 48.1421 966.114 48.3899 964.816 48.8855C963.541 49.3575 962.597 50.0065 961.984 50.8325C961.37 51.6585 961.051 52.6025 961.028 53.6645C960.981 54.5377 961.134 55.3282 961.488 56.0362C961.866 56.7206 962.456 57.346 963.258 57.9124C964.06 58.4552 965.087 58.9508 966.338 59.3992C967.589 59.8476 969.075 60.2488 970.798 60.6028L976.745 61.8771C980.757 62.7267 984.191 63.8477 987.046 65.2401C989.902 66.6325 992.238 68.2726 994.056 70.1606C995.873 72.025 997.206 74.1253 998.056 76.4617C998.929 78.7981 999.377 81.3468 999.401 84.108C999.377 88.8751 998.185 92.9107 995.826 96.2146C993.466 99.5185 990.091 102.032 985.701 103.755C981.335 105.477 976.084 106.339 969.948 106.339C963.647 106.339 958.149 105.407 953.452 103.542C948.78 101.678 945.145 98.8106 942.549 94.9402C939.977 91.0463 938.679 86.0668 938.655 80.0017H957.346C957.464 82.22 958.019 84.0844 959.01 85.5948C960.001 87.1051 961.394 88.2497 963.187 89.0285C965.004 89.8073 967.164 90.1967 969.665 90.1967C971.766 90.1967 973.524 89.9371 974.94 89.4179C976.356 88.8987 977.43 88.1789 978.161 87.2585C978.893 86.3382 979.27 85.288 979.294 84.108C979.27 82.9988 978.905 82.0312 978.197 81.2052C977.512 80.3557 976.379 79.6005 974.798 78.9397C973.217 78.2553 971.081 77.6181 968.391 77.0281L961.169 75.4705C954.75 74.0781 949.688 71.7536 945.983 68.4968C942.302 65.2165 940.473 60.7444 940.496 55.0804C940.473 50.4785 941.7 46.4548 944.178 43.0092C946.679 39.5401 950.137 36.8379 954.55 34.9028C958.986 32.9676 964.072 32 969.807 32C975.66 32 980.722 32.9794 984.993 34.9382C989.265 36.8969 992.557 39.6581 994.87 43.2216C997.206 46.7616 998.386 50.9033 998.41 55.6468H979.577Z" fill="#FAFAFA"/>
26 <path d="M870.211 105.489V32.9912H889.893V62.3019H890.884L912.69 32.9912H935.629L911.132 65.2755L936.195 105.489H912.69L896.406 78.3025L889.893 86.7983V105.489H870.211Z" fill="#FAFAFA"/>
27 <path d="M803.624 105.489V32.9912H834.917C840.298 32.9912 845.006 33.9706 849.042 35.9293C853.077 37.8881 856.216 40.7083 858.458 44.3898C860.7 48.0714 861.821 52.4845 861.821 57.6292C861.821 62.8211 860.664 67.1988 858.352 70.7624C856.063 74.3259 852.841 77.0163 848.688 78.8335C844.558 80.6506 839.732 81.5592 834.209 81.5592H815.518V66.2667H830.245C832.557 66.2667 834.528 65.9835 836.156 65.4171C837.808 64.8271 839.071 63.8949 839.944 62.6205C840.841 61.3461 841.289 59.6824 841.289 57.6292C841.289 55.5524 840.841 53.8651 839.944 52.5671C839.071 51.2455 837.808 50.2779 836.156 49.6643C834.528 49.0271 832.557 48.7085 830.245 48.7085H823.306V105.489H803.624ZM846.103 72.2138L864.228 105.489H842.847L825.147 72.2138H846.103Z" fill="#FAFAFA"/>
28 <path d="M794.518 69.2402C794.518 77.3113 792.948 84.1198 789.81 89.6657C786.671 95.188 782.435 99.377 777.101 102.233C771.768 105.064 765.82 106.48 759.26 106.48C752.652 106.48 746.681 105.053 741.348 102.197C736.038 99.318 731.813 95.1172 728.675 89.5949C725.56 84.049 724.002 77.2641 724.002 69.2402C724.002 61.1691 725.56 54.3725 728.675 48.8501C731.813 43.3042 736.038 39.1153 741.348 36.2833C746.681 33.4278 752.652 32 759.26 32C765.82 32 771.768 33.4278 777.101 36.2833C782.435 39.1153 786.671 43.3042 789.81 48.8501C792.948 54.3725 794.518 61.1691 794.518 69.2402ZM774.269 69.2402C774.269 64.8979 773.691 61.2399 772.535 58.2664C771.402 55.2692 769.714 53.0037 767.472 51.4697C765.254 49.9121 762.517 49.1333 759.26 49.1333C756.003 49.1333 753.254 49.9121 751.012 51.4697C748.793 53.0037 747.106 55.2692 745.95 58.2664C744.817 61.2399 744.25 64.8979 744.25 69.2402C744.25 73.5826 744.817 77.2523 745.95 80.2495C747.106 83.223 748.793 85.4886 751.012 87.0461C753.254 88.5801 756.003 89.3471 759.26 89.3471C762.517 89.3471 765.254 88.5801 767.472 87.0461C769.714 85.4886 771.402 83.223 772.535 80.2495C773.691 77.2523 774.269 73.5826 774.269 69.2402Z" fill="#FAFAFA"/>
29 <path d="M638.238 105.489L616.857 32.9912H638.804L648.716 77.5945H649.283L661.035 32.9912H678.31L690.063 77.7361H690.629L700.541 32.9912H722.489L701.107 105.489H682.275L669.956 64.9923H669.389L657.07 105.489H638.238Z" fill="#FAFAFA"/>
30 </g>
31 <path d="M649.525 133.05H642.505V164.911H638.725V133.05H631.704V129.81H649.525V133.05Z" fill="#FAFAFA"/>
32 <path d="M622.242 155.191H626.022V158.161C626.022 160.501 625.302 162.355 623.862 163.723C622.422 165.055 620.19 165.721 617.165 165.721C614.141 165.721 611.891 165.055 610.415 163.723C608.939 162.355 608.201 160.501 608.201 158.161V136.56C608.201 134.22 608.921 132.384 610.361 131.052C611.801 129.684 614.033 129 617.057 129C620.082 129 622.332 129.684 623.808 131.052C625.284 132.384 626.022 134.22 626.022 136.56V141.69H622.242V136.56C622.242 135.192 621.81 134.13 620.946 133.374C620.117 132.618 618.821 132.24 617.057 132.24C615.329 132.24 614.051 132.618 613.223 133.374C612.395 134.13 611.981 135.192 611.981 136.56V158.161C611.981 159.493 612.395 160.555 613.223 161.347C614.087 162.103 615.401 162.481 617.165 162.481C618.893 162.481 620.171 162.103 621 161.347C621.828 160.555 622.242 159.493 622.242 158.161V155.191Z" fill="#FAFAFA"/>
33 <path d="M594.456 164.911V129.81H598.236V164.911H594.456Z" fill="#FAFAFA"/>
34 <path d="M551.898 164.911V129.81H566.749V133.05H555.678V145.471H564.589V148.711H555.678V164.911H551.898ZM574.309 129.81H578.089V161.671H587.971V164.911H574.309V129.81Z" fill="#FAFAFA"/>
35 <path d="M521.839 164.911V129.81H525.457L537.337 156.811V129.81H541.117V164.911H537.499L525.619 137.91V164.911H521.839Z" fill="#FAFAFA"/>
36 <path d="M481.068 155.191H484.848V158.161C484.848 160.501 484.128 162.355 482.688 163.723C481.248 165.055 479.015 165.721 475.991 165.721C472.967 165.721 470.717 165.055 469.241 163.723C467.765 162.355 467.027 160.501 467.027 158.161V136.56C467.027 134.22 467.747 132.384 469.187 131.052C470.627 129.684 472.859 129 475.883 129C478.907 129 481.158 129.684 482.634 131.052C484.11 132.384 484.848 134.22 484.848 136.56V141.69H481.068V136.56C481.068 135.192 480.635 134.13 479.771 133.374C478.943 132.618 477.647 132.24 475.883 132.24C474.155 132.24 472.877 132.618 472.049 133.374C471.221 134.13 470.807 135.192 470.807 136.56V158.161C470.807 159.493 471.221 160.555 472.049 161.347C472.913 162.103 474.227 162.481 475.991 162.481C477.719 162.481 478.997 162.103 479.825 161.347C480.653 160.555 481.068 159.493 481.068 158.161V155.191Z" fill="#FAFAFA"/>
37 <path d="M435.94 164.911V152.329L427.03 129.81H431.026L437.83 147.901L444.635 129.81H448.631L439.72 152.383V164.911H435.94Z" fill="#FAFAFA"/>
38 <path d="M411.97 129.81C414.85 129.81 416.992 130.494 418.396 131.862C419.836 133.194 420.556 135.03 420.556 137.37V140.61C420.556 143.67 419.278 145.633 416.722 146.497C419.638 147.289 421.096 149.287 421.096 152.491V157.351C421.096 159.691 420.394 161.545 418.99 162.913C417.586 164.245 415.426 164.911 412.51 164.911H403.816V129.81H411.97ZM416.776 137.37C416.776 136.038 416.362 134.994 415.534 134.238C414.742 133.446 413.554 133.05 411.97 133.05H407.596V145.201H411.97C413.59 145.201 414.796 144.768 415.588 143.904C416.38 143.04 416.776 141.942 416.776 140.61V137.37ZM417.316 152.491C417.316 151.159 416.902 150.061 416.074 149.197C415.282 148.333 414.094 147.901 412.51 147.901H407.596V161.671H412.51C414.13 161.671 415.336 161.293 416.128 160.537C416.92 159.745 417.316 158.683 417.316 157.351V152.491Z" fill="#FAFAFA"/>
39 <path d="M364.106 164.911V129.81H372.8C375.68 129.81 377.822 130.494 379.226 131.862C380.666 133.194 381.386 135.03 381.386 137.37V157.351C381.386 159.691 380.684 161.545 379.28 162.913C377.876 164.245 375.716 164.911 372.8 164.911H364.106ZM377.606 137.37C377.606 136.038 377.192 134.994 376.364 134.238C375.572 133.446 374.384 133.05 372.8 133.05H367.886V161.671H372.8C374.42 161.671 375.626 161.293 376.418 160.537C377.21 159.745 377.606 158.683 377.606 157.351V137.37Z" fill="#FAFAFA"/>
40 <path d="M350.335 155.191H354.115V158.161C354.115 160.501 353.395 162.355 351.955 163.723C350.515 165.055 348.283 165.721 345.259 165.721C342.235 165.721 339.985 165.055 338.509 163.723C337.033 162.355 336.295 160.501 336.295 158.161V136.56C336.295 134.22 337.015 132.384 338.455 131.052C339.895 129.684 342.127 129 345.151 129C348.175 129 350.425 129.684 351.901 131.052C353.377 132.384 354.115 134.22 354.115 136.56V148.441H340.075V158.161C340.075 159.493 340.489 160.555 341.317 161.347C342.181 162.103 343.495 162.481 345.259 162.481C346.987 162.481 348.265 162.103 349.093 161.347C349.921 160.555 350.335 159.493 350.335 158.161V155.191ZM340.075 145.2H350.335V136.56C350.335 135.192 349.903 134.13 349.039 133.374C348.211 132.618 346.915 132.24 345.151 132.24C343.423 132.24 342.145 132.618 341.317 133.374C340.489 134.13 340.075 135.192 340.075 136.56V145.2Z" fill="#FAFAFA"/>
41 <path d="M315.483 164.911V129.81H317.103L318.669 133.806C319.173 132.438 319.857 131.304 320.721 130.404C321.585 129.468 322.737 129 324.177 129C326.265 129 327.849 129.594 328.929 130.782C330.045 131.934 330.603 133.68 330.603 136.02V139.53H326.985V135.804C326.985 134.76 326.751 133.914 326.283 133.266C325.815 132.582 325.059 132.24 324.015 132.24C323.331 132.24 322.611 132.51 321.855 133.05C321.135 133.59 320.523 134.454 320.019 135.642C319.515 136.794 319.263 138.324 319.263 140.232V164.911H315.483Z" fill="#FAFAFA"/>
42 <path d="M301.713 155.191H305.493V158.161C305.493 160.501 304.773 162.355 303.333 163.723C301.893 165.055 299.661 165.721 296.636 165.721C293.612 165.721 291.362 165.055 289.886 163.723C288.41 162.355 287.672 160.501 287.672 158.161V136.56C287.672 134.22 288.392 132.384 289.832 131.052C291.272 129.684 293.504 129 296.528 129C299.553 129 301.803 129.684 303.279 131.052C304.755 132.384 305.493 134.22 305.493 136.56V148.441H291.452V158.161C291.452 159.493 291.866 160.555 292.694 161.347C293.558 162.103 294.872 162.481 296.636 162.481C298.365 162.481 299.643 162.103 300.471 161.347C301.299 160.555 301.713 159.493 301.713 158.161V155.191ZM291.452 145.2H301.713V136.56C301.713 135.192 301.281 134.13 300.417 133.374C299.589 132.618 298.293 132.24 296.528 132.24C294.8 132.24 293.522 132.618 292.694 133.374C291.866 134.13 291.452 135.192 291.452 136.56V145.2Z" fill="#FAFAFA"/>
43 <path d="M260.684 129.81H264.464V158.161C264.464 159.493 264.878 160.555 265.706 161.347C266.534 162.103 267.776 162.481 269.432 162.481C271.124 162.481 272.384 162.103 273.212 161.347C274.04 160.555 274.454 159.493 274.454 158.161V129.81H278.235V158.161C278.235 160.501 277.479 162.355 275.966 163.723C274.49 165.055 272.312 165.721 269.432 165.721C267.776 165.721 266.39 165.433 265.274 164.857C264.158 164.281 263.258 163.525 262.574 162.589C261.926 163.561 261.026 164.335 259.874 164.911C258.722 165.451 257.336 165.721 255.716 165.721C252.836 165.721 250.64 165.055 249.128 163.723C247.652 162.355 246.914 160.501 246.914 158.161V129.81H250.694V158.161C250.694 159.493 251.108 160.555 251.936 161.347C252.8 162.103 254.06 162.481 255.716 162.481C257.408 162.481 258.65 162.103 259.442 161.347C260.27 160.555 260.684 159.493 260.684 158.161V129.81Z" fill="#FAFAFA"/>
44 <path d="M237.476 158.161C237.476 160.501 236.72 162.355 235.208 163.723C233.732 165.055 231.446 165.721 228.35 165.721C225.254 165.721 222.932 165.055 221.383 163.723C219.871 162.355 219.115 160.501 219.115 158.161V136.56C219.115 134.22 219.853 132.384 221.329 131.052C222.842 129.684 225.146 129 228.242 129C231.338 129 233.642 129.684 235.154 131.052C236.702 132.384 237.476 134.22 237.476 136.56V158.161ZM233.696 136.56C233.696 135.192 233.246 134.13 232.346 133.374C231.446 132.618 230.078 132.24 228.242 132.24C226.442 132.24 225.092 132.618 224.192 133.374C223.328 134.13 222.896 135.192 222.896 136.56V158.161C222.896 159.493 223.346 160.555 224.246 161.347C225.146 162.103 226.514 162.481 228.35 162.481C230.15 162.481 231.482 162.103 232.346 161.347C233.246 160.555 233.696 159.493 233.696 158.161V136.56Z" fill="#FAFAFA"/>
45 <path d="M194.56 164.911V129.81H202.714C205.594 129.81 207.736 130.494 209.14 131.862C210.58 133.194 211.3 135.03 211.3 137.37V146.551C211.3 148.891 210.598 150.745 209.194 152.113C207.79 153.445 205.63 154.111 202.714 154.111H198.34V164.911H194.56ZM207.52 137.37C207.52 136.038 207.106 134.994 206.278 134.238C205.486 133.446 204.298 133.05 202.714 133.05H198.34V150.871H202.714C204.334 150.871 205.54 150.493 206.332 149.737C207.124 148.945 207.52 147.883 207.52 146.551V137.37Z" fill="#FAFAFA"/>
46 </g>
47 <defs>
48 <filter id="filter0_d_303_614" x="0" y="0" width="1033" height="213" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
49 <feFlood flood-opacity="0" result="BackgroundImageFix"/>
50 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
51 <feOffset dy="4"/>
52 <feGaussianBlur stdDeviation="2"/>
53 <feComposite in2="hardAlpha" operator="out"/>
54 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
55 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
56 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
57 </filter>
58 <filter id="filter1_d_303_614" x="190.56" y="32" width="812.841" height="85.1917" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
59 <feFlood flood-opacity="0" result="BackgroundImageFix"/>
60 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
61 <feOffset dy="4"/>
62 <feGaussianBlur stdDeviation="2"/>
63 <feComposite in2="hardAlpha" operator="out"/>
64 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
65 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
66 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
67 </filter>
68 <linearGradient id="paint0_linear_303_614" x1="48.4373" y1="83.8162" x2="111.888" y2="65.7367" gradientUnits="userSpaceOnUse">
69 <stop stop-color="#DD4652"/>
70 <stop offset="0.15" stop-color="#DD4854"/>
71 <stop offset="0.23" stop-color="#DE4F5B"/>
72 <stop offset="0.29" stop-color="#E15C67"/>
73 <stop offset="0.34" stop-color="#E46F78"/>
74 <stop offset="0.38" stop-color="#E9878F"/>
75 <stop offset="0.42" stop-color="#EEA5AB"/>
76 <stop offset="0.46" stop-color="#F5C8CC"/>
77 <stop offset="0.49" stop-color="#FCF0F1"/>
78 <stop offset="0.5" stop-color="#FEFDFD"/>
79 <stop offset="0.52" stop-color="#F7D4D8"/>
80 <stop offset="0.55" stop-color="#F1B0B7"/>
81 <stop offset="0.58" stop-color="#EB8F9A"/>
82 <stop offset="0.61" stop-color="#E67481"/>
83 <stop offset="0.64" stop-color="#E25E6D"/>
84 <stop offset="0.68" stop-color="#DF4D5E"/>
85 <stop offset="0.73" stop-color="#DD4153"/>
86 <stop offset="0.8" stop-color="#DC3A4D"/>
87 <stop offset="1" stop-color="#DC394C"/>
88 </linearGradient>
89 <linearGradient id="paint1_linear_303_614" x1="107.569" y1="12.2053" x2="11.6611" y2="39.2106" gradientUnits="userSpaceOnUse">
90 <stop stop-color="#8B3138" stop-opacity="0"/>
91 <stop offset="0.65" stop-color="#DC394C"/>
92 </linearGradient>
93 <linearGradient id="paint2_linear_303_614" x1="107.569" y1="189.634" x2="11.6864" y2="162.629" gradientUnits="userSpaceOnUse">
94 <stop stop-color="#8B3138" stop-opacity="0"/>
95 <stop offset="0.65" stop-color="#DC394C"/>
96 </linearGradient>
97 <linearGradient id="paint3_linear_303_614" x1="30.5598" y1="131.944" x2="125.396" y2="131.944" gradientUnits="userSpaceOnUse">
98 <stop stop-color="#8B3138" stop-opacity="0"/>
99 <stop offset="0.65" stop-color="#DC394C"/>
100 </linearGradient>
101 <linearGradient id="paint4_linear_303_614" x1="30.5847" y1="69.8189" x2="125.396" y2="69.8189" gradientUnits="userSpaceOnUse">
102 <stop stop-color="#8B3138" stop-opacity="0"/>
103 <stop offset="0.65" stop-color="#DC394C"/>
104 </linearGradient>
105 <linearGradient id="paint5_linear_303_614" x1="-48.1491" y1="140.528" x2="123.546" y2="139.131" gradientUnits="userSpaceOnUse">
106 <stop stop-color="#6A1921"/>
107 <stop offset="1" stop-color="#DC394C"/>
108 </linearGradient>
109 </defs>
110 </svg>
--- a/static/css/input.css
+++ b/static/css/input.css
@@ -0,0 +1,4 @@
1
+/* Tailwind CSS is loaded via CDN in base.html for development.
2
+ For production, use the Tailwind standalone CLI:
3
+ npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --minify
4
+*/
--- a/static/css/input.css
+++ b/static/css/input.css
@@ -0,0 +1,4 @@
 
 
 
 
--- a/static/css/input.css
+++ b/static/css/input.css
@@ -0,0 +1,4 @@
1 /* Tailwind CSS is loaded via CDN in base.html for development.
2 For production, use the Tailwind standalone CLI:
3 npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --minify
4 */
--- a/static/img/fossilrepo-logo-dark.svg
+++ b/static/img/fossilrepo-logo-dark.svg
@@ -0,0 +1,110 @@
1
+<svg width="1033" height="213" viewBox="0 0 1033 213" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<g filter="url(#filter0_d_303_614)">
3
+<rect x="4" width="1025" height="205" fill="#2B2D2C"/>
4
+<path d="M491.56 167H500.064V163.116H495.59V132.884H500.138V129H491.56V167Z" fill="#DB394C"/>
5
+<path d="M506.018 129V132.884H510.529V163.116H505.981V167H514.56V129H506.018Z" fill="#DB394C"/>
6
+<path d="M505.093 139.932H501.026V155.788H505.093V139.932Z" fill="#DB394C"/>
7
+<path d="M148.624 101.062C168.554 83.5327 166.323 44.969 144.11 30.1725C134.17 22.9805 119.767 19.0365 107.519 19.1396C101.434 19.6293 95.7028 22.4134 90.7074 26.048C84.6217 30.5592 79.7024 35.9209 72.9573 40.2774C65.1219 44.969 54.1422 50.4597 44.0754 49.2223C37.3303 47.3921 37.1782 40.8703 37.4825 34.9929C37.4825 34.9414 37.4825 34.8641 37.4825 34.8125C37.2796 34.8125 37.1275 34.8641 36.9246 34.8641C34.4903 34.8641 32.4871 34.1681 30.8896 33.0854C30.8135 33.7041 30.7374 34.3485 30.7374 34.9929C29.6724 46.129 34.2367 55.873 46.2814 56.3628C61.9776 56.5175 77.0145 46.7734 88.0956 36.9005C97.5793 27.9556 106.125 23.3156 119.006 26.7956C132.522 29.1929 144.871 36.1014 151.311 48.6294C160.034 65.849 155.216 92.9158 136.477 101.01C155.216 109.079 160.034 136.171 151.311 153.391C144.871 165.919 132.395 172.518 118.879 174.915C105.947 178.395 97.63 174.09 88.0956 165.12C77.0145 155.247 61.9523 145.503 46.2814 145.657C34.2367 146.199 29.6724 155.865 30.7374 167.053C30.7374 173.523 34.5917 179.117 40.0943 181.54C40.0182 180.354 40.0943 179.194 40.3986 178.112C40.7536 176.849 41.2861 175.766 41.9454 174.812C39.2575 173.265 37.4317 170.378 37.4317 167.053C37.1275 161.201 37.2542 154.654 44.0246 152.824C54.0661 151.586 65.0459 157.077 72.9066 161.768C79.6517 166.125 84.571 171.513 90.6567 175.998C95.6521 179.633 101.383 182.417 107.469 182.906C119.716 183.009 134.119 179.065 144.059 171.873C166.272 157.103 168.529 118.539 148.573 100.984L148.624 101.062Z" fill="#DC394C"/>
8
+<path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="#DC394C"/>
9
+<g opacity="0.2">
10
+<path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="url(#paint0_linear_303_614)"/>
11
+</g>
12
+<path d="M37.7352 41.4374C37.4056 39.3236 37.4563 37.0294 37.5577 34.8382C37.5577 29.8889 41.5135 25.8676 46.3821 25.8676H79.5241C80.158 25.8676 80.7412 25.6356 81.223 25.2231C83.6827 23.1093 86.8523 20.7893 89.8445 19.0364H46.4074C37.8367 19.0364 30.8634 26.1253 30.8634 34.8382C30.483 38.7823 30.8381 42.5716 31.9538 45.8196C33.3991 43.7574 35.4277 42.1076 37.7606 41.4632L37.7352 41.4374Z" fill="url(#paint1_linear_303_614)"/>
13
+<path d="M37.7352 160.402C37.4056 162.516 37.4563 164.81 37.5577 167.001C37.5577 171.951 41.5135 175.972 46.3821 175.972H79.5241C80.158 175.972 80.7412 176.204 81.223 176.617C83.6827 178.73 86.8523 181.05 89.8445 182.803H46.4074C37.8367 182.803 30.8634 175.714 30.8634 167.001C30.483 163.057 30.8381 159.268 31.9538 156.02C33.3991 158.082 35.4277 159.732 37.7606 160.376L37.7352 160.402Z" fill="url(#paint2_linear_303_614)"/>
14
+<path d="M102.042 111.167C102.042 111.167 35.8849 111.167 30.5852 111.167V117.998H102.042C107.494 117.998 111.779 119.183 114.848 121.555C119.615 124.88 120.604 139.11 114.366 142.719C106.48 148.209 82.0859 144.781 72.8051 145.683C76.5833 147.436 80.3616 149.292 83.6327 151.483C84.5962 152.128 85.712 152.488 86.853 152.463C103.437 152.179 125.929 157.129 125.396 132.665C125.599 117.276 115.938 111.321 102.042 111.167Z" fill="url(#paint3_linear_303_614)"/>
15
+<path d="M87.7147 49.2997C86.3454 49.2997 84.9761 49.6606 83.8097 50.4339C80.6146 52.5735 76.9125 54.3779 73.2103 56.0793C82.77 56.9815 106.226 53.5273 114.34 59.0437C120.578 62.6526 119.563 76.9078 114.822 80.2074C111.753 82.5531 107.468 83.7647 102.016 83.7647H30.5593V90.5958H102.016C115.912 90.4669 125.573 84.4865 125.37 69.0971C125.928 44.8401 104.324 49.5059 87.6893 49.2997H87.7147Z" fill="url(#paint4_linear_303_614)"/>
16
+<path d="M121.263 126.427C120.198 126.608 119.133 126.814 118.068 127.072C119.539 132.614 118.702 140.218 114.391 142.744C106.505 148.235 82.1114 144.807 72.8306 145.709C76.6088 147.462 80.3871 149.318 83.6582 151.509C84.6217 152.153 85.7375 152.514 86.8785 152.489C103.462 152.205 125.954 157.154 125.422 132.691C125.447 130.062 125.168 127.768 124.661 125.654C123.596 126.04 122.48 126.298 121.288 126.427H121.263Z" fill="url(#paint5_linear_303_614)"/>
17
+<g filter="url(#filter1_d_303_614)">
18
+<rect x="195.06" y="32.5" width="803.841" height="76.1916" stroke="#2B2D2C" shape-rendering="crispEdges"/>
19
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.696 108.274V32.9176H582.786C588.268 32.9176 593.264 33.9167 597.661 36.0512C602.078 38.1951 605.594 41.3351 608.098 45.4472C610.637 49.6163 611.813 54.4753 611.813 59.8467C611.813 65.2685 610.6 70.1409 607.941 74.2414C606.304 76.7879 604.248 78.9243 601.812 80.6522L616.857 108.274H587.652L576.157 86.6632V108.274H549.696ZM590.127 104.154H609.921L596.291 79.1301C597.57 78.5163 598.748 77.8075 599.824 77.0036C601.677 75.6192 603.23 73.9531 604.481 72.005C606.622 68.706 607.693 64.6532 607.693 59.8467C607.693 55.0838 606.655 50.9983 604.579 47.59C602.504 44.1817 599.598 41.5709 595.862 39.7576C592.126 37.9442 587.768 37.0375 582.786 37.0375H553.816V104.154H572.037V82.0004H578.343L590.127 104.154ZM572.037 67.843H578.46C580.601 67.843 582.426 67.5808 583.933 67.0565C585.463 66.5103 586.631 65.6473 587.44 64.4675C588.27 63.2877 588.685 61.7474 588.685 59.8467C588.685 57.9241 588.27 56.3619 587.44 55.1603C586.631 53.9368 585.463 53.0411 583.933 52.473C582.426 51.8831 580.601 51.5882 578.46 51.5882H572.037V67.843ZM576.157 55.7081V63.7231H578.46C580.277 63.7231 581.612 63.499 582.563 63.171C583.38 62.8765 583.79 62.5051 584.041 62.1388L584.056 62.1176L584.071 62.0965C584.278 61.8011 584.565 61.1655 584.565 59.8467C584.565 58.4992 584.275 57.8278 584.05 57.5022L584.026 57.467L584.002 57.4314C583.73 57.019 583.305 56.6347 582.499 56.3351L582.465 56.3227L582.432 56.3096C581.53 55.9566 580.244 55.7081 578.46 55.7081H576.157Z" fill="#FAFAFA"/>
20
+<path fill-rule="evenodd" clip-rule="evenodd" d="M475.677 108.274V32.9176H532.288V55.8391H502.138V59.1351H529.797V82.0566H502.138V85.3525H532.157V108.274H475.677ZM498.018 89.4724V77.9367H525.677V63.2549H498.018V51.7193H528.168V37.0375H479.797V104.154H528.037V89.4724H498.018Z" fill="#FAFAFA"/>
21
+<path fill-rule="evenodd" clip-rule="evenodd" d="M406.344 108.274V32.9176H432.805V85.3525H459.94V108.274H406.344ZM428.685 89.4724V37.0375H410.464V104.154H455.82V89.4724H428.685Z" fill="#FAFAFA"/>
22
+<path fill-rule="evenodd" clip-rule="evenodd" d="M389.337 32.9176V108.274H362.876V32.9176H389.337ZM385.217 37.0375H366.996V104.154H385.217V37.0375Z" fill="#FAFAFA"/>
23
+<path fill-rule="evenodd" clip-rule="evenodd" d="M341.181 91.5344L341.177 91.5409C337.907 97.2938 333.433 101.742 327.775 104.771L327.768 104.775L327.762 104.778C322.179 107.742 316.001 109.192 309.313 109.192C302.581 109.192 296.376 107.731 290.786 104.738L290.776 104.733L290.767 104.728C285.138 101.676 280.682 97.2211 277.416 91.4754L277.411 91.4663L277.406 91.4573C274.097 85.5658 272.552 78.5492 272.552 70.5958C272.552 62.6044 274.095 55.5718 277.41 49.6951L277.413 49.69C280.679 43.9181 285.143 39.4637 290.789 36.4517C296.378 33.46 302.582 32 309.313 32C316.004 32 322.185 33.4617 327.769 36.4501C333.435 39.4604 337.911 43.9139 341.18 49.6886C344.519 55.5668 346.074 62.602 346.074 70.5958C346.074 78.5928 344.518 85.6372 341.181 91.5344ZM325.83 40.0853C320.892 37.4417 315.387 36.1199 309.313 36.1199C303.196 36.1199 297.668 37.4417 292.731 40.0853C287.815 42.707 283.904 46.585 280.998 51.7193C278.114 56.8317 276.672 63.1239 276.672 70.5958C276.672 78.0241 278.114 84.3054 280.998 89.4396C283.904 94.552 287.815 98.4409 292.731 101.106C297.668 103.75 303.196 105.072 309.313 105.072C315.387 105.072 320.892 103.761 325.83 101.139C330.768 98.4956 334.689 94.6176 337.595 89.5052C340.501 84.3709 341.954 78.0678 341.954 70.5958C341.954 63.1239 340.501 56.8317 337.595 51.7193C334.689 46.585 330.768 42.707 325.83 40.0853ZM317.756 61.9115L317.749 61.8931C316.943 59.7622 315.851 58.4077 314.59 57.5445L314.569 57.5305L314.549 57.5161C313.319 56.6524 311.655 56.1013 309.313 56.1013C306.973 56.1013 305.289 56.6519 304.028 57.5279L304.02 57.533C302.77 58.3979 301.668 59.7627 300.838 61.9119C300.021 64.0587 299.538 66.9113 299.538 70.5958C299.538 74.2732 300.019 77.1389 300.838 79.3129C301.662 81.4232 302.759 82.7991 304.024 83.6938C305.285 84.5503 306.973 85.0903 309.313 85.0903C311.665 85.0903 313.337 84.545 314.569 83.6941C315.854 82.8 316.95 81.4286 317.753 79.3212L317.756 79.313L317.759 79.3048C318.595 77.1376 319.088 74.2759 319.088 70.5958C319.088 66.9141 318.594 64.0686 317.763 61.9298L317.756 61.9115ZM316.916 87.08C314.862 88.5002 312.328 89.2102 309.313 89.2102C306.298 89.2102 303.753 88.5002 301.677 87.08C299.624 85.6381 298.061 83.5407 296.991 80.7879C295.942 78.0132 295.418 74.6158 295.418 70.5958C295.418 66.5758 295.942 63.1894 296.991 60.4366C298.061 57.6619 299.624 55.5645 301.677 54.1444C303.753 52.7024 306.298 51.9814 309.313 51.9814C312.328 51.9814 314.862 52.7024 316.916 54.1444C318.992 55.5645 320.554 57.6619 321.602 60.4366C322.673 63.1894 323.208 66.5758 323.208 70.5958C323.208 74.6158 322.673 78.0132 321.602 80.7879C320.554 83.5407 318.992 85.6381 316.916 87.08Z" fill="#FAFAFA"/>
24
+<path fill-rule="evenodd" clip-rule="evenodd" d="M194.56 108.274V32.9176H227.781C233.232 32.9176 238.075 33.6472 242.173 35.2702C246.242 36.8731 249.59 39.253 251.916 42.5486C254.255 45.8309 255.365 49.6469 255.365 53.8167C255.365 56.9112 254.675 59.8338 253.244 62.5031C252.104 64.6623 250.581 66.5265 248.711 68.0855C251.447 69.7483 253.658 71.9966 255.309 74.7798C257.146 77.8531 257.987 81.3596 257.987 85.1465C257.987 89.576 256.814 93.6497 254.409 97.2336C252.037 100.802 248.706 103.516 244.595 105.431C240.412 107.379 235.595 108.274 230.271 108.274H194.56ZM244.445 66.1032C244.662 65.967 244.875 65.8259 245.084 65.68C247.029 64.3036 248.536 62.5995 249.607 60.5676C250.699 58.5358 251.245 56.2855 251.245 53.8167C251.245 50.4084 250.35 47.448 248.558 44.9355C246.788 42.423 244.156 40.4785 240.66 39.1021C237.186 37.7257 232.893 37.0375 227.781 37.0375H198.679V104.154H230.271C235.143 104.154 239.338 103.335 242.856 101.696C246.373 100.058 249.082 97.8073 250.983 94.9453C252.906 92.0832 253.867 88.8169 253.867 85.1465C253.867 81.9786 253.168 79.2257 251.77 76.888C250.371 74.5284 248.493 72.6823 246.133 71.3496C245.682 71.0907 245.222 70.8546 244.752 70.6413C242.8 69.7553 240.683 69.2613 238.401 69.1594C238.357 69.1574 238.312 69.1556 238.268 69.1539V68.4984C238.276 68.4967 238.284 68.4951 238.293 68.4934C240.602 68.0248 242.653 67.2281 244.445 66.1032ZM228.809 80.0833L228.797 80.0783C228.133 79.7907 227.138 79.5659 225.683 79.5659H221.02V85.4836H225.421C228.065 85.4836 229.319 84.9764 229.832 84.6027C230.143 84.3635 230.477 83.9794 230.477 82.7869C230.477 81.8557 230.271 81.3515 230.077 81.0582C229.849 80.7117 229.491 80.3743 228.82 80.0881L228.809 80.0833ZM232.303 87.8993C230.774 89.0354 228.48 89.6035 225.421 89.6035H216.901V75.446H225.683C227.54 75.446 229.124 75.7301 230.435 76.2981C231.768 76.8662 232.795 77.6964 233.516 78.7888C234.237 79.8812 234.597 81.2139 234.597 82.7869C234.597 85.0373 233.833 86.7414 232.303 87.8993ZM228.114 58.8396L228.119 58.8327C228.228 58.6826 228.38 58.3975 228.38 57.7493C228.38 56.7949 228.117 56.5269 227.831 56.3166L227.815 56.305L227.799 56.2933C227.174 55.8244 226.219 55.4459 224.635 55.4459H221.02V60.0527H224.372C225.538 60.0527 226.399 59.869 227.035 59.6078C227.65 59.3551 227.943 59.0772 228.109 58.8465L228.114 58.8396ZM224.372 64.1726C225.967 64.1726 227.377 63.9213 228.6 63.4188C229.824 62.9163 230.774 62.1953 231.451 61.2559C232.15 60.2946 232.5 59.1257 232.5 57.7493C232.5 55.6737 231.757 54.0898 230.271 52.9974C228.786 51.8831 226.907 51.326 224.635 51.326H216.901V64.1726H224.372Z" fill="#FAFAFA"/>
25
+<path d="M979.577 55.6468C979.388 53.2869 978.503 51.4461 976.922 50.1245C975.365 48.8029 972.993 48.1421 969.807 48.1421C967.777 48.1421 966.114 48.3899 964.816 48.8855C963.541 49.3575 962.597 50.0065 961.984 50.8325C961.37 51.6585 961.051 52.6025 961.028 53.6645C960.981 54.5377 961.134 55.3282 961.488 56.0362C961.866 56.7206 962.456 57.346 963.258 57.9124C964.06 58.4552 965.087 58.9508 966.338 59.3992C967.589 59.8476 969.075 60.2488 970.798 60.6028L976.745 61.8771C980.757 62.7267 984.191 63.8477 987.046 65.2401C989.902 66.6325 992.238 68.2726 994.056 70.1606C995.873 72.025 997.206 74.1253 998.056 76.4617C998.929 78.7981 999.377 81.3468 999.401 84.108C999.377 88.8751 998.185 92.9107 995.826 96.2146C993.466 99.5185 990.091 102.032 985.701 103.755C981.335 105.477 976.084 106.339 969.948 106.339C963.647 106.339 958.149 105.407 953.452 103.542C948.78 101.678 945.145 98.8106 942.549 94.9402C939.977 91.0463 938.679 86.0668 938.655 80.0017H957.346C957.464 82.22 958.019 84.0844 959.01 85.5948C960.001 87.1051 961.394 88.2497 963.187 89.0285C965.004 89.8073 967.164 90.1967 969.665 90.1967C971.766 90.1967 973.524 89.9371 974.94 89.4179C976.356 88.8987 977.43 88.1789 978.161 87.2585C978.893 86.3382 979.27 85.288 979.294 84.108C979.27 82.9988 978.905 82.0312 978.197 81.2052C977.512 80.3557 976.379 79.6005 974.798 78.9397C973.217 78.2553 971.081 77.6181 968.391 77.0281L961.169 75.4705C954.75 74.0781 949.688 71.7536 945.983 68.4968C942.302 65.2165 940.473 60.7444 940.496 55.0804C940.473 50.4785 941.7 46.4548 944.178 43.0092C946.679 39.5401 950.137 36.8379 954.55 34.9028C958.986 32.9676 964.072 32 969.807 32C975.66 32 980.722 32.9794 984.993 34.9382C989.265 36.8969 992.557 39.6581 994.87 43.2216C997.206 46.7616 998.386 50.9033 998.41 55.6468H979.577Z" fill="#FAFAFA"/>
26
+<path d="M870.211 105.489V32.9912H889.893V62.3019H890.884L912.69 32.9912H935.629L911.132 65.2755L936.195 105.489H912.69L896.406 78.3025L889.893 86.7983V105.489H870.211Z" fill="#FAFAFA"/>
27
+<path d="M803.624 105.489V32.9912H834.917C840.298 32.9912 845.006 33.9706 849.042 35.9293C853.077 37.8881 856.216 40.7083 858.458 44.3898C860.7 48.0714 861.821 52.4845 861.821 57.6292C861.821 62.8211 860.664 67.1988 858.352 70.7624C856.063 74.3259 852.841 77.0163 848.688 78.8335C844.558 80.6506 839.732 81.5592 834.209 81.5592H815.518V66.2667H830.245C832.557 66.2667 834.528 65.9835 836.156 65.4171C837.808 64.8271 839.071 63.8949 839.944 62.6205C840.841 61.3461 841.289 59.6824 841.289 57.6292C841.289 55.5524 840.841 53.8651 839.944 52.5671C839.071 51.2455 837.808 50.2779 836.156 49.6643C834.528 49.0271 832.557 48.7085 830.245 48.7085H823.306V105.489H803.624ZM846.103 72.2138L864.228 105.489H842.847L825.147 72.2138H846.103Z" fill="#FAFAFA"/>
28
+<path d="M794.518 69.2402C794.518 77.3113 792.948 84.1198 789.81 89.6657C786.671 95.188 782.435 99.377 777.101 102.233C771.768 105.064 765.82 106.48 759.26 106.48C752.652 106.48 746.681 105.053 741.348 102.197C736.038 99.318 731.813 95.1172 728.675 89.5949C725.56 84.049 724.002 77.2641 724.002 69.2402C724.002 61.1691 725.56 54.3725 728.675 48.8501C731.813 43.3042 736.038 39.1153 741.348 36.2833C746.681 33.4278 752.652 32 759.26 32C765.82 32 771.768 33.4278 777.101 36.2833C782.435 39.1153 786.671 43.3042 789.81 48.8501C792.948 54.3725 794.518 61.1691 794.518 69.2402ZM774.269 69.2402C774.269 64.8979 773.691 61.2399 772.535 58.2664C771.402 55.2692 769.714 53.0037 767.472 51.4697C765.254 49.9121 762.517 49.1333 759.26 49.1333C756.003 49.1333 753.254 49.9121 751.012 51.4697C748.793 53.0037 747.106 55.2692 745.95 58.2664C744.817 61.2399 744.25 64.8979 744.25 69.2402C744.25 73.5826 744.817 77.2523 745.95 80.2495C747.106 83.223 748.793 85.4886 751.012 87.0461C753.254 88.5801 756.003 89.3471 759.26 89.3471C762.517 89.3471 765.254 88.5801 767.472 87.0461C769.714 85.4886 771.402 83.223 772.535 80.2495C773.691 77.2523 774.269 73.5826 774.269 69.2402Z" fill="#FAFAFA"/>
29
+<path d="M638.238 105.489L616.857 32.9912H638.804L648.716 77.5945H649.283L661.035 32.9912H678.31L690.063 77.7361H690.629L700.541 32.9912H722.489L701.107 105.489H682.275L669.956 64.9923H669.389L657.07 105.489H638.238Z" fill="#FAFAFA"/>
30
+</g>
31
+<path d="M649.525 133.05H642.505V164.911H638.725V133.05H631.704V129.81H649.525V133.05Z" fill="#FAFAFA"/>
32
+<path d="M622.242 155.191H626.022V158.161C626.022 160.501 625.302 162.355 623.862 163.723C622.422 165.055 620.19 165.721 617.165 165.721C614.141 165.721 611.891 165.055 610.415 163.723C608.939 162.355 608.201 160.501 608.201 158.161V136.56C608.201 134.22 608.921 132.384 610.361 131.052C611.801 129.684 614.033 129 617.057 129C620.082 129 622.332 129.684 623.808 131.052C625.284 132.384 626.022 134.22 626.022 136.56V141.69H622.242V136.56C622.242 135.192 621.81 134.13 620.946 133.374C620.117 132.618 618.821 132.24 617.057 132.24C615.329 132.24 614.051 132.618 613.223 133.374C612.395 134.13 611.981 135.192 611.981 136.56V158.161C611.981 159.493 612.395 160.555 613.223 161.347C614.087 162.103 615.401 162.481 617.165 162.481C618.893 162.481 620.171 162.103 621 161.347C621.828 160.555 622.242 159.493 622.242 158.161V155.191Z" fill="#FAFAFA"/>
33
+<path d="M594.456 164.911V129.81H598.236V164.911H594.456Z" fill="#FAFAFA"/>
34
+<path d="M551.898 164.911V129.81H566.749V133.05H555.678V145.471H564.589V148.711H555.678V164.911H551.898ZM574.309 129.81H578.089V161.671H587.971V164.911H574.309V129.81Z" fill="#FAFAFA"/>
35
+<path d="M521.839 164.911V129.81H525.457L537.337 156.811V129.81H541.117V164.911H537.499L525.619 137.91V164.911H521.839Z" fill="#FAFAFA"/>
36
+<path d="M481.068 155.191H484.848V158.161C484.848 160.501 484.128 162.355 482.688 163.723C481.248 165.055 479.015 165.721 475.991 165.721C472.967 165.721 470.717 165.055 469.241 163.723C467.765 162.355 467.027 160.501 467.027 158.161V136.56C467.027 134.22 467.747 132.384 469.187 131.052C470.627 129.684 472.859 129 475.883 129C478.907 129 481.158 129.684 482.634 131.052C484.11 132.384 484.848 134.22 484.848 136.56V141.69H481.068V136.56C481.068 135.192 480.635 134.13 479.771 133.374C478.943 132.618 477.647 132.24 475.883 132.24C474.155 132.24 472.877 132.618 472.049 133.374C471.221 134.13 470.807 135.192 470.807 136.56V158.161C470.807 159.493 471.221 160.555 472.049 161.347C472.913 162.103 474.227 162.481 475.991 162.481C477.719 162.481 478.997 162.103 479.825 161.347C480.653 160.555 481.068 159.493 481.068 158.161V155.191Z" fill="#FAFAFA"/>
37
+<path d="M435.94 164.911V152.329L427.03 129.81H431.026L437.83 147.901L444.635 129.81H448.631L439.72 152.383V164.911H435.94Z" fill="#FAFAFA"/>
38
+<path d="M411.97 129.81C414.85 129.81 416.992 130.494 418.396 131.862C419.836 133.194 420.556 135.03 420.556 137.37V140.61C420.556 143.67 419.278 145.633 416.722 146.497C419.638 147.289 421.096 149.287 421.096 152.491V157.351C421.096 159.691 420.394 161.545 418.99 162.913C417.586 164.245 415.426 164.911 412.51 164.911H403.816V129.81H411.97ZM416.776 137.37C416.776 136.038 416.362 134.994 415.534 134.238C414.742 133.446 413.554 133.05 411.97 133.05H407.596V145.201H411.97C413.59 145.201 414.796 144.768 415.588 143.904C416.38 143.04 416.776 141.942 416.776 140.61V137.37ZM417.316 152.491C417.316 151.159 416.902 150.061 416.074 149.197C415.282 148.333 414.094 147.901 412.51 147.901H407.596V161.671H412.51C414.13 161.671 415.336 161.293 416.128 160.537C416.92 159.745 417.316 158.683 417.316 157.351V152.491Z" fill="#FAFAFA"/>
39
+<path d="M364.106 164.911V129.81H372.8C375.68 129.81 377.822 130.494 379.226 131.862C380.666 133.194 381.386 135.03 381.386 137.37V157.351C381.386 159.691 380.684 161.545 379.28 162.913C377.876 164.245 375.716 164.911 372.8 164.911H364.106ZM377.606 137.37C377.606 136.038 377.192 134.994 376.364 134.238C375.572 133.446 374.384 133.05 372.8 133.05H367.886V161.671H372.8C374.42 161.671 375.626 161.293 376.418 160.537C377.21 159.745 377.606 158.683 377.606 157.351V137.37Z" fill="#FAFAFA"/>
40
+<path d="M350.335 155.191H354.115V158.161C354.115 160.501 353.395 162.355 351.955 163.723C350.515 165.055 348.283 165.721 345.259 165.721C342.235 165.721 339.985 165.055 338.509 163.723C337.033 162.355 336.295 160.501 336.295 158.161V136.56C336.295 134.22 337.015 132.384 338.455 131.052C339.895 129.684 342.127 129 345.151 129C348.175 129 350.425 129.684 351.901 131.052C353.377 132.384 354.115 134.22 354.115 136.56V148.441H340.075V158.161C340.075 159.493 340.489 160.555 341.317 161.347C342.181 162.103 343.495 162.481 345.259 162.481C346.987 162.481 348.265 162.103 349.093 161.347C349.921 160.555 350.335 159.493 350.335 158.161V155.191ZM340.075 145.2H350.335V136.56C350.335 135.192 349.903 134.13 349.039 133.374C348.211 132.618 346.915 132.24 345.151 132.24C343.423 132.24 342.145 132.618 341.317 133.374C340.489 134.13 340.075 135.192 340.075 136.56V145.2Z" fill="#FAFAFA"/>
41
+<path d="M315.483 164.911V129.81H317.103L318.669 133.806C319.173 132.438 319.857 131.304 320.721 130.404C321.585 129.468 322.737 129 324.177 129C326.265 129 327.849 129.594 328.929 130.782C330.045 131.934 330.603 133.68 330.603 136.02V139.53H326.985V135.804C326.985 134.76 326.751 133.914 326.283 133.266C325.815 132.582 325.059 132.24 324.015 132.24C323.331 132.24 322.611 132.51 321.855 133.05C321.135 133.59 320.523 134.454 320.019 135.642C319.515 136.794 319.263 138.324 319.263 140.232V164.911H315.483Z" fill="#FAFAFA"/>
42
+<path d="M301.713 155.191H305.493V158.161C305.493 160.501 304.773 162.355 303.333 163.723C301.893 165.055 299.661 165.721 296.636 165.721C293.612 165.721 291.362 165.055 289.886 163.723C288.41 162.355 287.672 160.501 287.672 158.161V136.56C287.672 134.22 288.392 132.384 289.832 131.052C291.272 129.684 293.504 129 296.528 129C299.553 129 301.803 129.684 303.279 131.052C304.755 132.384 305.493 134.22 305.493 136.56V148.441H291.452V158.161C291.452 159.493 291.866 160.555 292.694 161.347C293.558 162.103 294.872 162.481 296.636 162.481C298.365 162.481 299.643 162.103 300.471 161.347C301.299 160.555 301.713 159.493 301.713 158.161V155.191ZM291.452 145.2H301.713V136.56C301.713 135.192 301.281 134.13 300.417 133.374C299.589 132.618 298.293 132.24 296.528 132.24C294.8 132.24 293.522 132.618 292.694 133.374C291.866 134.13 291.452 135.192 291.452 136.56V145.2Z" fill="#FAFAFA"/>
43
+<path d="M260.684 129.81H264.464V158.161C264.464 159.493 264.878 160.555 265.706 161.347C266.534 162.103 267.776 162.481 269.432 162.481C271.124 162.481 272.384 162.103 273.212 161.347C274.04 160.555 274.454 159.493 274.454 158.161V129.81H278.235V158.161C278.235 160.501 277.479 162.355 275.966 163.723C274.49 165.055 272.312 165.721 269.432 165.721C267.776 165.721 266.39 165.433 265.274 164.857C264.158 164.281 263.258 163.525 262.574 162.589C261.926 163.561 261.026 164.335 259.874 164.911C258.722 165.451 257.336 165.721 255.716 165.721C252.836 165.721 250.64 165.055 249.128 163.723C247.652 162.355 246.914 160.501 246.914 158.161V129.81H250.694V158.161C250.694 159.493 251.108 160.555 251.936 161.347C252.8 162.103 254.06 162.481 255.716 162.481C257.408 162.481 258.65 162.103 259.442 161.347C260.27 160.555 260.684 159.493 260.684 158.161V129.81Z" fill="#FAFAFA"/>
44
+<path d="M237.476 158.161C237.476 160.501 236.72 162.355 235.208 163.723C233.732 165.055 231.446 165.721 228.35 165.721C225.254 165.721 222.932 165.055 221.383 163.723C219.871 162.355 219.115 160.501 219.115 158.161V136.56C219.115 134.22 219.853 132.384 221.329 131.052C222.842 129.684 225.146 129 228.242 129C231.338 129 233.642 129.684 235.154 131.052C236.702 132.384 237.476 134.22 237.476 136.56V158.161ZM233.696 136.56C233.696 135.192 233.246 134.13 232.346 133.374C231.446 132.618 230.078 132.24 228.242 132.24C226.442 132.24 225.092 132.618 224.192 133.374C223.328 134.13 222.896 135.192 222.896 136.56V158.161C222.896 159.493 223.346 160.555 224.246 161.347C225.146 162.103 226.514 162.481 228.35 162.481C230.15 162.481 231.482 162.103 232.346 161.347C233.246 160.555 233.696 159.493 233.696 158.161V136.56Z" fill="#FAFAFA"/>
45
+<path d="M194.56 164.911V129.81H202.714C205.594 129.81 207.736 130.494 209.14 131.862C210.58 133.194 211.3 135.03 211.3 137.37V146.551C211.3 148.891 210.598 150.745 209.194 152.113C207.79 153.445 205.63 154.111 202.714 154.111H198.34V164.911H194.56ZM207.52 137.37C207.52 136.038 207.106 134.994 206.278 134.238C205.486 133.446 204.298 133.05 202.714 133.05H198.34V150.871H202.714C204.334 150.871 205.54 150.493 206.332 149.737C207.124 148.945 207.52 147.883 207.52 146.551V137.37Z" fill="#FAFAFA"/>
46
+</g>
47
+<defs>
48
+<filter id="filter0_d_303_614" x="0" y="0" width="1033" height="213" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
49
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
50
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
51
+<feOffset dy="4"/>
52
+<feGaussianBlur stdDeviation="2"/>
53
+<feComposite in2="hardAlpha" operator="out"/>
54
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
55
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
56
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
57
+</filter>
58
+<filter id="filter1_d_303_614" x="190.56" y="32" width="812.841" height="85.1917" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
59
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
60
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
61
+<feOffset dy="4"/>
62
+<feGaussianBlur stdDeviation="2"/>
63
+<feComposite in2="hardAlpha" operator="out"/>
64
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
65
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
66
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
67
+</filter>
68
+<linearGradient id="paint0_linear_303_614" x1="48.4373" y1="83.8162" x2="111.888" y2="65.7367" gradientUnits="userSpaceOnUse">
69
+<stop stop-color="#DD4652"/>
70
+<stop offset="0.15" stop-color="#DD4854"/>
71
+<stop offset="0.23" stop-color="#DE4F5B"/>
72
+<stop offset="0.29" stop-color="#E15C67"/>
73
+<stop offset="0.34" stop-color="#E46F78"/>
74
+<stop offset="0.38" stop-color="#E9878F"/>
75
+<stop offset="0.42" stop-color="#EEA5AB"/>
76
+<stop offset="0.46" stop-color="#F5C8CC"/>
77
+<stop offset="0.49" stop-color="#FCF0F1"/>
78
+<stop offset="0.5" stop-color="#FEFDFD"/>
79
+<stop offset="0.52" stop-color="#F7D4D8"/>
80
+<stop offset="0.55" stop-color="#F1B0B7"/>
81
+<stop offset="0.58" stop-color="#EB8F9A"/>
82
+<stop offset="0.61" stop-color="#E67481"/>
83
+<stop offset="0.64" stop-color="#E25E6D"/>
84
+<stop offset="0.68" stop-color="#DF4D5E"/>
85
+<stop offset="0.73" stop-color="#DD4153"/>
86
+<stop offset="0.8" stop-color="#DC3A4D"/>
87
+<stop offset="1" stop-color="#DC394C"/>
88
+</linearGradient>
89
+<linearGradient id="paint1_linear_303_614" x1="107.569" y1="12.2053" x2="11.6611" y2="39.2106" gradientUnits="userSpaceOnUse">
90
+<stop stop-color="#8B3138" stop-opacity="0"/>
91
+<stop offset="0.65" stop-color="#DC394C"/>
92
+</linearGradient>
93
+<linearGradient id="paint2_linear_303_614" x1="107.569" y1="189.634" x2="11.6864" y2="162.629" gradientUnits="userSpaceOnUse">
94
+<stop stop-color="#8B3138" stop-opacity="0"/>
95
+<stop offset="0.65" stop-color="#DC394C"/>
96
+</linearGradient>
97
+<linearGradient id="paint3_linear_303_614" x1="30.5598" y1="131.944" x2="125.396" y2="131.944" gradientUnits="userSpaceOnUse">
98
+<stop stop-color="#8B3138" stop-opacity="0"/>
99
+<stop offset="0.65" stop-color="#DC394C"/>
100
+</linearGradient>
101
+<linearGradient id="paint4_linear_303_614" x1="30.5847" y1="69.8189" x2="125.396" y2="69.8189" gradientUnits="userSpaceOnUse">
102
+<stop stop-color="#8B3138" stop-opacity="0"/>
103
+<stop offset="0.65" stop-color="#DC394C"/>
104
+</linearGradient>
105
+<linearGradient id="paint5_linear_303_614" x1="-48.1491" y1="140.528" x2="123.546" y2="139.131" gradientUnits="userSpaceOnUse">
106
+<stop stop-color="#6A1921"/>
107
+<stop offset="1" stop-color="#DC394C"/>
108
+</linearGradient>
109
+</defs>
110
+</svg>
--- a/static/img/fossilrepo-logo-dark.svg
+++ b/static/img/fossilrepo-logo-dark.svg
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/static/img/fossilrepo-logo-dark.svg
+++ b/static/img/fossilrepo-logo-dark.svg
@@ -0,0 +1,110 @@
1 <svg width="1033" height="213" viewBox="0 0 1033 213" fill="none" xmlns="http://www.w3.org/2000/svg">
2 <g filter="url(#filter0_d_303_614)">
3 <rect x="4" width="1025" height="205" fill="#2B2D2C"/>
4 <path d="M491.56 167H500.064V163.116H495.59V132.884H500.138V129H491.56V167Z" fill="#DB394C"/>
5 <path d="M506.018 129V132.884H510.529V163.116H505.981V167H514.56V129H506.018Z" fill="#DB394C"/>
6 <path d="M505.093 139.932H501.026V155.788H505.093V139.932Z" fill="#DB394C"/>
7 <path d="M148.624 101.062C168.554 83.5327 166.323 44.969 144.11 30.1725C134.17 22.9805 119.767 19.0365 107.519 19.1396C101.434 19.6293 95.7028 22.4134 90.7074 26.048C84.6217 30.5592 79.7024 35.9209 72.9573 40.2774C65.1219 44.969 54.1422 50.4597 44.0754 49.2223C37.3303 47.3921 37.1782 40.8703 37.4825 34.9929C37.4825 34.9414 37.4825 34.8641 37.4825 34.8125C37.2796 34.8125 37.1275 34.8641 36.9246 34.8641C34.4903 34.8641 32.4871 34.1681 30.8896 33.0854C30.8135 33.7041 30.7374 34.3485 30.7374 34.9929C29.6724 46.129 34.2367 55.873 46.2814 56.3628C61.9776 56.5175 77.0145 46.7734 88.0956 36.9005C97.5793 27.9556 106.125 23.3156 119.006 26.7956C132.522 29.1929 144.871 36.1014 151.311 48.6294C160.034 65.849 155.216 92.9158 136.477 101.01C155.216 109.079 160.034 136.171 151.311 153.391C144.871 165.919 132.395 172.518 118.879 174.915C105.947 178.395 97.63 174.09 88.0956 165.12C77.0145 155.247 61.9523 145.503 46.2814 145.657C34.2367 146.199 29.6724 155.865 30.7374 167.053C30.7374 173.523 34.5917 179.117 40.0943 181.54C40.0182 180.354 40.0943 179.194 40.3986 178.112C40.7536 176.849 41.2861 175.766 41.9454 174.812C39.2575 173.265 37.4317 170.378 37.4317 167.053C37.1275 161.201 37.2542 154.654 44.0246 152.824C54.0661 151.586 65.0459 157.077 72.9066 161.768C79.6517 166.125 84.571 171.513 90.6567 175.998C95.6521 179.633 101.383 182.417 107.469 182.906C119.716 183.009 134.119 179.065 144.059 171.873C166.272 157.103 168.529 118.539 148.573 100.984L148.624 101.062Z" fill="#DC394C"/>
8 <path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="#DC394C"/>
9 <g opacity="0.2">
10 <path d="M114.366 59.0179C120.604 62.6268 119.59 76.882 114.848 80.1815C114.62 80.362 114.341 80.4909 114.087 80.6455C115.685 82.3727 116.724 84.5638 117.206 86.8322C122.43 83.5069 125.523 77.81 125.397 69.0713C125.955 44.8143 104.35 49.4801 87.7157 49.2739C86.3464 49.2739 84.9771 49.6348 83.8107 50.4081C80.6157 52.5477 76.9135 54.3521 73.2114 56.0535C82.7711 56.9557 106.227 53.5015 114.341 59.0179H114.366Z" fill="url(#paint0_linear_303_614)"/>
11 </g>
12 <path d="M37.7352 41.4374C37.4056 39.3236 37.4563 37.0294 37.5577 34.8382C37.5577 29.8889 41.5135 25.8676 46.3821 25.8676H79.5241C80.158 25.8676 80.7412 25.6356 81.223 25.2231C83.6827 23.1093 86.8523 20.7893 89.8445 19.0364H46.4074C37.8367 19.0364 30.8634 26.1253 30.8634 34.8382C30.483 38.7823 30.8381 42.5716 31.9538 45.8196C33.3991 43.7574 35.4277 42.1076 37.7606 41.4632L37.7352 41.4374Z" fill="url(#paint1_linear_303_614)"/>
13 <path d="M37.7352 160.402C37.4056 162.516 37.4563 164.81 37.5577 167.001C37.5577 171.951 41.5135 175.972 46.3821 175.972H79.5241C80.158 175.972 80.7412 176.204 81.223 176.617C83.6827 178.73 86.8523 181.05 89.8445 182.803H46.4074C37.8367 182.803 30.8634 175.714 30.8634 167.001C30.483 163.057 30.8381 159.268 31.9538 156.02C33.3991 158.082 35.4277 159.732 37.7606 160.376L37.7352 160.402Z" fill="url(#paint2_linear_303_614)"/>
14 <path d="M102.042 111.167C102.042 111.167 35.8849 111.167 30.5852 111.167V117.998H102.042C107.494 117.998 111.779 119.183 114.848 121.555C119.615 124.88 120.604 139.11 114.366 142.719C106.48 148.209 82.0859 144.781 72.8051 145.683C76.5833 147.436 80.3616 149.292 83.6327 151.483C84.5962 152.128 85.712 152.488 86.853 152.463C103.437 152.179 125.929 157.129 125.396 132.665C125.599 117.276 115.938 111.321 102.042 111.167Z" fill="url(#paint3_linear_303_614)"/>
15 <path d="M87.7147 49.2997C86.3454 49.2997 84.9761 49.6606 83.8097 50.4339C80.6146 52.5735 76.9125 54.3779 73.2103 56.0793C82.77 56.9815 106.226 53.5273 114.34 59.0437C120.578 62.6526 119.563 76.9078 114.822 80.2074C111.753 82.5531 107.468 83.7647 102.016 83.7647H30.5593V90.5958H102.016C115.912 90.4669 125.573 84.4865 125.37 69.0971C125.928 44.8401 104.324 49.5059 87.6893 49.2997H87.7147Z" fill="url(#paint4_linear_303_614)"/>
16 <path d="M121.263 126.427C120.198 126.608 119.133 126.814 118.068 127.072C119.539 132.614 118.702 140.218 114.391 142.744C106.505 148.235 82.1114 144.807 72.8306 145.709C76.6088 147.462 80.3871 149.318 83.6582 151.509C84.6217 152.153 85.7375 152.514 86.8785 152.489C103.462 152.205 125.954 157.154 125.422 132.691C125.447 130.062 125.168 127.768 124.661 125.654C123.596 126.04 122.48 126.298 121.288 126.427H121.263Z" fill="url(#paint5_linear_303_614)"/>
17 <g filter="url(#filter1_d_303_614)">
18 <rect x="195.06" y="32.5" width="803.841" height="76.1916" stroke="#2B2D2C" shape-rendering="crispEdges"/>
19 <path fill-rule="evenodd" clip-rule="evenodd" d="M549.696 108.274V32.9176H582.786C588.268 32.9176 593.264 33.9167 597.661 36.0512C602.078 38.1951 605.594 41.3351 608.098 45.4472C610.637 49.6163 611.813 54.4753 611.813 59.8467C611.813 65.2685 610.6 70.1409 607.941 74.2414C606.304 76.7879 604.248 78.9243 601.812 80.6522L616.857 108.274H587.652L576.157 86.6632V108.274H549.696ZM590.127 104.154H609.921L596.291 79.1301C597.57 78.5163 598.748 77.8075 599.824 77.0036C601.677 75.6192 603.23 73.9531 604.481 72.005C606.622 68.706 607.693 64.6532 607.693 59.8467C607.693 55.0838 606.655 50.9983 604.579 47.59C602.504 44.1817 599.598 41.5709 595.862 39.7576C592.126 37.9442 587.768 37.0375 582.786 37.0375H553.816V104.154H572.037V82.0004H578.343L590.127 104.154ZM572.037 67.843H578.46C580.601 67.843 582.426 67.5808 583.933 67.0565C585.463 66.5103 586.631 65.6473 587.44 64.4675C588.27 63.2877 588.685 61.7474 588.685 59.8467C588.685 57.9241 588.27 56.3619 587.44 55.1603C586.631 53.9368 585.463 53.0411 583.933 52.473C582.426 51.8831 580.601 51.5882 578.46 51.5882H572.037V67.843ZM576.157 55.7081V63.7231H578.46C580.277 63.7231 581.612 63.499 582.563 63.171C583.38 62.8765 583.79 62.5051 584.041 62.1388L584.056 62.1176L584.071 62.0965C584.278 61.8011 584.565 61.1655 584.565 59.8467C584.565 58.4992 584.275 57.8278 584.05 57.5022L584.026 57.467L584.002 57.4314C583.73 57.019 583.305 56.6347 582.499 56.3351L582.465 56.3227L582.432 56.3096C581.53 55.9566 580.244 55.7081 578.46 55.7081H576.157Z" fill="#FAFAFA"/>
20 <path fill-rule="evenodd" clip-rule="evenodd" d="M475.677 108.274V32.9176H532.288V55.8391H502.138V59.1351H529.797V82.0566H502.138V85.3525H532.157V108.274H475.677ZM498.018 89.4724V77.9367H525.677V63.2549H498.018V51.7193H528.168V37.0375H479.797V104.154H528.037V89.4724H498.018Z" fill="#FAFAFA"/>
21 <path fill-rule="evenodd" clip-rule="evenodd" d="M406.344 108.274V32.9176H432.805V85.3525H459.94V108.274H406.344ZM428.685 89.4724V37.0375H410.464V104.154H455.82V89.4724H428.685Z" fill="#FAFAFA"/>
22 <path fill-rule="evenodd" clip-rule="evenodd" d="M389.337 32.9176V108.274H362.876V32.9176H389.337ZM385.217 37.0375H366.996V104.154H385.217V37.0375Z" fill="#FAFAFA"/>
23 <path fill-rule="evenodd" clip-rule="evenodd" d="M341.181 91.5344L341.177 91.5409C337.907 97.2938 333.433 101.742 327.775 104.771L327.768 104.775L327.762 104.778C322.179 107.742 316.001 109.192 309.313 109.192C302.581 109.192 296.376 107.731 290.786 104.738L290.776 104.733L290.767 104.728C285.138 101.676 280.682 97.2211 277.416 91.4754L277.411 91.4663L277.406 91.4573C274.097 85.5658 272.552 78.5492 272.552 70.5958C272.552 62.6044 274.095 55.5718 277.41 49.6951L277.413 49.69C280.679 43.9181 285.143 39.4637 290.789 36.4517C296.378 33.46 302.582 32 309.313 32C316.004 32 322.185 33.4617 327.769 36.4501C333.435 39.4604 337.911 43.9139 341.18 49.6886C344.519 55.5668 346.074 62.602 346.074 70.5958C346.074 78.5928 344.518 85.6372 341.181 91.5344ZM325.83 40.0853C320.892 37.4417 315.387 36.1199 309.313 36.1199C303.196 36.1199 297.668 37.4417 292.731 40.0853C287.815 42.707 283.904 46.585 280.998 51.7193C278.114 56.8317 276.672 63.1239 276.672 70.5958C276.672 78.0241 278.114 84.3054 280.998 89.4396C283.904 94.552 287.815 98.4409 292.731 101.106C297.668 103.75 303.196 105.072 309.313 105.072C315.387 105.072 320.892 103.761 325.83 101.139C330.768 98.4956 334.689 94.6176 337.595 89.5052C340.501 84.3709 341.954 78.0678 341.954 70.5958C341.954 63.1239 340.501 56.8317 337.595 51.7193C334.689 46.585 330.768 42.707 325.83 40.0853ZM317.756 61.9115L317.749 61.8931C316.943 59.7622 315.851 58.4077 314.59 57.5445L314.569 57.5305L314.549 57.5161C313.319 56.6524 311.655 56.1013 309.313 56.1013C306.973 56.1013 305.289 56.6519 304.028 57.5279L304.02 57.533C302.77 58.3979 301.668 59.7627 300.838 61.9119C300.021 64.0587 299.538 66.9113 299.538 70.5958C299.538 74.2732 300.019 77.1389 300.838 79.3129C301.662 81.4232 302.759 82.7991 304.024 83.6938C305.285 84.5503 306.973 85.0903 309.313 85.0903C311.665 85.0903 313.337 84.545 314.569 83.6941C315.854 82.8 316.95 81.4286 317.753 79.3212L317.756 79.313L317.759 79.3048C318.595 77.1376 319.088 74.2759 319.088 70.5958C319.088 66.9141 318.594 64.0686 317.763 61.9298L317.756 61.9115ZM316.916 87.08C314.862 88.5002 312.328 89.2102 309.313 89.2102C306.298 89.2102 303.753 88.5002 301.677 87.08C299.624 85.6381 298.061 83.5407 296.991 80.7879C295.942 78.0132 295.418 74.6158 295.418 70.5958C295.418 66.5758 295.942 63.1894 296.991 60.4366C298.061 57.6619 299.624 55.5645 301.677 54.1444C303.753 52.7024 306.298 51.9814 309.313 51.9814C312.328 51.9814 314.862 52.7024 316.916 54.1444C318.992 55.5645 320.554 57.6619 321.602 60.4366C322.673 63.1894 323.208 66.5758 323.208 70.5958C323.208 74.6158 322.673 78.0132 321.602 80.7879C320.554 83.5407 318.992 85.6381 316.916 87.08Z" fill="#FAFAFA"/>
24 <path fill-rule="evenodd" clip-rule="evenodd" d="M194.56 108.274V32.9176H227.781C233.232 32.9176 238.075 33.6472 242.173 35.2702C246.242 36.8731 249.59 39.253 251.916 42.5486C254.255 45.8309 255.365 49.6469 255.365 53.8167C255.365 56.9112 254.675 59.8338 253.244 62.5031C252.104 64.6623 250.581 66.5265 248.711 68.0855C251.447 69.7483 253.658 71.9966 255.309 74.7798C257.146 77.8531 257.987 81.3596 257.987 85.1465C257.987 89.576 256.814 93.6497 254.409 97.2336C252.037 100.802 248.706 103.516 244.595 105.431C240.412 107.379 235.595 108.274 230.271 108.274H194.56ZM244.445 66.1032C244.662 65.967 244.875 65.8259 245.084 65.68C247.029 64.3036 248.536 62.5995 249.607 60.5676C250.699 58.5358 251.245 56.2855 251.245 53.8167C251.245 50.4084 250.35 47.448 248.558 44.9355C246.788 42.423 244.156 40.4785 240.66 39.1021C237.186 37.7257 232.893 37.0375 227.781 37.0375H198.679V104.154H230.271C235.143 104.154 239.338 103.335 242.856 101.696C246.373 100.058 249.082 97.8073 250.983 94.9453C252.906 92.0832 253.867 88.8169 253.867 85.1465C253.867 81.9786 253.168 79.2257 251.77 76.888C250.371 74.5284 248.493 72.6823 246.133 71.3496C245.682 71.0907 245.222 70.8546 244.752 70.6413C242.8 69.7553 240.683 69.2613 238.401 69.1594C238.357 69.1574 238.312 69.1556 238.268 69.1539V68.4984C238.276 68.4967 238.284 68.4951 238.293 68.4934C240.602 68.0248 242.653 67.2281 244.445 66.1032ZM228.809 80.0833L228.797 80.0783C228.133 79.7907 227.138 79.5659 225.683 79.5659H221.02V85.4836H225.421C228.065 85.4836 229.319 84.9764 229.832 84.6027C230.143 84.3635 230.477 83.9794 230.477 82.7869C230.477 81.8557 230.271 81.3515 230.077 81.0582C229.849 80.7117 229.491 80.3743 228.82 80.0881L228.809 80.0833ZM232.303 87.8993C230.774 89.0354 228.48 89.6035 225.421 89.6035H216.901V75.446H225.683C227.54 75.446 229.124 75.7301 230.435 76.2981C231.768 76.8662 232.795 77.6964 233.516 78.7888C234.237 79.8812 234.597 81.2139 234.597 82.7869C234.597 85.0373 233.833 86.7414 232.303 87.8993ZM228.114 58.8396L228.119 58.8327C228.228 58.6826 228.38 58.3975 228.38 57.7493C228.38 56.7949 228.117 56.5269 227.831 56.3166L227.815 56.305L227.799 56.2933C227.174 55.8244 226.219 55.4459 224.635 55.4459H221.02V60.0527H224.372C225.538 60.0527 226.399 59.869 227.035 59.6078C227.65 59.3551 227.943 59.0772 228.109 58.8465L228.114 58.8396ZM224.372 64.1726C225.967 64.1726 227.377 63.9213 228.6 63.4188C229.824 62.9163 230.774 62.1953 231.451 61.2559C232.15 60.2946 232.5 59.1257 232.5 57.7493C232.5 55.6737 231.757 54.0898 230.271 52.9974C228.786 51.8831 226.907 51.326 224.635 51.326H216.901V64.1726H224.372Z" fill="#FAFAFA"/>
25 <path d="M979.577 55.6468C979.388 53.2869 978.503 51.4461 976.922 50.1245C975.365 48.8029 972.993 48.1421 969.807 48.1421C967.777 48.1421 966.114 48.3899 964.816 48.8855C963.541 49.3575 962.597 50.0065 961.984 50.8325C961.37 51.6585 961.051 52.6025 961.028 53.6645C960.981 54.5377 961.134 55.3282 961.488 56.0362C961.866 56.7206 962.456 57.346 963.258 57.9124C964.06 58.4552 965.087 58.9508 966.338 59.3992C967.589 59.8476 969.075 60.2488 970.798 60.6028L976.745 61.8771C980.757 62.7267 984.191 63.8477 987.046 65.2401C989.902 66.6325 992.238 68.2726 994.056 70.1606C995.873 72.025 997.206 74.1253 998.056 76.4617C998.929 78.7981 999.377 81.3468 999.401 84.108C999.377 88.8751 998.185 92.9107 995.826 96.2146C993.466 99.5185 990.091 102.032 985.701 103.755C981.335 105.477 976.084 106.339 969.948 106.339C963.647 106.339 958.149 105.407 953.452 103.542C948.78 101.678 945.145 98.8106 942.549 94.9402C939.977 91.0463 938.679 86.0668 938.655 80.0017H957.346C957.464 82.22 958.019 84.0844 959.01 85.5948C960.001 87.1051 961.394 88.2497 963.187 89.0285C965.004 89.8073 967.164 90.1967 969.665 90.1967C971.766 90.1967 973.524 89.9371 974.94 89.4179C976.356 88.8987 977.43 88.1789 978.161 87.2585C978.893 86.3382 979.27 85.288 979.294 84.108C979.27 82.9988 978.905 82.0312 978.197 81.2052C977.512 80.3557 976.379 79.6005 974.798 78.9397C973.217 78.2553 971.081 77.6181 968.391 77.0281L961.169 75.4705C954.75 74.0781 949.688 71.7536 945.983 68.4968C942.302 65.2165 940.473 60.7444 940.496 55.0804C940.473 50.4785 941.7 46.4548 944.178 43.0092C946.679 39.5401 950.137 36.8379 954.55 34.9028C958.986 32.9676 964.072 32 969.807 32C975.66 32 980.722 32.9794 984.993 34.9382C989.265 36.8969 992.557 39.6581 994.87 43.2216C997.206 46.7616 998.386 50.9033 998.41 55.6468H979.577Z" fill="#FAFAFA"/>
26 <path d="M870.211 105.489V32.9912H889.893V62.3019H890.884L912.69 32.9912H935.629L911.132 65.2755L936.195 105.489H912.69L896.406 78.3025L889.893 86.7983V105.489H870.211Z" fill="#FAFAFA"/>
27 <path d="M803.624 105.489V32.9912H834.917C840.298 32.9912 845.006 33.9706 849.042 35.9293C853.077 37.8881 856.216 40.7083 858.458 44.3898C860.7 48.0714 861.821 52.4845 861.821 57.6292C861.821 62.8211 860.664 67.1988 858.352 70.7624C856.063 74.3259 852.841 77.0163 848.688 78.8335C844.558 80.6506 839.732 81.5592 834.209 81.5592H815.518V66.2667H830.245C832.557 66.2667 834.528 65.9835 836.156 65.4171C837.808 64.8271 839.071 63.8949 839.944 62.6205C840.841 61.3461 841.289 59.6824 841.289 57.6292C841.289 55.5524 840.841 53.8651 839.944 52.5671C839.071 51.2455 837.808 50.2779 836.156 49.6643C834.528 49.0271 832.557 48.7085 830.245 48.7085H823.306V105.489H803.624ZM846.103 72.2138L864.228 105.489H842.847L825.147 72.2138H846.103Z" fill="#FAFAFA"/>
28 <path d="M794.518 69.2402C794.518 77.3113 792.948 84.1198 789.81 89.6657C786.671 95.188 782.435 99.377 777.101 102.233C771.768 105.064 765.82 106.48 759.26 106.48C752.652 106.48 746.681 105.053 741.348 102.197C736.038 99.318 731.813 95.1172 728.675 89.5949C725.56 84.049 724.002 77.2641 724.002 69.2402C724.002 61.1691 725.56 54.3725 728.675 48.8501C731.813 43.3042 736.038 39.1153 741.348 36.2833C746.681 33.4278 752.652 32 759.26 32C765.82 32 771.768 33.4278 777.101 36.2833C782.435 39.1153 786.671 43.3042 789.81 48.8501C792.948 54.3725 794.518 61.1691 794.518 69.2402ZM774.269 69.2402C774.269 64.8979 773.691 61.2399 772.535 58.2664C771.402 55.2692 769.714 53.0037 767.472 51.4697C765.254 49.9121 762.517 49.1333 759.26 49.1333C756.003 49.1333 753.254 49.9121 751.012 51.4697C748.793 53.0037 747.106 55.2692 745.95 58.2664C744.817 61.2399 744.25 64.8979 744.25 69.2402C744.25 73.5826 744.817 77.2523 745.95 80.2495C747.106 83.223 748.793 85.4886 751.012 87.0461C753.254 88.5801 756.003 89.3471 759.26 89.3471C762.517 89.3471 765.254 88.5801 767.472 87.0461C769.714 85.4886 771.402 83.223 772.535 80.2495C773.691 77.2523 774.269 73.5826 774.269 69.2402Z" fill="#FAFAFA"/>
29 <path d="M638.238 105.489L616.857 32.9912H638.804L648.716 77.5945H649.283L661.035 32.9912H678.31L690.063 77.7361H690.629L700.541 32.9912H722.489L701.107 105.489H682.275L669.956 64.9923H669.389L657.07 105.489H638.238Z" fill="#FAFAFA"/>
30 </g>
31 <path d="M649.525 133.05H642.505V164.911H638.725V133.05H631.704V129.81H649.525V133.05Z" fill="#FAFAFA"/>
32 <path d="M622.242 155.191H626.022V158.161C626.022 160.501 625.302 162.355 623.862 163.723C622.422 165.055 620.19 165.721 617.165 165.721C614.141 165.721 611.891 165.055 610.415 163.723C608.939 162.355 608.201 160.501 608.201 158.161V136.56C608.201 134.22 608.921 132.384 610.361 131.052C611.801 129.684 614.033 129 617.057 129C620.082 129 622.332 129.684 623.808 131.052C625.284 132.384 626.022 134.22 626.022 136.56V141.69H622.242V136.56C622.242 135.192 621.81 134.13 620.946 133.374C620.117 132.618 618.821 132.24 617.057 132.24C615.329 132.24 614.051 132.618 613.223 133.374C612.395 134.13 611.981 135.192 611.981 136.56V158.161C611.981 159.493 612.395 160.555 613.223 161.347C614.087 162.103 615.401 162.481 617.165 162.481C618.893 162.481 620.171 162.103 621 161.347C621.828 160.555 622.242 159.493 622.242 158.161V155.191Z" fill="#FAFAFA"/>
33 <path d="M594.456 164.911V129.81H598.236V164.911H594.456Z" fill="#FAFAFA"/>
34 <path d="M551.898 164.911V129.81H566.749V133.05H555.678V145.471H564.589V148.711H555.678V164.911H551.898ZM574.309 129.81H578.089V161.671H587.971V164.911H574.309V129.81Z" fill="#FAFAFA"/>
35 <path d="M521.839 164.911V129.81H525.457L537.337 156.811V129.81H541.117V164.911H537.499L525.619 137.91V164.911H521.839Z" fill="#FAFAFA"/>
36 <path d="M481.068 155.191H484.848V158.161C484.848 160.501 484.128 162.355 482.688 163.723C481.248 165.055 479.015 165.721 475.991 165.721C472.967 165.721 470.717 165.055 469.241 163.723C467.765 162.355 467.027 160.501 467.027 158.161V136.56C467.027 134.22 467.747 132.384 469.187 131.052C470.627 129.684 472.859 129 475.883 129C478.907 129 481.158 129.684 482.634 131.052C484.11 132.384 484.848 134.22 484.848 136.56V141.69H481.068V136.56C481.068 135.192 480.635 134.13 479.771 133.374C478.943 132.618 477.647 132.24 475.883 132.24C474.155 132.24 472.877 132.618 472.049 133.374C471.221 134.13 470.807 135.192 470.807 136.56V158.161C470.807 159.493 471.221 160.555 472.049 161.347C472.913 162.103 474.227 162.481 475.991 162.481C477.719 162.481 478.997 162.103 479.825 161.347C480.653 160.555 481.068 159.493 481.068 158.161V155.191Z" fill="#FAFAFA"/>
37 <path d="M435.94 164.911V152.329L427.03 129.81H431.026L437.83 147.901L444.635 129.81H448.631L439.72 152.383V164.911H435.94Z" fill="#FAFAFA"/>
38 <path d="M411.97 129.81C414.85 129.81 416.992 130.494 418.396 131.862C419.836 133.194 420.556 135.03 420.556 137.37V140.61C420.556 143.67 419.278 145.633 416.722 146.497C419.638 147.289 421.096 149.287 421.096 152.491V157.351C421.096 159.691 420.394 161.545 418.99 162.913C417.586 164.245 415.426 164.911 412.51 164.911H403.816V129.81H411.97ZM416.776 137.37C416.776 136.038 416.362 134.994 415.534 134.238C414.742 133.446 413.554 133.05 411.97 133.05H407.596V145.201H411.97C413.59 145.201 414.796 144.768 415.588 143.904C416.38 143.04 416.776 141.942 416.776 140.61V137.37ZM417.316 152.491C417.316 151.159 416.902 150.061 416.074 149.197C415.282 148.333 414.094 147.901 412.51 147.901H407.596V161.671H412.51C414.13 161.671 415.336 161.293 416.128 160.537C416.92 159.745 417.316 158.683 417.316 157.351V152.491Z" fill="#FAFAFA"/>
39 <path d="M364.106 164.911V129.81H372.8C375.68 129.81 377.822 130.494 379.226 131.862C380.666 133.194 381.386 135.03 381.386 137.37V157.351C381.386 159.691 380.684 161.545 379.28 162.913C377.876 164.245 375.716 164.911 372.8 164.911H364.106ZM377.606 137.37C377.606 136.038 377.192 134.994 376.364 134.238C375.572 133.446 374.384 133.05 372.8 133.05H367.886V161.671H372.8C374.42 161.671 375.626 161.293 376.418 160.537C377.21 159.745 377.606 158.683 377.606 157.351V137.37Z" fill="#FAFAFA"/>
40 <path d="M350.335 155.191H354.115V158.161C354.115 160.501 353.395 162.355 351.955 163.723C350.515 165.055 348.283 165.721 345.259 165.721C342.235 165.721 339.985 165.055 338.509 163.723C337.033 162.355 336.295 160.501 336.295 158.161V136.56C336.295 134.22 337.015 132.384 338.455 131.052C339.895 129.684 342.127 129 345.151 129C348.175 129 350.425 129.684 351.901 131.052C353.377 132.384 354.115 134.22 354.115 136.56V148.441H340.075V158.161C340.075 159.493 340.489 160.555 341.317 161.347C342.181 162.103 343.495 162.481 345.259 162.481C346.987 162.481 348.265 162.103 349.093 161.347C349.921 160.555 350.335 159.493 350.335 158.161V155.191ZM340.075 145.2H350.335V136.56C350.335 135.192 349.903 134.13 349.039 133.374C348.211 132.618 346.915 132.24 345.151 132.24C343.423 132.24 342.145 132.618 341.317 133.374C340.489 134.13 340.075 135.192 340.075 136.56V145.2Z" fill="#FAFAFA"/>
41 <path d="M315.483 164.911V129.81H317.103L318.669 133.806C319.173 132.438 319.857 131.304 320.721 130.404C321.585 129.468 322.737 129 324.177 129C326.265 129 327.849 129.594 328.929 130.782C330.045 131.934 330.603 133.68 330.603 136.02V139.53H326.985V135.804C326.985 134.76 326.751 133.914 326.283 133.266C325.815 132.582 325.059 132.24 324.015 132.24C323.331 132.24 322.611 132.51 321.855 133.05C321.135 133.59 320.523 134.454 320.019 135.642C319.515 136.794 319.263 138.324 319.263 140.232V164.911H315.483Z" fill="#FAFAFA"/>
42 <path d="M301.713 155.191H305.493V158.161C305.493 160.501 304.773 162.355 303.333 163.723C301.893 165.055 299.661 165.721 296.636 165.721C293.612 165.721 291.362 165.055 289.886 163.723C288.41 162.355 287.672 160.501 287.672 158.161V136.56C287.672 134.22 288.392 132.384 289.832 131.052C291.272 129.684 293.504 129 296.528 129C299.553 129 301.803 129.684 303.279 131.052C304.755 132.384 305.493 134.22 305.493 136.56V148.441H291.452V158.161C291.452 159.493 291.866 160.555 292.694 161.347C293.558 162.103 294.872 162.481 296.636 162.481C298.365 162.481 299.643 162.103 300.471 161.347C301.299 160.555 301.713 159.493 301.713 158.161V155.191ZM291.452 145.2H301.713V136.56C301.713 135.192 301.281 134.13 300.417 133.374C299.589 132.618 298.293 132.24 296.528 132.24C294.8 132.24 293.522 132.618 292.694 133.374C291.866 134.13 291.452 135.192 291.452 136.56V145.2Z" fill="#FAFAFA"/>
43 <path d="M260.684 129.81H264.464V158.161C264.464 159.493 264.878 160.555 265.706 161.347C266.534 162.103 267.776 162.481 269.432 162.481C271.124 162.481 272.384 162.103 273.212 161.347C274.04 160.555 274.454 159.493 274.454 158.161V129.81H278.235V158.161C278.235 160.501 277.479 162.355 275.966 163.723C274.49 165.055 272.312 165.721 269.432 165.721C267.776 165.721 266.39 165.433 265.274 164.857C264.158 164.281 263.258 163.525 262.574 162.589C261.926 163.561 261.026 164.335 259.874 164.911C258.722 165.451 257.336 165.721 255.716 165.721C252.836 165.721 250.64 165.055 249.128 163.723C247.652 162.355 246.914 160.501 246.914 158.161V129.81H250.694V158.161C250.694 159.493 251.108 160.555 251.936 161.347C252.8 162.103 254.06 162.481 255.716 162.481C257.408 162.481 258.65 162.103 259.442 161.347C260.27 160.555 260.684 159.493 260.684 158.161V129.81Z" fill="#FAFAFA"/>
44 <path d="M237.476 158.161C237.476 160.501 236.72 162.355 235.208 163.723C233.732 165.055 231.446 165.721 228.35 165.721C225.254 165.721 222.932 165.055 221.383 163.723C219.871 162.355 219.115 160.501 219.115 158.161V136.56C219.115 134.22 219.853 132.384 221.329 131.052C222.842 129.684 225.146 129 228.242 129C231.338 129 233.642 129.684 235.154 131.052C236.702 132.384 237.476 134.22 237.476 136.56V158.161ZM233.696 136.56C233.696 135.192 233.246 134.13 232.346 133.374C231.446 132.618 230.078 132.24 228.242 132.24C226.442 132.24 225.092 132.618 224.192 133.374C223.328 134.13 222.896 135.192 222.896 136.56V158.161C222.896 159.493 223.346 160.555 224.246 161.347C225.146 162.103 226.514 162.481 228.35 162.481C230.15 162.481 231.482 162.103 232.346 161.347C233.246 160.555 233.696 159.493 233.696 158.161V136.56Z" fill="#FAFAFA"/>
45 <path d="M194.56 164.911V129.81H202.714C205.594 129.81 207.736 130.494 209.14 131.862C210.58 133.194 211.3 135.03 211.3 137.37V146.551C211.3 148.891 210.598 150.745 209.194 152.113C207.79 153.445 205.63 154.111 202.714 154.111H198.34V164.911H194.56ZM207.52 137.37C207.52 136.038 207.106 134.994 206.278 134.238C205.486 133.446 204.298 133.05 202.714 133.05H198.34V150.871H202.714C204.334 150.871 205.54 150.493 206.332 149.737C207.124 148.945 207.52 147.883 207.52 146.551V137.37Z" fill="#FAFAFA"/>
46 </g>
47 <defs>
48 <filter id="filter0_d_303_614" x="0" y="0" width="1033" height="213" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
49 <feFlood flood-opacity="0" result="BackgroundImageFix"/>
50 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
51 <feOffset dy="4"/>
52 <feGaussianBlur stdDeviation="2"/>
53 <feComposite in2="hardAlpha" operator="out"/>
54 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
55 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
56 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
57 </filter>
58 <filter id="filter1_d_303_614" x="190.56" y="32" width="812.841" height="85.1917" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
59 <feFlood flood-opacity="0" result="BackgroundImageFix"/>
60 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
61 <feOffset dy="4"/>
62 <feGaussianBlur stdDeviation="2"/>
63 <feComposite in2="hardAlpha" operator="out"/>
64 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
65 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_303_614"/>
66 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_303_614" result="shape"/>
67 </filter>
68 <linearGradient id="paint0_linear_303_614" x1="48.4373" y1="83.8162" x2="111.888" y2="65.7367" gradientUnits="userSpaceOnUse">
69 <stop stop-color="#DD4652"/>
70 <stop offset="0.15" stop-color="#DD4854"/>
71 <stop offset="0.23" stop-color="#DE4F5B"/>
72 <stop offset="0.29" stop-color="#E15C67"/>
73 <stop offset="0.34" stop-color="#E46F78"/>
74 <stop offset="0.38" stop-color="#E9878F"/>
75 <stop offset="0.42" stop-color="#EEA5AB"/>
76 <stop offset="0.46" stop-color="#F5C8CC"/>
77 <stop offset="0.49" stop-color="#FCF0F1"/>
78 <stop offset="0.5" stop-color="#FEFDFD"/>
79 <stop offset="0.52" stop-color="#F7D4D8"/>
80 <stop offset="0.55" stop-color="#F1B0B7"/>
81 <stop offset="0.58" stop-color="#EB8F9A"/>
82 <stop offset="0.61" stop-color="#E67481"/>
83 <stop offset="0.64" stop-color="#E25E6D"/>
84 <stop offset="0.68" stop-color="#DF4D5E"/>
85 <stop offset="0.73" stop-color="#DD4153"/>
86 <stop offset="0.8" stop-color="#DC3A4D"/>
87 <stop offset="1" stop-color="#DC394C"/>
88 </linearGradient>
89 <linearGradient id="paint1_linear_303_614" x1="107.569" y1="12.2053" x2="11.6611" y2="39.2106" gradientUnits="userSpaceOnUse">
90 <stop stop-color="#8B3138" stop-opacity="0"/>
91 <stop offset="0.65" stop-color="#DC394C"/>
92 </linearGradient>
93 <linearGradient id="paint2_linear_303_614" x1="107.569" y1="189.634" x2="11.6864" y2="162.629" gradientUnits="userSpaceOnUse">
94 <stop stop-color="#8B3138" stop-opacity="0"/>
95 <stop offset="0.65" stop-color="#DC394C"/>
96 </linearGradient>
97 <linearGradient id="paint3_linear_303_614" x1="30.5598" y1="131.944" x2="125.396" y2="131.944" gradientUnits="userSpaceOnUse">
98 <stop stop-color="#8B3138" stop-opacity="0"/>
99 <stop offset="0.65" stop-color="#DC394C"/>
100 </linearGradient>
101 <linearGradient id="paint4_linear_303_614" x1="30.5847" y1="69.8189" x2="125.396" y2="69.8189" gradientUnits="userSpaceOnUse">
102 <stop stop-color="#8B3138" stop-opacity="0"/>
103 <stop offset="0.65" stop-color="#DC394C"/>
104 </linearGradient>
105 <linearGradient id="paint5_linear_303_614" x1="-48.1491" y1="140.528" x2="123.546" y2="139.131" gradientUnits="userSpaceOnUse">
106 <stop stop-color="#6A1921"/>
107 <stop offset="1" stop-color="#DC394C"/>
108 </linearGradient>
109 </defs>
110 </svg>
--- a/templates/admin/base_site.html
+++ b/templates/admin/base_site.html
@@ -0,0 +1,44 @@
1
+{% extends "admin/base.html" %}
2
+{% load static %}
3
+
4
+{% block title %}
5
+ {% if subtitle %} {{ subtitle }} | {% endif %}
6
+ {{ title }} | {{ site_title|default:_('Django site admin') }}
7
+{% endblock %}
8
+
9
+{% block extrastyle %}
10
+ {{ block.super }}
11
+ <link rel="stylesheet" href="{% static 'admin/css/dark_theme.css' %}">
12
+{% endblock %}
13
+
14
+{% block welcome-msg %}
15
+ <strong>{% firstof user.get_short_name user.get_username %}</strong>.
16
+{% endblock %}
17
+
18
+{% block userlinks %}
19
+ <span class="bw-links">
20
+ <a target="_blank" href="/">App</a>
21
+ <a target="_blank" href="/health/">Health</a>
22
+ <a target="_blank" href="http://localhost:8025">Mailpit</a>
23
+ </span>
24
+ {% if user.has_usable_password %}
25
+ <a href="{% url 'admin:password_change' %}">PWD</a> /
26
+ {% endif %}
27
+ <form id="logout-form" method="post" action="{% url 'admin:logout' %}" style="display:inline">
28
+ {% csrf_token %}
29
+ <button type="submit">Log out</button>
30
+ </form>
31
+ {% include "admin/color_theme_toggle.html" %}
32
+{% endblock %}
33
+
34
+{% block branding %}
35
+ <h1 id="site-name">
36
+ <div class="logo">
37
+ <a role="listitem" class="item" href="/">
38
+ <img src="{% static 'admin/img/logo-dark.svg' %}" alt="Fossilrepo" height="40">
39
+ </a>
40
+ </div>
41
+ </h1>
42
+{% endblock %}
43
+
44
+{% block nav-global %}{% endblock %}
--- a/templates/admin/base_site.html
+++ b/templates/admin/base_site.html
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/admin/base_site.html
+++ b/templates/admin/base_site.html
@@ -0,0 +1,44 @@
1 {% extends "admin/base.html" %}
2 {% load static %}
3
4 {% block title %}
5 {% if subtitle %} {{ subtitle }} | {% endif %}
6 {{ title }} | {{ site_title|default:_('Django site admin') }}
7 {% endblock %}
8
9 {% block extrastyle %}
10 {{ block.super }}
11 <link rel="stylesheet" href="{% static 'admin/css/dark_theme.css' %}">
12 {% endblock %}
13
14 {% block welcome-msg %}
15 <strong>{% firstof user.get_short_name user.get_username %}</strong>.
16 {% endblock %}
17
18 {% block userlinks %}
19 <span class="bw-links">
20 <a target="_blank" href="/">App</a>
21 <a target="_blank" href="/health/">Health</a>
22 <a target="_blank" href="http://localhost:8025">Mailpit</a>
23 </span>
24 {% if user.has_usable_password %}
25 <a href="{% url 'admin:password_change' %}">PWD</a> /
26 {% endif %}
27 <form id="logout-form" method="post" action="{% url 'admin:logout' %}" style="display:inline">
28 {% csrf_token %}
29 <button type="submit">Log out</button>
30 </form>
31 {% include "admin/color_theme_toggle.html" %}
32 {% endblock %}
33
34 {% block branding %}
35 <h1 id="site-name">
36 <div class="logo">
37 <a role="listitem" class="item" href="/">
38 <img src="{% static 'admin/img/logo-dark.svg' %}" alt="Fossilrepo" height="40">
39 </a>
40 </div>
41 </h1>
42 {% endblock %}
43
44 {% block nav-global %}{% endblock %}
--- a/templates/auth1/login.html
+++ b/templates/auth1/login.html
@@ -0,0 +1,36 @@
1
+{% extends "base.html" %}
2
+{% load static %}
3
+{% block title %}Sign In — Fossilrecontent %}
4
+<div class="flex min-h-[80vh] items-center justify-center">
5
+ <div class="w-full max-w-sm space-y-8">
6
+ <div class="flex flex-col items-center">
7
+ <img src="{% static 'img/fossilrepo-logo-dark.svss="h-12 w-auto mb-6">
8
+ <h2 class="text-center text-3xl font-bold tracking-tight text-gray-100">Sign in</h2>
9
+ <p class="mt-2 text-center text-sm text-gray-400">Fossilrepo Django + HTMX</p>
10
+ </div>
11
+
12
+ {% if form.errors %}
13
+ <div class="rounded-md bg-red-900/50 border border-red-700 p-4">
14
+ <p class="text-sm text-red-300">Invalid username or password.</p>
15
+ </div>
16
+ {% endif %}
17
+
18
+ /div>
19
+ {% endif %}
20
+
21
+ <form method="post" class="space-y-6">
22
+ {% csrf_token %}
23
+ <div>
24
+ <label for="id_username" class="block text-sm font-medium text-gray-300">Username</label>
25
+ <div class="mt-1">{{ form.username }}</div>
26
+ </div>
27
+ <div>
28
+ <label for="id_password" class="block text-sm font-medium text-gray-300">Password</label>
29
+ <div class="mt-1">{{ form.password }v>
30
+ {% endif %}
31
+ <button type="submit"
32
+ class="w-full rounded-md bg-brand px-3 py-2 text-sm font-semibold text-white shadow-sm-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-gray-950 transition-colors">
33
+ Sign in
34
+ </button>
35
+ </form>
36
+ </
--- a/templates/auth1/login.html
+++ b/templates/auth1/login.html
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/auth1/login.html
+++ b/templates/auth1/login.html
@@ -0,0 +1,36 @@
1 {% extends "base.html" %}
2 {% load static %}
3 {% block title %}Sign In — Fossilrecontent %}
4 <div class="flex min-h-[80vh] items-center justify-center">
5 <div class="w-full max-w-sm space-y-8">
6 <div class="flex flex-col items-center">
7 <img src="{% static 'img/fossilrepo-logo-dark.svss="h-12 w-auto mb-6">
8 <h2 class="text-center text-3xl font-bold tracking-tight text-gray-100">Sign in</h2>
9 <p class="mt-2 text-center text-sm text-gray-400">Fossilrepo Django + HTMX</p>
10 </div>
11
12 {% if form.errors %}
13 <div class="rounded-md bg-red-900/50 border border-red-700 p-4">
14 <p class="text-sm text-red-300">Invalid username or password.</p>
15 </div>
16 {% endif %}
17
18 /div>
19 {% endif %}
20
21 <form method="post" class="space-y-6">
22 {% csrf_token %}
23 <div>
24 <label for="id_username" class="block text-sm font-medium text-gray-300">Username</label>
25 <div class="mt-1">{{ form.username }}</div>
26 </div>
27 <div>
28 <label for="id_password" class="block text-sm font-medium text-gray-300">Password</label>
29 <div class="mt-1">{{ form.password }v>
30 {% endif %}
31 <button type="submit"
32 class="w-full rounded-md bg-brand px-3 py-2 text-sm font-semibold text-white shadow-sm-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-gray-950 transition-colors">
33 Sign in
34 </button>
35 </form>
36 </
--- a/templates/base.html
+++ b/templates/base.html
@@ -0,0 +1,71 @@
1
+<!DOCTYPE html>
2
+<html lang="en" class="h-full bg-gray-950">
3
+<head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="csrf-token" content="{{ csrf_token }}">
7
+ <title>{% block title %}Fossilrepo{% endblock %}</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ brand: {
15
+ DEFAULT: '#DC394C',
16
+ dark: '#8B3138',
17
+ hover: '#c42d3f',
18
+ light: '#e8677a',
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ </script>
25
+ <style type="text/tailwindcss">
26
+ @layer base {
27
+ input[type="text"], input[type="number"], input[type="email"],
28
+ input[type="password"], input[type="search"], input[type="url"],
29
+ textarea, select {
30
+ @apply bg-gray-800 border-gray-700 text-gray-100 rounded-md shadow-sm
31
+ focus:border-brand focus:ring-brand sm:text-sm;
32
+ }
33
+ }
34
+ </style>
35
+ <script src="https://unpkg.com/[email protected]"></script>
36
+ <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
37
+ <script>
38
+ document.body.addEventListener('htmx:configRequest', function(event) {
39
+ var token = document.querySelector('meta[name="csrf-token"]');
40
+ if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
41
+ });
42
+ </script>
43
+</head>
44
+<body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
45
+ <div class="min-h-full">
46
+ {% if user.is_authenticated %}
47
+ {% include "includes/nav.html" %}
48
+ {% endif %}
49
+
50
+ <main class="{% if user.is_authenticated %}py-6{% endif %}">
51
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
52
+ {% if messages %}
53
+ <div id="messages" class="mb-4 space-y-2">
54
+ {% for message in messages %}
55
+ <div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"
56
+ x-transition class="rounded-md p-4 {% if message.tags == 'success' %}bg-green-900/50 text-green-300 border border-green-700{% elif message.tags == 'error' %}bg-red-900/50 text-red-300 border border-red-700{% else %}bg-gray-800 text-gray-300 border border-gray-700{% endif %}">
57
+ <div class="flex justify-between">
58
+ <p class="text-sm font-medium">{{ message }}</p>
59
+ <button @click="show = false" class="ml-3 text-sm font-medium underline">&times;</button>
60
+ </div>
61
+ </div>
62
+ {% endfor %}
63
+ </div>
64
+ {% endif %}
65
+
66
+ {% block content %}{% endblock %}
67
+ </div>
68
+ </main>
69
+ </div>
70
+</body>
71
+</html>
--- a/templates/base.html
+++ b/templates/base.html
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/base.html
+++ b/templates/base.html
@@ -0,0 +1,71 @@
1 <!DOCTYPE html>
2 <html lang="en" class="h-full bg-gray-950">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="csrf-token" content="{{ csrf_token }}">
7 <title>{% block title %}Fossilrepo{% endblock %}</title>
8 <script src="https://cdn.tailwindcss.com"></script>
9 <script>
10 tailwind.config = {
11 theme: {
12 extend: {
13 colors: {
14 brand: {
15 DEFAULT: '#DC394C',
16 dark: '#8B3138',
17 hover: '#c42d3f',
18 light: '#e8677a',
19 }
20 }
21 }
22 }
23 }
24 </script>
25 <style type="text/tailwindcss">
26 @layer base {
27 input[type="text"], input[type="number"], input[type="email"],
28 input[type="password"], input[type="search"], input[type="url"],
29 textarea, select {
30 @apply bg-gray-800 border-gray-700 text-gray-100 rounded-md shadow-sm
31 focus:border-brand focus:ring-brand sm:text-sm;
32 }
33 }
34 </style>
35 <script src="https://unpkg.com/[email protected]"></script>
36 <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
37 <script>
38 document.body.addEventListener('htmx:configRequest', function(event) {
39 var token = document.querySelector('meta[name="csrf-token"]');
40 if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
41 });
42 </script>
43 </head>
44 <body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
45 <div class="min-h-full">
46 {% if user.is_authenticated %}
47 {% include "includes/nav.html" %}
48 {% endif %}
49
50 <main class="{% if user.is_authenticated %}py-6{% endif %}">
51 <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
52 {% if messages %}
53 <div id="messages" class="mb-4 space-y-2">
54 {% for message in messages %}
55 <div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"
56 x-transition class="rounded-md p-4 {% if message.tags == 'success' %}bg-green-900/50 text-green-300 border border-green-700{% elif message.tags == 'error' %}bg-red-900/50 text-red-300 border border-red-700{% else %}bg-gray-800 text-gray-300 border border-gray-700{% endif %}">
57 <div class="flex justify-between">
58 <p class="text-sm font-medium">{{ message }}</p>
59 <button @click="show = false" class="ml-3 text-sm font-medium underline">&times;</button>
60 </div>
61 </div>
62 {% endfor %}
63 </div>
64 {% endif %}
65
66 {% block content %}{% endblock %}
67 </div>
68 </main>
69 </div>
70 </body>
71 </html>
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -0,0 +1,19 @@
1
+{% extends "base.html" %}
2
+{% load static %}
3
+{% block title %}Dashboard — Fossilre>
4
+{% endblock %}
5
+
6
+{% blod:flex md:items-cenjustify-between mb-811@48,g@Lv,W:sm:grid-cols-2 lg:grid-cols-3">
7
+e@st,g@tZ,G:group rounded-lgO@B~,e:bg-gray-800 p-6 shadow-sm hover:shadow-mdV@157,6:all">
8
+K@vk,2:lgS@167,7: group-G@eW,G:">Projects</h3>
9
+H@1Cl,items.view_itemitemItem@133,d@13d,G:group rounded-lgyour item catalogsv, runbooks, and internal documentation.</p>
10
+ </a>
11
+ {% endif %}
12
+
13
+W@yS,G:organization %}
14
+J@1Ek,Y@19e,G:group rounded-lgO@B~,e:bg-gray-800 p-6 shadow-sm hover:shadow-mdV@157,6:all">
15
+K@vk,2:lgS@167,7: group-G@eW,This is the Fossilrepo Django + HTMX template. Server-rendered with progressive enhance, and configuration.</p>
16
+ </a>
17
+ {% endif %}
18
+
19
+ {% if uDocsdocu
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -0,0 +1,19 @@
1 {% extends "base.html" %}
2 {% load static %}
3 {% block title %}Dashboard — Fossilre>
4 {% endblock %}
5
6 {% blod:flex md:items-cenjustify-between mb-811@48,g@Lv,W:sm:grid-cols-2 lg:grid-cols-3">
7 e@st,g@tZ,G:group rounded-lgO@B~,e:bg-gray-800 p-6 shadow-sm hover:shadow-mdV@157,6:all">
8 K@vk,2:lgS@167,7: group-G@eW,G:">Projects</h3>
9 H@1Cl,items.view_itemitemItem@133,d@13d,G:group rounded-lgyour item catalogsv, runbooks, and internal documentation.</p>
10 </a>
11 {% endif %}
12
13 W@yS,G:organization %}
14 J@1Ek,Y@19e,G:group rounded-lgO@B~,e:bg-gray-800 p-6 shadow-sm hover:shadow-mdV@157,6:all">
15 K@vk,2:lgS@167,7: group-G@eW,This is the Fossilrepo Django + HTMX template. Server-rendered with progressive enhance, and configuration.</p>
16 </a>
17 {% endif %}
18
19 {% if uDocsdocu
--- a/templates/includes/nav.html
+++ b/templates/includes/nav.html
@@ -0,0 +1,63 @@
1
+{% load static %}
2
+<nav class="bg-gray-900 border-b border-gray-700" x-data="{ mobileOpen: false }">
3
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
4
+ <div class="flex h-16 items-center justify-between">
5
+ <div class="flex items-center">
6
+ <a href="{% url 'dashboard' %}" class="flex-shrink-0">
7
+ <img src="{% static 'img/fossilrepo-logo-dark.svg' %}" alt="Fossilrepo" class="h-8 w-auto">
8
+ </a>
9
+ <div class="hidden md:block ml-10">
10
+ <div class="flex items-baseline space-x-4">
11
+ <a href="{% url 'dashboard' %}"
12
+ class="{% if request.path == '/dashboard/' %}bg-brand text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %} rounded-md px-3 py-2 text-sm font-medium">
13
+ Dashboard
14
+ </a>
15
+ {% if perms.items.view_item %}
16
+ <a href="{% url 'items:list' %}"
17
+ class="{% if '/items/' in request.path %}bg-brand text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %} rounded-md px-3 py-2 text-sm font-medium">
18
+ Items
19
+ </a>
20
+ {% endif %}
21
+ {% if user.is_staff %}
22
+ <a href="{% url 'admin:index' %}"
23
+ class="text-gray-400 hover:bg-gray-800 hover:text-white rounded-md px-3 py-2 text-sm font-medium">
24
+ Admin
25
+ </a>
26
+ {% endif %}
27
+ </div>
28
+ </div>
29
+ </div>
30
+ <div class="hidden md:block">
31
+ <div class="ml-4 flex items-center md:ml-6" x-data="{ open: false }">
32
+ <div class="relative">
33
+ <button @click="open = !open" class="flex items-center text-sm text-gray-400 hover:text-white">
34
+ {{ user.get_full_name|default:user.username }}
35
+ <svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/></svg>
36
+ </button>
37
+ <div x-show="open" @click.outside="open = false" x-transition
38
+ class="absolute right-0 z-10 mt-2 w-48 rounded-md bg-gray-800 py-1 shadow-lg ring-1 ring-gray-700">
39
+ <form method="post" action="{% url 'auth1:logout' %}">{% csrf_token %}<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-gray-700 hover:text-white">Sign out</button></form>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <div class="md:hidden">
45
+ <button @click="mobileOpen = !mobileOpen" class="text-gray-400 hover:text-white">
46
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
47
+ <path x-show="!mobileOpen" stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
48
+ <path x-show="mobileOpen" stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
49
+ </svg>
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <div x-show="mobileOpen" class="md:hidden">
55
+ <div class="space-y-1 px-2 pb-3 pt-2">
56
+ <a href="{% url 'dashboard' %}" class="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Dashboard</a>
57
+ {% if perms.items.view_item %}
58
+ <a href="{% url 'items:list' %}" class="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Items</a>
59
+ {% endif %}
60
+ <form method="post" action="{% url 'auth1:logout' %}">{% csrf_token %}<button type="submit" class="block w-full text-left rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Sign out</button></form>
61
+ </div>
62
+ </div>
63
+</nav>
--- a/templates/includes/nav.html
+++ b/templates/includes/nav.html
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/includes/nav.html
+++ b/templates/includes/nav.html
@@ -0,0 +1,63 @@
1 {% load static %}
2 <nav class="bg-gray-900 border-b border-gray-700" x-data="{ mobileOpen: false }">
3 <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
4 <div class="flex h-16 items-center justify-between">
5 <div class="flex items-center">
6 <a href="{% url 'dashboard' %}" class="flex-shrink-0">
7 <img src="{% static 'img/fossilrepo-logo-dark.svg' %}" alt="Fossilrepo" class="h-8 w-auto">
8 </a>
9 <div class="hidden md:block ml-10">
10 <div class="flex items-baseline space-x-4">
11 <a href="{% url 'dashboard' %}"
12 class="{% if request.path == '/dashboard/' %}bg-brand text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %} rounded-md px-3 py-2 text-sm font-medium">
13 Dashboard
14 </a>
15 {% if perms.items.view_item %}
16 <a href="{% url 'items:list' %}"
17 class="{% if '/items/' in request.path %}bg-brand text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %} rounded-md px-3 py-2 text-sm font-medium">
18 Items
19 </a>
20 {% endif %}
21 {% if user.is_staff %}
22 <a href="{% url 'admin:index' %}"
23 class="text-gray-400 hover:bg-gray-800 hover:text-white rounded-md px-3 py-2 text-sm font-medium">
24 Admin
25 </a>
26 {% endif %}
27 </div>
28 </div>
29 </div>
30 <div class="hidden md:block">
31 <div class="ml-4 flex items-center md:ml-6" x-data="{ open: false }">
32 <div class="relative">
33 <button @click="open = !open" class="flex items-center text-sm text-gray-400 hover:text-white">
34 {{ user.get_full_name|default:user.username }}
35 <svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/></svg>
36 </button>
37 <div x-show="open" @click.outside="open = false" x-transition
38 class="absolute right-0 z-10 mt-2 w-48 rounded-md bg-gray-800 py-1 shadow-lg ring-1 ring-gray-700">
39 <form method="post" action="{% url 'auth1:logout' %}">{% csrf_token %}<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-gray-700 hover:text-white">Sign out</button></form>
40 </div>
41 </div>
42 </div>
43 </div>
44 <div class="md:hidden">
45 <button @click="mobileOpen = !mobileOpen" class="text-gray-400 hover:text-white">
46 <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
47 <path x-show="!mobileOpen" stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
48 <path x-show="mobileOpen" stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
49 </svg>
50 </button>
51 </div>
52 </div>
53 </div>
54 <div x-show="mobileOpen" class="md:hidden">
55 <div class="space-y-1 px-2 pb-3 pt-2">
56 <a href="{% url 'dashboard' %}" class="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Dashboard</a>
57 {% if perms.items.view_item %}
58 <a href="{% url 'items:list' %}" class="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Items</a>
59 {% endif %}
60 <form method="post" action="{% url 'auth1:logout' %}">{% csrf_token %}<button type="submit" class="block w-full text-left rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-800 hover:text-white">Sign out</button></form>
61 </div>
62 </div>
63 </nav>
--- a/templates/items/item_confirm_delete.html
+++ b/templates/items/item_confirm_delete.html
@@ -0,0 +1,28 @@
1
+{% extends "base.html" %}
2
+{% block title %}Delete {{ item.name }} — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="mb-6">
6
+ <a href="{% url 'items:detail' slug=item.slug %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to {{ item.name }}</a>
7
+</div>
8
+
9
+<div class="mx-auto max-w-lg">
10
+ <div class="rounded-lg bg-gray-800 p-6 shadow border border-gray-700">
11
+ <h2 class="text-lg font-semibold text-gray-100">Delete Item</h2>
12
+ <p class="mt-2 text-sm text-gray-400">
13
+ Are you sure you want to delete <strong class="text-gray-100">{{ item.name }}</strong>? This action uses soft delete — the record will be marked as deleted but can be recovered.
14
+ </p>
15
+ <form method="post" class="mt-6 flex justify-end gap-3">
16
+ {% csrf_token %}
17
+ <a href="{% url 'items:detail' slug=item.slug %}"
18
+ class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19
+ Cancel
20
+ </a>
21
+ <button type="submit"
22
+ class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
23
+ Delete
24
+ </button>
25
+ </form>
26
+ </div>
27
+</div>
28
+{% endblock %}
--- a/templates/items/item_confirm_delete.html
+++ b/templates/items/item_confirm_delete.html
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/items/item_confirm_delete.html
+++ b/templates/items/item_confirm_delete.html
@@ -0,0 +1,28 @@
1 {% extends "base.html" %}
2 {% block title %}Delete {{ item.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'items:detail' slug=item.slug %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to {{ item.name }}</a>
7 </div>
8
9 <div class="mx-auto max-w-lg">
10 <div class="rounded-lg bg-gray-800 p-6 shadow border border-gray-700">
11 <h2 class="text-lg font-semibold text-gray-100">Delete Item</h2>
12 <p class="mt-2 text-sm text-gray-400">
13 Are you sure you want to delete <strong class="text-gray-100">{{ item.name }}</strong>? This action uses soft delete — the record will be marked as deleted but can be recovered.
14 </p>
15 <form method="post" class="mt-6 flex justify-end gap-3">
16 {% csrf_token %}
17 <a href="{% url 'items:detail' slug=item.slug %}"
18 class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19 Cancel
20 </a>
21 <button type="submit"
22 class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
23 Delete
24 </button>
25 </form>
26 </div>
27 </div>
28 {% endblock %}
--- a/templates/items/item_detail.html
+++ b/templates/items/item_detail.html
@@ -0,0 +1,70 @@
1
+{% extends "base.html" %}
2
+{% block title %}{{ item.name }} — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="mb-6">
6
+ <a href="{% url 'items:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Items</a>
7
+</div>
8
+
9
+<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
10
+ <div class="px-6 py-5 sm:flex sm:items-center sm:justify-between">
11
+ <div>
12
+ <h1 class="text-2xl font-bold text-gray-100">{{ item.name }}</h1>
13
+ <p class="mt-1 text-sm text-gray-400">{{ item.slug }}</p>
14
+ </div>
15
+ <div class="mt-4 flex gap-3 sm:mt-0">
16
+ {% if perms.items.change_item %}
17
+ <a href="{% url 'items:update' slug=item.slug %}"
18
+ class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19
+ Edit
20
+ </a>
21
+ {% endif %}
22
+ {% if perms.items.delete_item %}
23
+ <a href="{% url 'items:delete' slug=item.slug %}"
24
+ class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
25
+ Delete
26
+ </a>
27
+ {% endif %}
28
+ </div>
29
+ </div>
30
+
31
+ <div class="border-t border-gray-700 px-6 py-5">
32
+ <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
33
+ <div>
34
+ <dt class="text-sm font-medium text-gray-400">Price</dt>
35
+ <dd class="mt-1 text-sm text-gray-100">${{ item.price }}</dd>
36
+ </div>
37
+ <div>
38
+ <dt class="text-sm font-medium text-gray-400">SKU</dt>
39
+ <dd class="mt-1 text-sm text-gray-100">{{ item.sku|default:"—" }}</dd>
40
+ </div>
41
+ <div>
42
+ <dt class="text-sm font-medium text-gray-400">Status</dt>
43
+ <dd class="mt-1 text-sm">
44
+ {% if item.is_active %}
45
+ <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Active</span>
46
+ {% else %}
47
+ <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Inactive</span>
48
+ {% endif %}
49
+ </dd>
50
+ </div>
51
+ <div>
52
+ <dt class="text-sm font-medium text-gray-400">GUID</dt>
53
+ <dd class="mt-1 text-sm text-gray-400 font-mono">{{ item.guid }}</dd>
54
+ </div>
55
+ <div class="sm:col-span-2">
56
+ <dt class="text-sm font-medium text-gray-400">Description</dt>
57
+ <dd class="mt-1 text-sm text-gray-100">{{ item.description|default:"No description." }}</dd>
58
+ </div>
59
+ <div>
60
+ <dt class="text-sm font-medium text-gray-400">Created</dt>
61
+ <dd class="mt-1 text-sm text-gray-400">{{ item.created_at|date:"N j, Y g:i a" }} by {{ item.created_by|default:"system" }}</dd>
62
+ </div>
63
+ <div>
64
+ <dt class="text-sm font-medium text-gray-400">Updated</dt>
65
+ <dd class="mt-1 text-sm text-gray-400">{{ item.updated_at|date:"N j, Y g:i a" }}</dd>
66
+ </div>
67
+ </dl>
68
+ </div>
69
+</div>
70
+{% endblock %}
--- a/templates/items/item_detail.html
+++ b/templates/items/item_detail.html
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/items/item_detail.html
+++ b/templates/items/item_detail.html
@@ -0,0 +1,70 @@
1 {% extends "base.html" %}
2 {% block title %}{{ item.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'items:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Items</a>
7 </div>
8
9 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
10 <div class="px-6 py-5 sm:flex sm:items-center sm:justify-between">
11 <div>
12 <h1 class="text-2xl font-bold text-gray-100">{{ item.name }}</h1>
13 <p class="mt-1 text-sm text-gray-400">{{ item.slug }}</p>
14 </div>
15 <div class="mt-4 flex gap-3 sm:mt-0">
16 {% if perms.items.change_item %}
17 <a href="{% url 'items:update' slug=item.slug %}"
18 class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19 Edit
20 </a>
21 {% endif %}
22 {% if perms.items.delete_item %}
23 <a href="{% url 'items:delete' slug=item.slug %}"
24 class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
25 Delete
26 </a>
27 {% endif %}
28 </div>
29 </div>
30
31 <div class="border-t border-gray-700 px-6 py-5">
32 <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
33 <div>
34 <dt class="text-sm font-medium text-gray-400">Price</dt>
35 <dd class="mt-1 text-sm text-gray-100">${{ item.price }}</dd>
36 </div>
37 <div>
38 <dt class="text-sm font-medium text-gray-400">SKU</dt>
39 <dd class="mt-1 text-sm text-gray-100">{{ item.sku|default:"—" }}</dd>
40 </div>
41 <div>
42 <dt class="text-sm font-medium text-gray-400">Status</dt>
43 <dd class="mt-1 text-sm">
44 {% if item.is_active %}
45 <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Active</span>
46 {% else %}
47 <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Inactive</span>
48 {% endif %}
49 </dd>
50 </div>
51 <div>
52 <dt class="text-sm font-medium text-gray-400">GUID</dt>
53 <dd class="mt-1 text-sm text-gray-400 font-mono">{{ item.guid }}</dd>
54 </div>
55 <div class="sm:col-span-2">
56 <dt class="text-sm font-medium text-gray-400">Description</dt>
57 <dd class="mt-1 text-sm text-gray-100">{{ item.description|default:"No description." }}</dd>
58 </div>
59 <div>
60 <dt class="text-sm font-medium text-gray-400">Created</dt>
61 <dd class="mt-1 text-sm text-gray-400">{{ item.created_at|date:"N j, Y g:i a" }} by {{ item.created_by|default:"system" }}</dd>
62 </div>
63 <div>
64 <dt class="text-sm font-medium text-gray-400">Updated</dt>
65 <dd class="mt-1 text-sm text-gray-400">{{ item.updated_at|date:"N j, Y g:i a" }}</dd>
66 </div>
67 </dl>
68 </div>
69 </div>
70 {% endblock %}
--- a/templates/items/item_form.html
+++ b/templates/items/item_form.html
@@ -0,0 +1,42 @@
1
+{% extends "base.html" %}
2
+{% block title %}{{ title }} — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="mb-6">
6
+ <a href="{% url 'items:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Items</a>
7
+</div>
8
+
9
+<div class="mx-auto max-w-2xl">
10
+ <h1 class="text-2xl font-bold text-gray-100 mb-6">{{ title }}</h1>
11
+
12
+ <form method="post" class="space-y-6 rounded-lg bg-gray-800 p-6 shadow border border-gray-700">
13
+ {% csrf_token %}
14
+
15
+ {% for field in form %}
16
+ <div>
17
+ <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-300">
18
+ {{ field.label }}{% if field.field.required %} <span class="text-red-400">*</span>{% endif %}
19
+ </label>
20
+ <div class="mt-1">{{ field }}</div>
21
+ {% if field.errors %}
22
+ <p class="mt-1 text-sm text-red-400">{{ field.errors.0 }}</p>
23
+ {% endif %}
24
+ {% if field.help_text %}
25
+ <p class="mt-1 text-sm text-gray-400">{{ field.help_text }}</p>
26
+ {% endif %}
27
+ </div>
28
+ {% endfor %}
29
+
30
+ <div class="flex justify-end gap-3 pt-4">
31
+ <a href="{% url 'items:list' %}"
32
+ class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
33
+ Cancel
34
+ </a>
35
+ <button type="submit"
36
+ class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
37
+ {% if item %}Update{% else %}Create{% endif %}
38
+ </button>
39
+ </div>
40
+ </form>
41
+</div>
42
+{% endblock %}
--- a/templates/items/item_form.html
+++ b/templates/items/item_form.html
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/items/item_form.html
+++ b/templates/items/item_form.html
@@ -0,0 +1,42 @@
1 {% extends "base.html" %}
2 {% block title %}{{ title }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'items:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Items</a>
7 </div>
8
9 <div class="mx-auto max-w-2xl">
10 <h1 class="text-2xl font-bold text-gray-100 mb-6">{{ title }}</h1>
11
12 <form method="post" class="space-y-6 rounded-lg bg-gray-800 p-6 shadow border border-gray-700">
13 {% csrf_token %}
14
15 {% for field in form %}
16 <div>
17 <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-300">
18 {{ field.label }}{% if field.field.required %} <span class="text-red-400">*</span>{% endif %}
19 </label>
20 <div class="mt-1">{{ field }}</div>
21 {% if field.errors %}
22 <p class="mt-1 text-sm text-red-400">{{ field.errors.0 }}</p>
23 {% endif %}
24 {% if field.help_text %}
25 <p class="mt-1 text-sm text-gray-400">{{ field.help_text }}</p>
26 {% endif %}
27 </div>
28 {% endfor %}
29
30 <div class="flex justify-end gap-3 pt-4">
31 <a href="{% url 'items:list' %}"
32 class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
33 Cancel
34 </a>
35 <button type="submit"
36 class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
37 {% if item %}Update{% else %}Create{% endif %}
38 </button>
39 </div>
40 </form>
41 </div>
42 {% endblock %}
--- a/templates/items/item_list.html
+++ b/templates/items/item_list.html
@@ -0,0 +1,29 @@
1
+{% extends "base.html" %}
2
+{% block title %}Items — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="md:flex md:items-center md:justify-between mb-6">
6
+ <h1 class="text-2xl font-bold text-gray-100">Items</h1>
7
+ {% if perms.items.add_item %}
8
+ <a href="{% url 'items:create' %}"
9
+ class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10
+ New Item
11
+ </a>
12
+ {% endif %}
13
+</div>
14
+
15
+<div class="mb-4">
16
+ <input type="search"
17
+ name="search"
18
+ value="{{ search }}"
19
+ placeholder="Search items..."
20
+ class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21
+ hx-get="{% url 'items:list' %}"
22
+ hx-trigger="input changed delay:300ms, search"
23
+ hx-target="#item-table"
24
+ hx-swap="outerHTML"
25
+ hx-push-url="true" />
26
+</div>
27
+
28
+{% include "items/partials/item_table.html" %}
29
+{% endblock %}
--- a/templates/items/item_list.html
+++ b/templates/items/item_list.html
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/items/item_list.html
+++ b/templates/items/item_list.html
@@ -0,0 +1,29 @@
1 {% extends "base.html" %}
2 {% block title %}Items — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="md:flex md:items-center md:justify-between mb-6">
6 <h1 class="text-2xl font-bold text-gray-100">Items</h1>
7 {% if perms.items.add_item %}
8 <a href="{% url 'items:create' %}"
9 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10 New Item
11 </a>
12 {% endif %}
13 </div>
14
15 <div class="mb-4">
16 <input type="search"
17 name="search"
18 value="{{ search }}"
19 placeholder="Search items..."
20 class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21 hx-get="{% url 'items:list' %}"
22 hx-trigger="input changed delay:300ms, search"
23 hx-target="#item-table"
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "items/partials/item_table.html" %}
29 {% endblock %}
--- a/templates/items/partials/item_table.html
+++ b/templates/items/partials/item_table.html
@@ -0,0 +1,44 @@
1
+<div id="item-table">
2
+ <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
3
+ <table class="min-w-full divide-y divide-gray-700">
4
+ <thead class="bg-gray-900">
5
+ <tr>
6
+ <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Name</th>
7
+ <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">SKU</th>
8
+ <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Price</th>
9
+ <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Status</th>
10
+ <th class="px-6 py-3 text-right text-xs font-medium uppercase text-gray-400">Actions</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody class="divide-y divide-gray-700 bg-gray-800">
14
+ {% for item in items %}
15
+ <tr class="hover:bg-gray-700/50">
16
+ <td class="px-6 py-4 whitespace-nowrap">
17
+ <a href="{% url 'items:detail' slug=item.slug %}" class="text-brand-light hover:text-brand font-medium">
18
+ {{ item.name }}
19
+ </a>
20
+ </td>
21
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">{{ item.sku|default:"—" }}</td>
22
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-100">${{ item.price }}</td>
23
+ <td class="px-6 py-4 whitespace-nowrap">
24
+ {% if item.is_active %}
25
+ <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Active</span>
26
+ {% else %}
27
+ <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Inactive</span>
28
+ {% endif %}
29
+ </td>
30
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
31
+ {% if perms.items.change_item %}
32
+ <a href="{% url 'items:update' slug=item.slug %}" class="text-brand-light hover:text-brand">Edit</a>
33
+ {% endif %}
34
+ </td>
35
+ </tr>
36
+ {% empty %}
37
+ <tr>
38
+ <td colspan="5" class="px-6 py-8 text-center text-sm text-gray-400">No items found.</td>
39
+ </tr>
40
+ {% endfor %}
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+</div>
--- a/templates/items/partials/item_table.html
+++ b/templates/items/partials/item_table.html
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/items/partials/item_table.html
+++ b/templates/items/partials/item_table.html
@@ -0,0 +1,44 @@
1 <div id="item-table">
2 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
3 <table class="min-w-full divide-y divide-gray-700">
4 <thead class="bg-gray-900">
5 <tr>
6 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Name</th>
7 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">SKU</th>
8 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Price</th>
9 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Status</th>
10 <th class="px-6 py-3 text-right text-xs font-medium uppercase text-gray-400">Actions</th>
11 </tr>
12 </thead>
13 <tbody class="divide-y divide-gray-700 bg-gray-800">
14 {% for item in items %}
15 <tr class="hover:bg-gray-700/50">
16 <td class="px-6 py-4 whitespace-nowrap">
17 <a href="{% url 'items:detail' slug=item.slug %}" class="text-brand-light hover:text-brand font-medium">
18 {{ item.name }}
19 </a>
20 </td>
21 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">{{ item.sku|default:"—" }}</td>
22 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-100">${{ item.price }}</td>
23 <td class="px-6 py-4 whitespace-nowrap">
24 {% if item.is_active %}
25 <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Active</span>
26 {% else %}
27 <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Inactive</span>
28 {% endif %}
29 </td>
30 <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
31 {% if perms.items.change_item %}
32 <a href="{% url 'items:update' slug=item.slug %}" class="text-brand-light hover:text-brand">Edit</a>
33 {% endif %}
34 </td>
35 </tr>
36 {% empty %}
37 <tr>
38 <td colspan="5" class="px-6 py-8 text-center text-sm text-gray-400">No items found.</td>
39 </tr>
40 {% endfor %}
41 </tbody>
42 </table>
43 </div>
44 </div>

No diff available

--- a/testdata/apps.py
+++ b/testdata/apps.py
@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class TestdataConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "testdata"
--- a/testdata/apps.py
+++ b/testdata/apps.py
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/testdata/apps.py
+++ b/testdata/apps.py
@@ -0,0 +1,6 @@
1 from django.apps import AppConfig
2
3
4 class TestdataConfig(AppConfig):
5 default_auto_field = "django.db.models.BigAutoField"
6 name = "testdata"

No diff available

--- a/testdata/management/commands/seed.py
+++ b/testdata/management/commands/seed.py
@@ -0,0 +1,44 @@
1
+import logging
2
+
3
+from django.contrib.auth.models import Group, Permission, User
4
+from django.core.management.base import BaseCommand
5
+
6
+from items.models import Item
7
+from organization.models import Organization, OrganizationMemberimport Project, ProjectTeam
8
+
9
+logger = logging.getLogger(__name__)
10
+
11
+
12
+class Command(BaseCommand):
13
+ help = "Seed the database with initial data for development."
14
+
15
+ def add_arguments(self, parser):
16
+ parser.add_argument("--flush", action="store_true", help="Flush non-system tables before seeding.")
17
+
18
+ def handle(self, *args, **options):
19
+ if options["flush"]:
20
+ sitem and organizationself.stdout.write("FlusItemissions
21
+ admin_group,ber.all_objects.all().delete()
22
+ Organization.all_objects.all().delete()
23
+
24
+ # Groups and permissions
25
+ admin_group, _ = Group.objects.get_or_create(name="Administrators")
26
+ viewer_group, _ = Group.objects.get_or_create(name="Viewers")
27
+
28
+ item_up.permissions.add(*perms)
29
+
30
+ "items")roup.objects.get_or_.permissions.set(item_perms)bel__in=["organizationitem_perms.filter(coil": "[email protected]_or_corg_up.permissions.add(*perms)
31
+
32
+ "organization")roup.objects.get_or_.permissions.add(*org defaults={"email": "[email protected]", "is_staff": True, "is_superuser": True},
33
+ )
34
+ if created:
35
+ admin_user.set_password("admin")
36
+ admin_user.save()
37
+ self.stdout.write(self.style.SUCCESS("Created superuser: admin / admin"))
38
+
39
+ # Regular user
40
+ viewer_user, created = User.objects.get_or_create(
41
+ username="viewer",
42
+ defaults={"email": "[email protected]", "is_staff": False, "is_superuser": False},
43
+ )
44
+
--- a/testdata/management/commands/seed.py
+++ b/testdata/management/commands/seed.py
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/testdata/management/commands/seed.py
+++ b/testdata/management/commands/seed.py
@@ -0,0 +1,44 @@
1 import logging
2
3 from django.contrib.auth.models import Group, Permission, User
4 from django.core.management.base import BaseCommand
5
6 from items.models import Item
7 from organization.models import Organization, OrganizationMemberimport Project, ProjectTeam
8
9 logger = logging.getLogger(__name__)
10
11
12 class Command(BaseCommand):
13 help = "Seed the database with initial data for development."
14
15 def add_arguments(self, parser):
16 parser.add_argument("--flush", action="store_true", help="Flush non-system tables before seeding.")
17
18 def handle(self, *args, **options):
19 if options["flush"]:
20 sitem and organizationself.stdout.write("FlusItemissions
21 admin_group,ber.all_objects.all().delete()
22 Organization.all_objects.all().delete()
23
24 # Groups and permissions
25 admin_group, _ = Group.objects.get_or_create(name="Administrators")
26 viewer_group, _ = Group.objects.get_or_create(name="Viewers")
27
28 item_up.permissions.add(*perms)
29
30 "items")roup.objects.get_or_.permissions.set(item_perms)bel__in=["organizationitem_perms.filter(coil": "[email protected]_or_corg_up.permissions.add(*perms)
31
32 "organization")roup.objects.get_or_.permissions.add(*org defaults={"email": "[email protected]", "is_staff": True, "is_superuser": True},
33 )
34 if created:
35 admin_user.set_password("admin")
36 admin_user.save()
37 self.stdout.write(self.style.SUCCESS("Created superuser: admin / admin"))
38
39 # Regular user
40 viewer_user, created = User.objects.get_or_create(
41 username="viewer",
42 defaults={"email": "[email protected]", "is_staff": False, "is_superuser": False},
43 )
44

No diff available

+1350
--- a/uv.lock
+++ b/uv.lock
@@ -0,0 +1,1350 @@
1
+version = 1
2
+revision = 3
3
+requires-python = ">=3.12"
4
+
5
+[[package]]
6
+name = "amqp"
7
+version = "5.3.1"
8
+source = { registry = "https://pypi.org/simple" }
9
+dependencies = [
10
+ { name = "vine" },
11
+]
12
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
13
+wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
15
+]
16
+
17
+[[package]]
18
+name = "asgiref"
19
+version = "3.11.1"
20
+source = { registry = "https://pypi.org/simple" }
21
+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" }
22
+wheels = [
23
+ { 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" },
24
+]
25
+
26
+[[package]]
27
+name = "billiard"
28
+version = "4.2.4"
29
+source = { registry = "https://pypi.org/simple" }
30
+sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
31
+wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
33
+]
34
+
35
+[[package]]
36
+name = "boilerworks-django-htmx { url = "httspecifier = ">=2.9" },
37
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
38
+ { name = "pytest-cov", marker =5cb94a", size = 35914, uper = "extra == 'dev'", specifier = ">=4.9" },
39
+ { name = "redis", specifier = ">=5.0" },
40
+ { name = "requests", specifier = ">=2.31" },
41
+ { name = "rich", specifier = ">=13.0" },
42
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
43
+ { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
44
+ { name = "whitenoise", specifier = ">=6.7" },
45
+]
46
+provides-extras = ["dev"]
47
+
48
+[n"
49
+version = "1.5.5"
50
+source = {pi.orich size = 342432, upl8e1c137dfebd5759a2ist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
51
+wheels = [
52
+ { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freeoverage05, upload-time = 7.66e4188f887375398c761f340ff900470a45a4d/liceion = 3
53
+requires-python = ">=3.12"
54
+
55
+[[package]]
56
+name = "amqp"
57
+version = "5.3.1"
58
+source = { registry = "https://pypi.org/simple" }
59
+dependencies = [
60
+ { name = "vine" },
61
+]
62
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
63
+wheels = [
64
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
65
+]
66
+
67
+[[package= 1
68
+revision = 3
69
+requires-python = ">=3.12"
70
+
71
+[[package]]
72
+name = "amqp"
73
+version = "5.3.1"
74
+source = { registry = "https://pypi.org/simple" }
75
+dependencies = [
76
+ { name = "vine" },
77
+]
78
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
79
+wheels = [
80
+ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3boolean-py"
81
+version = "5.0"
82
+source = { registry = "https://pypi.org/simple" }
83
+sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
84
+wheels = [
85
+ { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
86
+]
87
+
88
+[[package]]
89
+name = "boto3"
90
+version = "1.42.76"
91
+source = { registry = "https://pypi.org/simple" }
92
+dependencies = [
93
+ { name = "botocore" },
94
+ { name = "jmespath" },
95
+ { name = "s3transfer" },
96
+]
97
+sdist = { url = "https://files.pythonhosted.org/packages/f1/13/33c8b8704d677fcaf5555ba8c6cc39468fc7b9a0c6b6c496e008cd5557fc/boto3-1.42.76.tar.gz", hash = "sha256:aa2b1973eee8973a9475d24bb579b1dee7176595338d4e4f7880b5c6189b8814", size = 112789, upload-time = "2026-03-25T19:33:25.985Z" }
98
+wheels = [
99
+ { url = "https://files.pythonhosted.org/packages/f0/dc/21b3dfb135125eb7e3a46b9aab0aede847726f239fc8f39474742a87ebb0/boto3-1.42.76-py3-none-any.whl", hash = "sha256:63c6779c814847016b89ae1b72ed968f8a63d80e589ba337511aa6fc1b59585e", size = 140557, upload-time = "2026-03-25T19:33:23.289Z" },
100
+]
101
+
102
+[[package]]
103
+name = "botocore"
104
+version = "1.42.76"
105
+source = { registry = "https://pypi.org/simple" }
106
+dependencies = [
107
+ { name = "jmespath" },
108
+ { name = "python-dateutil" },
109
+ { name = "urllib3" },
110
+]
111
+sdist = { url = "https://files.pythonhosted.org/packages/70/62/a982acb81c5e0312f90f841b790abad65622c08aad356eed7008ea3d475b/botocore-1.42.76.tar.gz", hash = "sha256:c553fa0ae29e36a5c407f74da78b78404b81b74b15fb62bf640a3cd9385f0874", size = 15021811, upload-time = "2026-03-25T19:33:12.171Z" }
112
+wheels = [
113
+ { url = "https://files.pythonhosted.org/packages/f5/63/7429d68876b7718ab5c4b8a44414de7907f5ba6bb27ccfad384df14fb277/botocore-1.42.76-py3-none-any.whl", hash = "sha256:151e714ae3c32f68ea0b4dc60751401e03f84a87c6cf864ea0ee64aa10eb4607", size = 14697736, upload-time = "2026-03-25T19:33:07.573Z" },
114
+]
115
+
116
+[[package]]
117
+name = "cachecontrol"
118
+version = "0.14.4"
119
+source = { registry = "https://pypi.org/simple" }
120
+dependencies = [
121
+ { name = "msgpack" },
122
+ { name = "requests" },
123
+]
124
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
125
+wheels = [
126
+ { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
127
+]
128
+
129
+[package.optional-dependencies]
130
+filecache = [
131
+ { name = "filelock" },
132
+]
133
+
134
+[[package]]
135
+name = "version = 1
136
+revisiversion = 1
137
+revision = 3
138
+requires-python = ">=3.12"
139
+
140
+[[package]]
141
+name = "amqp"
142
+version = "5.3.1"
143
+source = { registry = "https://pypi.org/simple" }
144
+dependencies = [
145
+ { name = "vine" },
146
+]
147
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
148
+wheels = [
149
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
150
+]
151
+
152
+[[package]]
153
+name = "asgiref"
154
+version = "3.11.1"
155
+source = { registry = "https://pypi.org/simple" }
156
+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" }
157
+wheels = [
158
+ { 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" },
159
+]
160
+
161
+[[package]]
162
+name = "billiard"
163
+version = "4.2.4"
164
+source = { registry = "https://pypi.org/simple" }
165
+sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
166
+wheels = [
167
+ { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
168
+]
169
+
170
+[[package]]
171
+name = "boolean-py"
172
+version = "5.0"
173
+source = { registry = "https://pypi.org/simple" }
174
+sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
175
+wheels = [
176
+ { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
177
+]
178
+
179
+[[package]]
180
+name = "boto3"
181
+version = "1.42.76"
182
+source = { registry = "https://pypi.org/simple" }
183
+dependencies = [
184
+ { name = "botocore" },
185
+ { name = "jmespath" },
186
+ { name = "s3transfer" },
187
+]
188
+sdist = { url = "https://files.pythonhosted.org/packages/f1/13/33c8b8704d677fcaf5555ba8c6cc39468fc7b9a0c6b6c496e008cd5557fc/boto3-1.42.76.tar.gz", hash = "sha256:aa2b1973eee8973a9475d24bb579b1dee7176595338d4e4f7880b5c6189b8814", size = 112789, upload-time = "2026-03-25T19:33:25.985Z" }
189
+wheels = [
190
+ { url = "https://files.pythonhosted.org/packages/f0/dc/21b3dfb135125eb7e3a46b9aab0aede847726f239fc8f39474742a87ebb0/boto3-1.42.76-py3-none-any.whl", hash = "sha256:63c6779c814847016b89ae1b72ed968f8a63d80e589ba337511aa6fc1b59585e", size = 140557, upload-time = "2026-03-25T19:33:23.289Z" },
191
+]
192
+
193
+[[package]]
194
+name = "botocore"
195
+version = "1.42.76"
196
+source = { registry = "https://pypi.org/simple" }
197
+dependencies = [
198
+ { name = "jmespath" },
199
+ { name = "python-dateutil" },
200
+ { name = "urllib3" },
201
+]
202
+sdist = { url = "https://files.pythonhosted.org/packages/70/62/a982acb81c5e0312f90f841b790abad65622c08aad356eed7008ea3d475b/botocore-1.42.76.tar.gz", hash = "sha256:c553fa0ae29e36a5c407f74da78b78404b81b74b15fb62bf640a3cd9385f0874", size = 15021811, upload-time = "2026-03-25T19:33:12.171Z" }
203
+wheels = [
204
+ { url = "https://files.pythonhosted.org/packages/f5/63/7429d68876b7718ab5c4b8a44414de7907f5ba6bb27ccfad384df14fb277/botocore-1.42.76-py3-none-any.whl", hash = "sha256:151e714ae3c32f68ea0b4dc60751401e03f84a87c6cf864ea0ee64aa10eb4607", size = 14697736, upload-time = "2026-03-25T19:33:07.573Z" },
205
+]
206
+
207
+[[package]]
208
+name = "cachecontrol"
209
+version = "0.14.4"
210
+source = { registry = "https://pypi.org/simple" }
211
+dependencies = [
212
+ { name = "msgpack" },
213
+ { name = "requests" },
214
+]
215
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
216
+wheels = [
217
+ { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
218
+]
219
+
220
+[package.optional-dependencies]
221
+filecache = [
222
+ { name = "filelock" },
223
+]
224
+
225
+[[package]]
226
+name = "celery"
227
+version = "5.6.3"
228
+source = { registry = "https://pypi.org/simple" }
229
+dependencies = [
230
+ { name = "billiard" },
231
+ { name = "click" },
232
+ { name = "click-didyoumean" },
233
+ { name = "click-plugins" },
234
+ { name = "click-repl" },
235
+ { name = "kombu" },
236
+ { name = "python-dateutil" },
237
+ { name = "tzlocal" },
238
+ { name = "vine" },
239
+]
240
+sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" }
241
+wheels = [
242
+ { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" },
243
+]
244
+
245
+[package.optional-dependencies]
246
+redis = [
247
+ { name = "kombu", extra = ["redis"] },
248
+]
249
+
250
+[[package]]
251
+name = "certifi"
252
+version = "2026.2.25"
253
+source = { registry = "https://pypi.org/simple" }
254
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
255
+wheels = [
256
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
257
+]
258
+
259
+[[package]]
260
+name = "cffi"
261
+version = "2.0.0"
262
+source = { registry = "https://pypi.org/simple" }
263
+dependencies = [
264
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
265
+]
266
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
267
+wheels = [
268
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
269
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
270
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
271
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
272
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
273
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
274
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
275
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
276
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
277
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
278
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
279
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
280
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
281
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
282
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
283
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
284
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
285
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
286
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
287
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
288
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
289
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
290
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
291
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
292
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
293
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
294
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
295
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
296
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
297
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
298
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
299
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
300
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
301
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
302
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
303
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
304
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
305
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
306
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
307
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
308
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
309
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
310
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
311
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
312
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
313
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
314
+]
315
+
316
+[[package]]
317
+name = "charset-normalizer"
318
+version = "3.4.6"
319
+source = { registry = "https://pypi.org/simple" }
320
+sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
321
+wheels = [
322
+ { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" },
323
+ { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" },
324
+ { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" },
325
+ { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" },
326
+ { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" },
327
+ { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" },
328
+ { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" },
329
+ { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" },
330
+ { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" },
331
+ { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" },
332
+ { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" },
333
+ { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" },
334
+ { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" },
335
+ { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" },
336
+ { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" },
337
+ { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" },
338
+ { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
339
+ { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
340
+ { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
341
+ { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
342
+ { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
343
+ { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
344
+ { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
345
+ { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
346
+ { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
347
+ { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
348
+ { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
349
+ { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
350
+ { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
351
+ { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
352
+ { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
353
+ { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
354
+ { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
355
+ { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
356
+ { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
357
+ { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
358
+ { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
359
+ { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
360
+ { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
361
+ { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
362
+ { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
363
+ { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
364
+ { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
365
+ { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
366
+ { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
367
+ { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
368
+ { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
369
+ { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
370
+ { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
371
+ { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
372
+ { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
373
+ { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
374
+ { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
375
+ { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
376
+ { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
377
+ { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
378
+ { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
379
+ { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
380
+ { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
381
+ { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
382
+ { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
383
+ { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
384
+ { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
385
+ { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
386
+ { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
387
+]
388
+
389
+[[package]]
390
+name = "click"
391
+version = "8.3.1"
392
+source = { registry = "https://pypi.org/simple" }
393
+dependencies = [
394
+ { name = "colorama", marker = "sys_platform == 'win32'" },
395
+]
396
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
397
+wheels = [
398
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
399
+]
400
+
401
+[[package]]
402
+name = "click-didyoumean"
403
+version = "0.3.1"
404
+source = { registry = "https://pypi.org/simple" }
405
+dependencies = [
406
+ { name = "click" },
407
+]
408
+sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
409
+wheels = [
410
+ { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
411
+]
412
+
413
+[[package]]
414
+name = "click-plugins"
415
+version = "1.1.1.2"
416
+source = { registry = "https://pypi.org/simple" }
417
+dependencies = [
418
+ { name = "click" },
419
+]
420
+sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
421
+wheels = [
422
+ { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
423
+]
424
+
425
+[[package]]
426
+name = "click-repl"
427
+version = "0.3.0"
428
+source = { registry = "https://pypi.org/simple" }
429
+dependencies = [
430
+ { name = "click" },
431
+ { name = "prompt-toolkit" },
432
+]
433
+sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
434
+wheels = [
435
+ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
436
+]
437
+
438
+[[package]]
439
+name = "colorama"
440
+version = "0.4.6"
441
+source = { registry = "https://pypi.org/simple" }
442
+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" }
443
+wheels = [
444
+ { 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" },
445
+]
446
+
447
+[[package]]
448
+name = "coverage"
449
+version = "7.13.5"
450
+source = { registry = "https://pypi.org/simple" }
451
+sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
452
+wheels = [
453
+ { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
454
+ { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
455
+ { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
456
+ { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
457
+ { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964dyclonedx-python-lib"
458
+version = "11.7.0"
459
+source = { registry = "https://pypi.org/simple" }
460
+dependencies = [
461
+ { name = "license-expression" },
462
+ { name = "packageurl-python" },
463
+ { name = "py-serializable" },
464
+ { name = "sortedcontainers" },
465
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
466
+]
467
+sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" }
468
+wheels = [
469
+ { url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" },
470
+]
471
+
472
+[[package]]
473
+name = "defusedxml"
474
+version = "0.7.1"
475
+source = { registry = "https://pypi.org/simple" }
476
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
477
+wheels = [
478
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
479
+]
480
+
481
+[[package]]
482
+name = "diff-match-patch"
483
+version = "20241021"
484
+source = { registry = "https://pypi.org/simple" }
485
+sdist = { url = "https://files.pythonhosted.org/packages/0e/ad/32e1777dd57d8e85fa31e3a243af66c538245b8d64b7265bec9a61f2ca33/diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073", size = 39962, upload-time = "2024-10-21T19:41:21.094Z" }
486
+wheels = [
487
+ { url = "https://files.pythonhosted.org/packages/f7/bb/2aa9b46a01197398b901e458974c20ed107935c26e44e37ad5b0e5511e44/diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", size = 43252, upload-time = "2024-10-21T19:41:19.914Z" },
488
+]
489
+
490
+[[package]]
491
+name = "django"
492
+version = "5.2.12"
493
+source = { registry = "https://pypi.org/simple" }
494
+dependencies = [
495
+ { name = "asgiref" },
496
+ { name = "sqlparse" },
497
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
498
+]
499
+sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b9445fc0695b03746f355c05b2eecc54c34e05198c686f4fc4406b722b52/django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb", size = 10860574, upload-time = "2026-03-03T13:56:05.509Z" }
500
+wheels = [
501
+ { url = "https://files.pythonhosted.org/packages/4e/32/4b144e125678efccf5d5b61581de1c4088d6b0286e46096e3b8de0d556c8/django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7", size = 8310245, upload-time = "2026-03-03T13:56:01.174Z" },
502
+]
503
+
504
+[[package]]
505
+name = "django-celery-beat"
506
+version = "2.9.0"
507
+source = { registry = "https://pypi.org/simple" }
508
+dependencies = [
509
+ { name = "celery" },
510
+ { name = "cron-descriptor" },
511
+ { name = "django" },
512
+ { name = "django-timezone-field" },
513
+ { name = "python-crontab" },
514
+ { name = "tzdata" },
515
+]
516
+sdist = { url = "https://files.pythonhosted.org/packages/05/45/fc97bc1d9af8e7dc07f1e37044d9551a30e6793249864cef802341e2e3a8/django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c", size = 177667, upload-time = "2026-02-28T16:45:34.749Z" }
517
+wheels = [
518
+ { url = "https://files.pythonhosted.org/packages/71/ae/9befa7ae37f5e5c41be636a254fcf47ff30dd5c88bd115070e252f6b9162/django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157", size = 105013, upload-time = "2026-02-28T16:45:32.822Z" },
519
+]
520
+
521
+[[package]]
522
+name = "django-celery-results"
523
+version = "2.6.0"
524
+source = { registry = "https://pypi.org/simple" }
525
+dependencies = [
526
+ { name = "celery" },
527
+ { name = "django" },
528
+]
529
+sdist = { url = "https://files.pythonhosted.org/packages/a6/b5/9966c28e31014c228305e09d48b19b35522a8f941fe5af5f81f40dc8fa80/django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c", size = 83985, upload-time = "2025-04-10T08:23:52.677Z" }
530
+wheels = [
531
+ { url = "https://files.pythonhosted.org/packages/2c/da/70f0f3c5364735344c4bc89e53413bcaae95b4fc1de4e98a7a3b9fb70c88/django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73", size = 38351, upload-time = "2025-04-10T08:23:49.965Z" },
532
+]
533
+
534
+[[package]]
535
+name = "django-constance"
536
+version = "4.3.5"
537
+source = { registry = "https://pypi.org/simple" }
538
+sdist = { url = "https://files.pythonhosted.org/packages/9c/95/8eff746544ba8958f431f4cfb162fd632db5f914b05b6a355a98eaf45cfd/django_constance-4.3.5.tar.gz", hash = "sha256:081177483d272b664cf768deae76fc2fbb0a777076f45620b6fde4d9075ee2b3", size = 181943, upload-time = "2026-03-15T11:23:50.799Z" }
539
+wheels = [
540
+ { url = "https://files.pythonhosted.org/packages/61/aa/3ff4198d02c0cb23c595fcfbed364bcf03b80f9109b7b971eaa34604c349/django_constance-4.3.5-py3-none-any.whl", hash = "sha256:c28f360c2822112772a3e4caf02db758c82cca8de7d0b9f648fef371bf4b8bc6", size = 66907, upload-time = "2026-03-15T11:23:49.166Z" },
541
+]
542
+
543
+[[package]]
544
+name = "django-cors-headers"
545
+version = "4.9.0"
546
+source = { registry = "https://pypi.org/simple" }
547
+dependencies = [
548
+ { name = "asgiref" },
549
+ { name = "django" },
550
+]
551
+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" }
552
+wheels = [
553
+ { 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" },
554
+]
555
+
556
+[[package]]
557
+name = "django-health-check"
558
+version = "4.2.1"
559
+source = { registry = "https://pypi.org/simple" }
560
+dependencies = [
561
+ { name = "django" },
562
+ { name = "dnspython" },
563
+]
564
+sdist = { url = "https://files.pythonhosted.org/packages/85/fb/fd4f1122c7be1ca28eb9c279b6098432b49565d55041dbe96ebcf815d5c2/django_health_check-4.2.1.tar.gz", hash = "sha256:aa05f57d6b01fe502842273aaa944e988b85d1f58e3ea67b6f98c5f9808a530a", size = 21391, upload-time = "2026-03-20T13:38:01.724Z" }
565
+wheels = [
566
+ { url = "https://files.pythonhosted.org/packages/93/05/d30df07b08194f1d89de44ecba867c467e9cc8e047a4cd7682a994a9468b/django_health_check-4.2.1-py3-none-any.whl", hash = "sha256:7216ba208f82f7587dc0ac0fe4c8f8a1c4d0cebbbae46cff6bd89779b378daf3", size = 26435, upload-time = "2026-03-20T13:38:00.557Z" },
567
+]
568
+
569
+[[package]]
570
+name = "django-import-export"
571
+version = "4.4.0"
572
+source = { registry = "https://pypi.org/simple" }
573
+dependencies = [
574
+ { name = "diff-match-patch" },
575
+ { name = "django" },
576
+ { name = "tablib" },
577
+]
578
+sdist = { url = "https://files.pythonhosted.org/packages/22/26/279bc8e6cb2c83d1b5dcdca07e932207c3352af11c6d305d6964a2d03ccc/django_import_export-4.4.0.tar.gz", hash = "sha256:9900e99c89027594941074fb4cd63a5f2964975e239021765c0f066003fcd412", size = 2237714, upload-time = "2026-01-10T20:57:35.128Z" }
579
+wheels = [
580
+ { url = "https://files.pythonhosted.org/packages/2f/e0/f4aa6d2374cc6b53b23f36bd0d5814e1db2769b25931b9908723fa295bb0/django_import_export-4.4.0-py3-none-any.whl", hash = "sha256:2d9b234c0f024d3377167f4d9c5a506e095c5bad98e06d30700e1d0752829e3d", size = 157449, upload-time = "2026-01-10T20:57:33.141Z" },
581
+]
582
+
583
+[[package]]
584
+name = "django-ratelimit"
585
+version = "4.1.0"
586
+source = { registry = "https://pypi.org/simple" }
587
+sdist = { url = "https://files.pythonhosted.org/packages/6f/8f/94038fe739b095aca3e4708ecc8a4e77f1fcfd87bed5d6baff43d4c80bc4/django-ratelimit-4.1.0.tar.gz", hash = "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", size = 11551, upload-time = "2023-07-24T20:34:32.374Z" }
588
+wheels = [
589
+ { url = "https://files.pythonhosted.org/packages/fb/78/2c59b30cd8bc8068d02349acb6aeed5c4e05eb01cdf2107ccd76f2e81487/django_ratelimit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92", size = 11608, upload-time = "2023-07-24T20:34:31.362Z" },
590
+]
591
+
592
+[[package]]
593
+name = "django-ses"
594
+version = "4.7.2"
595
+source = { registry = "https://pypi.org/simple" }
596
+dependencies = [
597
+ { name = "boto3" },
598
+ { name = "django" },
599
+]
600
+sdist = { url = "https://files.pythonhosted.org/packages/7f/25/25838da8e213c9f125b26a25360f0bb8ac57f07c24977451f3e7a0d63ddd/django_ses-4.7.2.tar.gz", hash = "sha256:a36f2af0e4ce060bf36053ed4c94feac1703ea3351e677c6f6421abd01433a35", size = 71828, upload-time = "2026-02-20T19:22:35.078Z" }
601
+wheels = [
602
+ { url = "https://files.pythonhosted.org/packages/84/f2/15d4bd54bd01e68e8a116e0b66243c91cb62a3fa0d780a39d2af6654e8ae/django_ses-4.7.2-py3-none-any.whl", hash = "sha256:f3db567fb6f43c01d7d890f5c991e1ebbfa48220de0be24d497ba6332004abcb", size = 37796, upload-time = "2026-02-20T19:22:32.819Z" },
603
+]
604
+
605
+[[package]]
606
+name = "django-simple-history"
607
+version = "3.11.0"
608
+source = { registry = "https://pypi.org/simple" }
609
+dependencies = [
610
+ { name = "django" },
611
+]
612
+sdist = { url = "https://files.pythonhosted.org/packages/a8/11/410049f1454b99a78f719d3403fc89437c2a38ee092e939d5ab8d4846738/django_simple_history-3.11.0.tar.gz", hash = "sha256:2c587479cf2c3071e9aa555d0d11b73676994db4910770958f57659ade2deffe", size = 234862, upload-time = "2025-12-11T13:50:55.022Z" }
613
+wheels = [
614
+ { url = "https://files.pythonhosted.org/packages/6e/c2/e9854a3438cfc80891ab4d3826b7c61a0fe5ba3a4da89104a8f5c9afb5df/django_simple_history-3.11.0-py3-none-any.whl", hash = "sha256:f3c298db49e418ffce7fb709a5e83108452ea2179ec5c4b9232484c25427192a", size = 81868, upload-time = "2025-12-11T13:50:53.71Z" },
615
+]
616
+
617
+[[package]]
618
+name = "django-storages"
619
+version = "1.14.6"
620
+source = { registry = "https://pypi.org/simple" }
621
+dependencies = [
622
+ { name = "django" },
623
+]
624
+sdist = { url = "https://files.pythonhosted.org/packages/ff/d6/2e50e378fff0408d558f36c4acffc090f9a641fd6e084af9e54d45307efa/django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9", size = 87587, upload-time = "2025-04-02T02:34:55.103Z" }
625
+wheels = [
626
+ { url = "https://files.pythonhosted.org/packages/1f/21/3cedee63417bc5553eed0c204be478071c9ab208e5e259e97287590194f1/django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9", size = 33095, upload-time = "2025-04-02T02:34:53.291Z" },
627
+]
628
+
629
+[package.optional-dependencies]
630
+s3 = [
631
+ { name = "boto3" },
632
+]
633
+
634
+[[package]]
635
+name = "django-timezone-field"
636
+version = "7.2.1"
637
+source = { registry = "https://pypi.org/simple" }
638
+dependencies = [
639
+ { name = "django" },
640
+]
641
+sdist = { url = "https://files.pythonhosted.org/packages/da/05/9b93a66452cdb8a08ab26f08d5766d2332673e659a8b2aeb73f2a904d421/django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e", size = 13096, upload-time = "2025-12-06T23:50:44.591Z" }
642
+wheels = [
643
+ { url = "https://files.pythonhosted.org/packages/41/7f/d885667401515b467f84569c56075bc9add72c9fd425fca51a25f4c997e1/django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44", size = 13284, upload-time = "2025-12-06T23:50:43.302Z" },
644
+]
645
+
646
+[[package]]
647
+name = "dnspython"
648
+version = "2.8.0"
649
+source = { registry = "https://pypi.org/simple" }
650
+sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
651
+wheels = [
652
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
653
+]
654
+
655
+[[package]]
656
+name = "filelock"
657
+version = "3.25.2"
658
+source = { registry = "https://pypi.org/simple" }
659
+sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
660
+wheels = [
661
+ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
662
+]
663
+
664
+[[package]]
665
+name = "fossilrepo"
666
+version = "0.1.0"
667
+source = { editable = "." }
668
+dependencies = [
669
+ { name = "boto3" },
670
+ { name = "celery", extra = ["redis"] },
671
+ { name liard"
672
+version = "4.2.4"
673
+version = 1
674
+revision = 3
675
+requires-python = ">=3.12"
676
+
677
+[[package]]
678
+name = "amqp"
679
+version = "5.3.1"
680
+source = { registry = "https://pypi.org/simple" }
681
+dependencies = [
682
+ { name = "vine" },
683
+]
684
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
685
+wheels = [
686
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
687
+]
688
+
689
+[[package]]
690
+name = "asgiref"
691
+version = "3.11.1"
692
+source = { registry = "https://pypi.org/simple" }
693
+sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4cversio},
694
+ { name = "django-ses", specifier = ">=4.1" },
695
+ { name = "django-simple-history", specifier = ">=3.7" },
696
+ { name = "django-storages", extras = ["s3"], specifier = ">=1.14" },
697
+ { name = "freezegun", marker = "extra == 'dev'", specifier = ">=1.4" },
698
+ { name = "gunicorn", specifier = ">=23.0" },
699
+ { name = "markdown", specifier = ">=3.6" },
700
+ { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" },
701
+ { name = "psycopg2-binary", specifier = ">=2.9" },
702
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
703
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
704
+ { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
705
+ { name = "redis", specifier = ">=5.0" },
706
+ { name = "requests", specifier = ">=2.31" },
707
+ { name = "rich", specifier = ">=13.0" },
708
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
709
+ { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
710
+ { name = "whitenoise", specifier = ">=6.7" },
711
+]
712
+provides-extras = ["dev"]
713
+
714
+[[package]]
715
+name = "freezegun"
716
+version = "1.5.5"
717
+source = { registry = "https://pypi.orich size = 342432, upl8e1c137dfebd5759a2ist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
718
+wheels = [
719
+ { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
720
+]
721
+
722
+[[package]]
723
+name = "gunicorn"
724
+version = "25.2.0"
725
+source = { registry = "https://pypi.org/simple" }
726
+dependencies = [
727
+ { name = "packaging" },
728
+]
729
+sdist = { url = "https://files.pythonhosted.org/packages/dd/13/dd3f8e40ea3ee907a6cbf3d1f1f81afcc3ecd0087d313baabfe95372f15c/gunicorn-25.2.0.tar.gz", hash = "sha256:10bd7adb36d44945d97d0a1fdf9a0fb086ae9c7b39e56b4dece8555a6bf4a09c", size = 632709, up56:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
730
+]
731
+
732
+[[package]]
733
+name = "markdown-it-py"
734
+version = "4.0.0"
735
+source = { registry = "https://pypi.org/simple" }
736
+dependencies = [
737
+ { name = "mdurl" },
738
+]
739
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
740
+wheels = [
741
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
742
+]
743
+
744
+[[package]]
745
+name = "mdurl"
746
+version = "0.1.2"
747
+source = { registry = "https://pypi.org/simple" }
748
+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" }
749
+wheels = [
750
+ { 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" },
751
+]
752
+
753
+[[package]]
754
+name = "msgpack"
755
+version = "1.1.2"
756
+source = { registry = "https://pypi.org/simple" }
757
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
758
+wheels = [
759
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
760
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
761
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
762
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
763
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
764
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
765
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
766
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
767
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
768
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
769
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
770
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
771
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
772
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
773
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
774
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
775
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
776
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
777
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
778
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
779
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
780
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
781
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
782
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
783
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
784
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
785
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
786
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
787
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
788
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
789
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
790
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
791
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
792
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
793
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
794
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
795
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
796
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
797
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
798
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
799
+]
800
+
801
+[[package]]
802
+name = "packageurl-python"
803
+version = "0.17.6"
804
+source = { registry = "https://pypi.org/simple" }
805
+sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
806
+wheels = [
807
+ { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
808
+]
809
+
810
+[[package]]
811
+name = "packaging"
812
+version = "26.0"
813
+source = { registry = "https://pypi.org/simple" }
814
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
815
+wheels = [
816
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
817
+]
818
+
819
+[[package]]
820
+name = "pip"
821
+version = "26.0.1"
822
+source = { registry = "https://pypi.org/simple" }
823
+sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
824
+wheels = [
825
+ { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
826
+]
827
+
828
+[[package]]
829
+name = "pip-api"
830
+version = "0.0.34"
831
+source = { registry = "https://pypi.org/simple" }
832
+dependencies = [
833
+ { name = "pip" },
834
+]
835
+sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
836
+wheels = [
837
+ { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
838
+]
839
+
840
+[[package]]
841
+name = "pip-audit"
842
+version = "2.10.0"
843
+source = { registry = "https://pypi.org/simple" }
844
+dependencies = [
845
+ { name = "cachecontrol", extra = ["filecache"] },
846
+ { name = "cyclonedx-python-lib" },
847
+ { name = "packaging" },
848
+ { name = "pip-api" },
849
+ { name = "pip-requirements-parser" },
850
+ { name = "platformdirs" },
851
+ { name = "requests" },
852
+ { name = "rich" },
853
+ { name = "tomli" },
854
+ { name = "tomli-w" },
855
+]
856
+sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" }
857
+wheels = [
858
+ { url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" },
859
+]
860
+
861
+[[package]]
862
+name = "pip-requirements-parser"
863
+version = "32.0.1"
864
+source = { registry = "https://pypi.org/simple" }
865
+dependencies = [
866
+ { name = "packaging" },
867
+ { name = "pyparsing" },
868
+]
869
+sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
870
+wheels = [
871
+ { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
872
+]
873
+
874
+[[package]]
875
+name = "platformdirs"
876
+version = "4.9.4"
877
+source = { registry = "https://pypi.org/simple" }
878
+sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
879
+wheels = [
880
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
881
+]
882
+
883
+[[package]]
884
+name = "pluggy"
885
+version = "1.6.0"
886
+source = { registry = "https://pypi.org/simple" }
887
+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" }
888
+wheels = [
889
+ { 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" },
890
+]
891
+
892
+[[package]]
893
+name = "prompt-toolkit"
894
+version = "3.0.52"
895
+source = { registry = "https://pypi.org/simple" }
896
+dependencies = [
897
+ { name = "wcwidth" },
898
+]
899
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
900
+wheels = [
901
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
902
+]
903
+
904
+[[package]]
905
+name = "psycopg2-binary"
906
+version = "2.9.11"
907
+source = { registry = "https://pypi.org/simple" }
908
+sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
909
+wheels = [
910
+ { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
911
+ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
912
+ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
913
+ { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
914
+ { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
915
+ { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
916
+ { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
917
+ { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
918
+ { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
919
+ { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
920
+ { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
921
+ { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
922
+ { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
923
+ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
924
+ { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
925
+ { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
926
+ { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
927
+ { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
928
+ { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
929
+ { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
930
+ { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
931
+ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
932
+ { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
933
+ { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
934
+ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
935
+ { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
936
+ { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
937
+ { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
938
+ { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
939
+ { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
940
+ { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
941
+ { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
942
+ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
943
+]
944
+
945
+[[package]]
946
+name = "py-serializable"
947
+version = "2.1.0"
948
+source = { registry = "https://pypi.org/simple" }
949
+dependencies = [
950
+ { name = "defusedxml" },
951
+]
952
+sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
953
+wheels = [
954
+ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
955
+]
956
+
957
+[[package]]
958
+name = "pycparser"
959
+version = "3.0"
960
+source = { registry = "https://pypi.org/simple" }
961
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
962
+wheels = [
963
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
964
+]
965
+
966
+[[package]]
967
+name = "pygments"
968
+version = "2.19.2"
969
+source = { registry = "https://pypi.org/simple" }
970
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
971
+wheels = [
972
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
973
+]
974
+
975
+[[package]]
976
+name = "pyparsing"
977
+version = "3.3.2"
978
+source = { registry = "https://pypi.org/simple" }
979
+sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
980
+wheels = [
981
+ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
982
+]
983
+
984
+[[package]]
985
+name = "pytest"
986
+version = "9.0.2"
987
+source = { registry = "https://pypi.org/simple" }
988
+dependencies = [
989
+ { name = "colorama", marker = "sys_platform == 'win32'" },
990
+ { name = "iniconfig" },
991
+ { name = "packaging" },
992
+ { name = "pluggy" },
993
+ { name = "pygments" },
994
+]
995
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
996
+wheels = [
997
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
998
+]
999
+
1000
+[[package]]
1001
+name = "pytest-cov"
1002
+version = "7.1.0"
1003
+source = { registry = "https://pypi.org/simple" }
1004
+dependencies = [
1005
+ { name = "coverage" },
1006
+ { name = "pluggy" },
1007
+ { name = "pytest" },
1008
+]
1009
+sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
1010
+wheels = [
1011
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
1012
+]
1013
+
1014
+[[package]]
1015
+name = "pytest-django"
1016
+version = "4.12.0"
1017
+source = { registry = "https://pypi.org/simple" }
1018
+dependencies = [
1019
+ { name = "pytest" },
1020
+]
1021
+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" }
1022
+wheels = [
1023
+ { 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" },
1024
+]
1025
+
1026
+[[package]]
1027
+name = "python-crontab"
1028
+version = "3.3.0"
1029
+source = { registry = "https://pypi.org/simple" }
1030
+sdist = { url = "https://files.pythonhosted.org/packages/99/7f/c54fb7e70b59844526aa4ae321e927a167678660ab51dda979955eafb89a/python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b", size = 57626, upload-time = "2025-07-13T20:05:35.535Z" }
1031
+wheels = [
1032
+ { url = "https://files.pythonhosted.org/packages/47/42/bb4afa5b088f64092036221843fc989b7db9d9d302494c1f8b024ee78a46/python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884", size = 27533, upload-time = "2025-07-13T20:05:34.266Z" },
1033
+]
1034
+
1035
+[[package]]
1036
+name = "python-dateutil"
1037
+version = "2.9.0.post0"
1038
+source = { registry = "https://pypi.org/simple" }
1039
+dependencies = [
1040
+ { name = "six" },
1041
+]
1042
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
1043
+wheels = [
1044
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
1045
+]
1046
+
1047
+[[package]]
1048
+name = "redis"
1049
+version = "6.4.0"
1050
+source = { registry = "https://pypi.org/simple" }
1051
+sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
1052
+wheels = [
1053
+ { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
1054
+]
1055
+
1056
+[[package]]
1057
+name = "requests"
1058
+version = "2.33.0"
1059
+source = { registry = "https://pypi.org/simple" }
1060
+dependencies = [
1061
+ { name = "certifi" },
1062
+ { name = "charset-normalizer" },
1063
+ { name = "idna" },
1064
+ { name = "urllib3" },
1065
+]
1066
+sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
1067
+wheels = [
1068
+ { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
1069
+]
1070
+
1071
+[[package]]
1072
+name = "rich"
1073
+version = "14.3.3"
1074
+source = { registry = "https://pypi.org/simple" }
1075
+dependencies = [
1076
+ { name = "markdown-it-py" },
1077
+ { name = "pygments" },
1078
+]
1079
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
1080
+wheels = [
1081
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
1082
+]
1083
+
1084
+[[package]]
1085
+name = "ruff"
1086
+version = "0.15.7"
1087
+source = { registry = "https://pypi.org/simple" }
1088
+sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
1089
+wheels = [
1090
+ { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
1091
+ { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
1092
+ { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
1093
+ { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
1094
+ { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
1095
+ { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
1096
+ { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
1097
+ { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
1098
+ { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
1099
+ { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
1100
+ { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
1101
+ { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
1102
+ { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
1103
+ { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
1104
+ { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
1105
+ { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
1106
+ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
1107
+]
1108
+
1109
+[[package]]
1110
+name = "s3transfer"
1111
+version = "0.16.0"
1112
+source = { registry = "https://pypi.org/simple" }
1113
+dependencies = [
1114
+ { name = "botocore" },
1115
+]
1116
+sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
1117
+wheels = [
1118
+ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
1119
+]
1120
+
1121
+[[package]]
1122
+name = "sentry-sdk"
1123
+version = "2.56.0"
1124
+source = { registry = "https://pypi.org/simple" }
1125
+dependencies = [
1126
+ { name = "certifi" },
1127
+ { name = "urllib3" },
1128
+]
1129
+sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" }
1130
+wheels = [
1131
+ { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" },
1132
+]
1133
+
1134
+[package.optional-dependencies]
1135
+django = [
1136
+ { name = "django" },
1137
+]
1138
+
1139
+[[package]]
1140
+name = "six"
1141
+version = "1.17.0"
1142
+source = { registry = "https://pypi.org/simple" }
1143
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
1144
+wheels = [
1145
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
1146
+]
1147
+
1148
+[[package]]
1149
+name = "sortedcontainers"
1150
+version = "2.4.0"
1151
+source = { registry = "https://pypi.org/simple" }
1152
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
1153
+wheels = [
1154
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
1155
+]
1156
+
1157
+[[package]]
1158
+name = "sqlparse"
1159
+version = "0.5.5"
1160
+source = { registry = "https://pypi.org/simple" }
1161
+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" }
1162
+wheels = [
1163
+ { 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" },
1164
+]
1165
+
1166
+[[package]]
1167
+name = "tablib"
1168
+version = "3.9.0"
1169
+source = { registry = "https://pypi.org/simple" }
1170
+sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" }
1171
+wheels = [
1172
+ { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" },
1173
+]
1174
+
1175
+[[package]]
1176
+name = "tomli"
1177
+version = "2.4.1"
1178
+source = { registry = "https://pypi.org/simple" }
1179
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
1180
+wheels = [
1181
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
1182
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
1183
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
1184
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
1185
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
1186
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
1187
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
1188
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
1189
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
1190
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
1191
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
1192
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
1193
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
1194
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
1195
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
1196
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
1197
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
1198
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
1199
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
1200
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
1201
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
1202
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
1203
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
1204
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
1205
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
1206
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
1207
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
1208
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
1209
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
1210
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
1211
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
1212
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
1213
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
1214
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
1215
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
1216
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
1217
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
1218
+]
1219
+
1220
+[[package]]
1221
+name = "tomli-w"
1222
+version = "1.2.0"
1223
+source = { registry = "https://pypi.org/simple" }
1224
+sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
1225
+wheels = [
1226
+ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
1227
+]
1228
+
1229
+[[package]]
1230
+name = "typing-extensions"
1231
+version = "4.15.0"
1232
+source = { registry = "https://pypi.org/simple" }
1233
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
1234
+wheels = [
1235
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
1236
+]
1237
+
1238
+[[package]]
1239
+name = "tzdata"
1240
+version = "2025.3"
1241
+source = { registry = "https://pypi.org/simple" }
1242
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
1243
+wheels = [
1244
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
1245
+]
1246
+
1247
+[[package]]
1248
+name = "tzlocal"
1249
+version = "5.3.1"
1250
+source = { registry = "https://pypi.org/simple" }
1251
+dependencies = [
1252
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
1253
+]
1254
+sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
1255
+wheels = [
1256
+ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
1257
+]
1258
+
1259
+[[package]]
1260
+name = "urllib3"
1261
+version = "2.6.3"
1262
+source = { registry = "https://pypi.org/simple" }
1263
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
1264
+wheels = [
1265
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
1266
+]
1267
+
1268
+[[package]]
1269
+name = "vine"
1270
+version = "5.1.0"
1271
+source = { registry = "https://pypi.org/simple" }
1272
+sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
1273
+wheels = [
1274
+ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
1275
+]
1276
+
1277
+[[package]]
1278
+name = "wcwidth"
1279
+version = "0.6.0"
1280
+source = { registry = "https://pypi.org/simple" }
1281
+sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
1282
+wheels = [
1283
+ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
1284
+]
1285
+
1286
+[[package]]
1287
+name = "whitenoise"
1288
+version = "6.12.0"
1289
+source = { registry = "https://pypi.org/simple" }
1290
+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" }
1291
+wheels = [
1292
+ { 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" },
1293
+]
1294
+5.1,<6.0" },
1295
+ { name = "django-celery-beat", specifier = ">=2.7" },
1296
+ { name = "django-celery-results", specifier = ">=2.5" },
1297
+ { name = "django-constance", extras = ["database"], specifier = ">=4.1" },
1298
+ { name = "django-cors-headers", specifier = ">=4.4" },
1299
+ { name = "django-health-check", specifier = ">=3.18" },
1300
+ { name = "django-import-export", specifier = ">=4.0" },
1301
+ { name = "django-ratelimit", specifier = ">=4.1" },
1302
+ { name = "django-ses", specifier = ">=4.1" },
1303
+ { name = "django-simple-history", specifier = ">=3.7" },
1304
+ { name = "django-storages", extras = ["s3"], specifier = ">=1.14" },
1305
+ { name = "freezegun", marker = "extra == 'dev'", specifier = ">=1.4" },
1306
+ { name = "gunicorn", specifier = ">=23.0" },
1307
+ { name = "markdown", specifier = ">=3.6" },
1308
+ { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" },
1309
+ { name = "psycopg2-binary", specifier = ">=2.9" },
1310
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
1311
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
1312
+ { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
1313
+ { name = "redis", specifier = ">=5.0" },
1314
+ { name = "requests", specifier = ">=2.31" },
1315
+ { name = "rich", specifier = ">=13.0" },
1316
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
1317
+ { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
1318
+ { name = "whitenoise", specifier = ">=6.7" },
1319
+]
1320
+provides-extras = ["dev"]
1321
+
1322
+[[package]]
1323
+name = "freezegun"
1324
+version = "1.5.5"
1325
+source = { registry = "https://pypi.org/simple" }
1326
+dependencies = [
1327
+ { name = "python-dateutil" },
1328
+]
1329
+sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
1330
+wheels = [
1331
+ { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
1332
+]
1333
+
1334
+[[package]]
1335
+name = "gunicorn"
1336
+version = "25.2.0"
1337
+source = { registry = "https://pypi.org/simple" }
1338
+dependencies = [
1339
+ { name = "packaging" },
1340
+]
1341
+sdist = { url = "https://files.pythonhosted.org/packages/dd/13/dd3f8e40ea3ee907a6cbf3d1f1f81afcc3ecd0087d313baabfe95372f15c/gunicorn-25.2.0.tar.gz", hash = "sha256:10bd7adb36d44945d97d0a1fdf9a0fb086ae9c7b39e56b4dece8555a6bf4a09c", size = 632709, upload-time = "2026-03-24T22:49:54.433Z" }
1342
+wheels = [
1343
+ { url = "https://files.pythonhosted.org/packages/11/53/fb024445837e02cd5cf989cf349bfac6f3f433c05184ea5d49c8ade751c6/gunicorn-25.2.0-py3-none-any.whl", hash = "sha256:88f5b444d0055bf298435384af7294f325e2273fd37ba9f9ff7b98e0a1e5dfdc", size = 211659, upload-time = "2026-03-24T22:49:52.528Z" },
1344
+]
1345
+
1346
+[[package]]
1347
+name = "idna"
1348
+version = "3.11"
1349
+source = { registry = "https://pypi.org/simple" }
1350
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c0
--- a/uv.lock
+++ b/uv.lock
@@ -0,0 +1,1350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/uv.lock
+++ b/uv.lock
@@ -0,0 +1,1350 @@
1 version = 1
2 revision = 3
3 requires-python = ">=3.12"
4
5 [[package]]
6 name = "amqp"
7 version = "5.3.1"
8 source = { registry = "https://pypi.org/simple" }
9 dependencies = [
10 { name = "vine" },
11 ]
12 sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
13 wheels = [
14 { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
15 ]
16
17 [[package]]
18 name = "asgiref"
19 version = "3.11.1"
20 source = { registry = "https://pypi.org/simple" }
21 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" }
22 wheels = [
23 { 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" },
24 ]
25
26 [[package]]
27 name = "billiard"
28 version = "4.2.4"
29 source = { registry = "https://pypi.org/simple" }
30 sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
31 wheels = [
32 { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
33 ]
34
35 [[package]]
36 name = "boilerworks-django-htmx { url = "httspecifier = ">=2.9" },
37 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
38 { name = "pytest-cov", marker =5cb94a", size = 35914, uper = "extra == 'dev'", specifier = ">=4.9" },
39 { name = "redis", specifier = ">=5.0" },
40 { name = "requests", specifier = ">=2.31" },
41 { name = "rich", specifier = ">=13.0" },
42 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
43 { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
44 { name = "whitenoise", specifier = ">=6.7" },
45 ]
46 provides-extras = ["dev"]
47
48 [n"
49 version = "1.5.5"
50 source = {pi.orich size = 342432, upl8e1c137dfebd5759a2ist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
51 wheels = [
52 { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freeoverage05, upload-time = 7.66e4188f887375398c761f340ff900470a45a4d/liceion = 3
53 requires-python = ">=3.12"
54
55 [[package]]
56 name = "amqp"
57 version = "5.3.1"
58 source = { registry = "https://pypi.org/simple" }
59 dependencies = [
60 { name = "vine" },
61 ]
62 sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
63 wheels = [
64 { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
65 ]
66
67 [[package= 1
68 revision = 3
69 requires-python = ">=3.12"
70
71 [[package]]
72 name = "amqp"
73 version = "5.3.1"
74 source = { registry = "https://pypi.org/simple" }
75 dependencies = [
76 { name = "vine" },
77 ]
78 sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
79 wheels = [
80 { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3boolean-py"
81 version = "5.0"
82 source = { registry = "https://pypi.org/simple" }
83 sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
84 wheels = [
85 { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
86 ]
87
88 [[package]]
89 name = "boto3"
90 version = "1.42.76"
91 source = { registry = "https://pypi.org/simple" }
92 dependencies = [
93 { name = "botocore" },
94 { name = "jmespath" },
95 { name = "s3transfer" },
96 ]
97 sdist = { url = "https://files.pythonhosted.org/packages/f1/13/33c8b8704d677fcaf5555ba8c6cc39468fc7b9a0c6b6c496e008cd5557fc/boto3-1.42.76.tar.gz", hash = "sha256:aa2b1973eee8973a9475d24bb579b1dee7176595338d4e4f7880b5c6189b8814", size = 112789, upload-time = "2026-03-25T19:33:25.985Z" }
98 wheels = [
99 { url = "https://files.pythonhosted.org/packages/f0/dc/21b3dfb135125eb7e3a46b9aab0aede847726f239fc8f39474742a87ebb0/boto3-1.42.76-py3-none-any.whl", hash = "sha256:63c6779c814847016b89ae1b72ed968f8a63d80e589ba337511aa6fc1b59585e", size = 140557, upload-time = "2026-03-25T19:33:23.289Z" },
100 ]
101
102 [[package]]
103 name = "botocore"
104 version = "1.42.76"
105 source = { registry = "https://pypi.org/simple" }
106 dependencies = [
107 { name = "jmespath" },
108 { name = "python-dateutil" },
109 { name = "urllib3" },
110 ]
111 sdist = { url = "https://files.pythonhosted.org/packages/70/62/a982acb81c5e0312f90f841b790abad65622c08aad356eed7008ea3d475b/botocore-1.42.76.tar.gz", hash = "sha256:c553fa0ae29e36a5c407f74da78b78404b81b74b15fb62bf640a3cd9385f0874", size = 15021811, upload-time = "2026-03-25T19:33:12.171Z" }
112 wheels = [
113 { url = "https://files.pythonhosted.org/packages/f5/63/7429d68876b7718ab5c4b8a44414de7907f5ba6bb27ccfad384df14fb277/botocore-1.42.76-py3-none-any.whl", hash = "sha256:151e714ae3c32f68ea0b4dc60751401e03f84a87c6cf864ea0ee64aa10eb4607", size = 14697736, upload-time = "2026-03-25T19:33:07.573Z" },
114 ]
115
116 [[package]]
117 name = "cachecontrol"
118 version = "0.14.4"
119 source = { registry = "https://pypi.org/simple" }
120 dependencies = [
121 { name = "msgpack" },
122 { name = "requests" },
123 ]
124 sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
125 wheels = [
126 { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
127 ]
128
129 [package.optional-dependencies]
130 filecache = [
131 { name = "filelock" },
132 ]
133
134 [[package]]
135 name = "version = 1
136 revisiversion = 1
137 revision = 3
138 requires-python = ">=3.12"
139
140 [[package]]
141 name = "amqp"
142 version = "5.3.1"
143 source = { registry = "https://pypi.org/simple" }
144 dependencies = [
145 { name = "vine" },
146 ]
147 sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
148 wheels = [
149 { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
150 ]
151
152 [[package]]
153 name = "asgiref"
154 version = "3.11.1"
155 source = { registry = "https://pypi.org/simple" }
156 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" }
157 wheels = [
158 { 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" },
159 ]
160
161 [[package]]
162 name = "billiard"
163 version = "4.2.4"
164 source = { registry = "https://pypi.org/simple" }
165 sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
166 wheels = [
167 { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
168 ]
169
170 [[package]]
171 name = "boolean-py"
172 version = "5.0"
173 source = { registry = "https://pypi.org/simple" }
174 sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
175 wheels = [
176 { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
177 ]
178
179 [[package]]
180 name = "boto3"
181 version = "1.42.76"
182 source = { registry = "https://pypi.org/simple" }
183 dependencies = [
184 { name = "botocore" },
185 { name = "jmespath" },
186 { name = "s3transfer" },
187 ]
188 sdist = { url = "https://files.pythonhosted.org/packages/f1/13/33c8b8704d677fcaf5555ba8c6cc39468fc7b9a0c6b6c496e008cd5557fc/boto3-1.42.76.tar.gz", hash = "sha256:aa2b1973eee8973a9475d24bb579b1dee7176595338d4e4f7880b5c6189b8814", size = 112789, upload-time = "2026-03-25T19:33:25.985Z" }
189 wheels = [
190 { url = "https://files.pythonhosted.org/packages/f0/dc/21b3dfb135125eb7e3a46b9aab0aede847726f239fc8f39474742a87ebb0/boto3-1.42.76-py3-none-any.whl", hash = "sha256:63c6779c814847016b89ae1b72ed968f8a63d80e589ba337511aa6fc1b59585e", size = 140557, upload-time = "2026-03-25T19:33:23.289Z" },
191 ]
192
193 [[package]]
194 name = "botocore"
195 version = "1.42.76"
196 source = { registry = "https://pypi.org/simple" }
197 dependencies = [
198 { name = "jmespath" },
199 { name = "python-dateutil" },
200 { name = "urllib3" },
201 ]
202 sdist = { url = "https://files.pythonhosted.org/packages/70/62/a982acb81c5e0312f90f841b790abad65622c08aad356eed7008ea3d475b/botocore-1.42.76.tar.gz", hash = "sha256:c553fa0ae29e36a5c407f74da78b78404b81b74b15fb62bf640a3cd9385f0874", size = 15021811, upload-time = "2026-03-25T19:33:12.171Z" }
203 wheels = [
204 { url = "https://files.pythonhosted.org/packages/f5/63/7429d68876b7718ab5c4b8a44414de7907f5ba6bb27ccfad384df14fb277/botocore-1.42.76-py3-none-any.whl", hash = "sha256:151e714ae3c32f68ea0b4dc60751401e03f84a87c6cf864ea0ee64aa10eb4607", size = 14697736, upload-time = "2026-03-25T19:33:07.573Z" },
205 ]
206
207 [[package]]
208 name = "cachecontrol"
209 version = "0.14.4"
210 source = { registry = "https://pypi.org/simple" }
211 dependencies = [
212 { name = "msgpack" },
213 { name = "requests" },
214 ]
215 sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
216 wheels = [
217 { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
218 ]
219
220 [package.optional-dependencies]
221 filecache = [
222 { name = "filelock" },
223 ]
224
225 [[package]]
226 name = "celery"
227 version = "5.6.3"
228 source = { registry = "https://pypi.org/simple" }
229 dependencies = [
230 { name = "billiard" },
231 { name = "click" },
232 { name = "click-didyoumean" },
233 { name = "click-plugins" },
234 { name = "click-repl" },
235 { name = "kombu" },
236 { name = "python-dateutil" },
237 { name = "tzlocal" },
238 { name = "vine" },
239 ]
240 sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" }
241 wheels = [
242 { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" },
243 ]
244
245 [package.optional-dependencies]
246 redis = [
247 { name = "kombu", extra = ["redis"] },
248 ]
249
250 [[package]]
251 name = "certifi"
252 version = "2026.2.25"
253 source = { registry = "https://pypi.org/simple" }
254 sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
255 wheels = [
256 { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
257 ]
258
259 [[package]]
260 name = "cffi"
261 version = "2.0.0"
262 source = { registry = "https://pypi.org/simple" }
263 dependencies = [
264 { name = "pycparser", marker = "implementation_name != 'PyPy'" },
265 ]
266 sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
267 wheels = [
268 { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
269 { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
270 { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
271 { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
272 { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
273 { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
274 { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
275 { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
276 { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
277 { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
278 { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
279 { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
280 { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
281 { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
282 { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
283 { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
284 { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
285 { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
286 { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
287 { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
288 { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
289 { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
290 { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
291 { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
292 { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
293 { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
294 { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
295 { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
296 { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
297 { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
298 { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
299 { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
300 { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
301 { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
302 { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
303 { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
304 { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
305 { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
306 { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
307 { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
308 { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
309 { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
310 { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
311 { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
312 { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
313 { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
314 ]
315
316 [[package]]
317 name = "charset-normalizer"
318 version = "3.4.6"
319 source = { registry = "https://pypi.org/simple" }
320 sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
321 wheels = [
322 { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" },
323 { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" },
324 { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" },
325 { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" },
326 { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" },
327 { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" },
328 { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" },
329 { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" },
330 { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" },
331 { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" },
332 { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" },
333 { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" },
334 { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" },
335 { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" },
336 { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" },
337 { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" },
338 { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
339 { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
340 { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
341 { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
342 { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
343 { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
344 { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
345 { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
346 { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
347 { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
348 { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
349 { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
350 { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
351 { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
352 { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
353 { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
354 { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
355 { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
356 { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
357 { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
358 { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
359 { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
360 { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
361 { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
362 { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
363 { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
364 { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
365 { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
366 { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
367 { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
368 { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
369 { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
370 { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
371 { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
372 { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
373 { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
374 { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
375 { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
376 { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
377 { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
378 { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
379 { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
380 { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
381 { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
382 { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
383 { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
384 { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
385 { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
386 { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
387 ]
388
389 [[package]]
390 name = "click"
391 version = "8.3.1"
392 source = { registry = "https://pypi.org/simple" }
393 dependencies = [
394 { name = "colorama", marker = "sys_platform == 'win32'" },
395 ]
396 sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
397 wheels = [
398 { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
399 ]
400
401 [[package]]
402 name = "click-didyoumean"
403 version = "0.3.1"
404 source = { registry = "https://pypi.org/simple" }
405 dependencies = [
406 { name = "click" },
407 ]
408 sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
409 wheels = [
410 { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
411 ]
412
413 [[package]]
414 name = "click-plugins"
415 version = "1.1.1.2"
416 source = { registry = "https://pypi.org/simple" }
417 dependencies = [
418 { name = "click" },
419 ]
420 sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
421 wheels = [
422 { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
423 ]
424
425 [[package]]
426 name = "click-repl"
427 version = "0.3.0"
428 source = { registry = "https://pypi.org/simple" }
429 dependencies = [
430 { name = "click" },
431 { name = "prompt-toolkit" },
432 ]
433 sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
434 wheels = [
435 { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
436 ]
437
438 [[package]]
439 name = "colorama"
440 version = "0.4.6"
441 source = { registry = "https://pypi.org/simple" }
442 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" }
443 wheels = [
444 { 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" },
445 ]
446
447 [[package]]
448 name = "coverage"
449 version = "7.13.5"
450 source = { registry = "https://pypi.org/simple" }
451 sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
452 wheels = [
453 { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
454 { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
455 { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
456 { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
457 { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964dyclonedx-python-lib"
458 version = "11.7.0"
459 source = { registry = "https://pypi.org/simple" }
460 dependencies = [
461 { name = "license-expression" },
462 { name = "packageurl-python" },
463 { name = "py-serializable" },
464 { name = "sortedcontainers" },
465 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
466 ]
467 sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" }
468 wheels = [
469 { url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" },
470 ]
471
472 [[package]]
473 name = "defusedxml"
474 version = "0.7.1"
475 source = { registry = "https://pypi.org/simple" }
476 sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
477 wheels = [
478 { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
479 ]
480
481 [[package]]
482 name = "diff-match-patch"
483 version = "20241021"
484 source = { registry = "https://pypi.org/simple" }
485 sdist = { url = "https://files.pythonhosted.org/packages/0e/ad/32e1777dd57d8e85fa31e3a243af66c538245b8d64b7265bec9a61f2ca33/diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073", size = 39962, upload-time = "2024-10-21T19:41:21.094Z" }
486 wheels = [
487 { url = "https://files.pythonhosted.org/packages/f7/bb/2aa9b46a01197398b901e458974c20ed107935c26e44e37ad5b0e5511e44/diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", size = 43252, upload-time = "2024-10-21T19:41:19.914Z" },
488 ]
489
490 [[package]]
491 name = "django"
492 version = "5.2.12"
493 source = { registry = "https://pypi.org/simple" }
494 dependencies = [
495 { name = "asgiref" },
496 { name = "sqlparse" },
497 { name = "tzdata", marker = "sys_platform == 'win32'" },
498 ]
499 sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b9445fc0695b03746f355c05b2eecc54c34e05198c686f4fc4406b722b52/django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb", size = 10860574, upload-time = "2026-03-03T13:56:05.509Z" }
500 wheels = [
501 { url = "https://files.pythonhosted.org/packages/4e/32/4b144e125678efccf5d5b61581de1c4088d6b0286e46096e3b8de0d556c8/django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7", size = 8310245, upload-time = "2026-03-03T13:56:01.174Z" },
502 ]
503
504 [[package]]
505 name = "django-celery-beat"
506 version = "2.9.0"
507 source = { registry = "https://pypi.org/simple" }
508 dependencies = [
509 { name = "celery" },
510 { name = "cron-descriptor" },
511 { name = "django" },
512 { name = "django-timezone-field" },
513 { name = "python-crontab" },
514 { name = "tzdata" },
515 ]
516 sdist = { url = "https://files.pythonhosted.org/packages/05/45/fc97bc1d9af8e7dc07f1e37044d9551a30e6793249864cef802341e2e3a8/django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c", size = 177667, upload-time = "2026-02-28T16:45:34.749Z" }
517 wheels = [
518 { url = "https://files.pythonhosted.org/packages/71/ae/9befa7ae37f5e5c41be636a254fcf47ff30dd5c88bd115070e252f6b9162/django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157", size = 105013, upload-time = "2026-02-28T16:45:32.822Z" },
519 ]
520
521 [[package]]
522 name = "django-celery-results"
523 version = "2.6.0"
524 source = { registry = "https://pypi.org/simple" }
525 dependencies = [
526 { name = "celery" },
527 { name = "django" },
528 ]
529 sdist = { url = "https://files.pythonhosted.org/packages/a6/b5/9966c28e31014c228305e09d48b19b35522a8f941fe5af5f81f40dc8fa80/django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c", size = 83985, upload-time = "2025-04-10T08:23:52.677Z" }
530 wheels = [
531 { url = "https://files.pythonhosted.org/packages/2c/da/70f0f3c5364735344c4bc89e53413bcaae95b4fc1de4e98a7a3b9fb70c88/django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73", size = 38351, upload-time = "2025-04-10T08:23:49.965Z" },
532 ]
533
534 [[package]]
535 name = "django-constance"
536 version = "4.3.5"
537 source = { registry = "https://pypi.org/simple" }
538 sdist = { url = "https://files.pythonhosted.org/packages/9c/95/8eff746544ba8958f431f4cfb162fd632db5f914b05b6a355a98eaf45cfd/django_constance-4.3.5.tar.gz", hash = "sha256:081177483d272b664cf768deae76fc2fbb0a777076f45620b6fde4d9075ee2b3", size = 181943, upload-time = "2026-03-15T11:23:50.799Z" }
539 wheels = [
540 { url = "https://files.pythonhosted.org/packages/61/aa/3ff4198d02c0cb23c595fcfbed364bcf03b80f9109b7b971eaa34604c349/django_constance-4.3.5-py3-none-any.whl", hash = "sha256:c28f360c2822112772a3e4caf02db758c82cca8de7d0b9f648fef371bf4b8bc6", size = 66907, upload-time = "2026-03-15T11:23:49.166Z" },
541 ]
542
543 [[package]]
544 name = "django-cors-headers"
545 version = "4.9.0"
546 source = { registry = "https://pypi.org/simple" }
547 dependencies = [
548 { name = "asgiref" },
549 { name = "django" },
550 ]
551 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" }
552 wheels = [
553 { 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" },
554 ]
555
556 [[package]]
557 name = "django-health-check"
558 version = "4.2.1"
559 source = { registry = "https://pypi.org/simple" }
560 dependencies = [
561 { name = "django" },
562 { name = "dnspython" },
563 ]
564 sdist = { url = "https://files.pythonhosted.org/packages/85/fb/fd4f1122c7be1ca28eb9c279b6098432b49565d55041dbe96ebcf815d5c2/django_health_check-4.2.1.tar.gz", hash = "sha256:aa05f57d6b01fe502842273aaa944e988b85d1f58e3ea67b6f98c5f9808a530a", size = 21391, upload-time = "2026-03-20T13:38:01.724Z" }
565 wheels = [
566 { url = "https://files.pythonhosted.org/packages/93/05/d30df07b08194f1d89de44ecba867c467e9cc8e047a4cd7682a994a9468b/django_health_check-4.2.1-py3-none-any.whl", hash = "sha256:7216ba208f82f7587dc0ac0fe4c8f8a1c4d0cebbbae46cff6bd89779b378daf3", size = 26435, upload-time = "2026-03-20T13:38:00.557Z" },
567 ]
568
569 [[package]]
570 name = "django-import-export"
571 version = "4.4.0"
572 source = { registry = "https://pypi.org/simple" }
573 dependencies = [
574 { name = "diff-match-patch" },
575 { name = "django" },
576 { name = "tablib" },
577 ]
578 sdist = { url = "https://files.pythonhosted.org/packages/22/26/279bc8e6cb2c83d1b5dcdca07e932207c3352af11c6d305d6964a2d03ccc/django_import_export-4.4.0.tar.gz", hash = "sha256:9900e99c89027594941074fb4cd63a5f2964975e239021765c0f066003fcd412", size = 2237714, upload-time = "2026-01-10T20:57:35.128Z" }
579 wheels = [
580 { url = "https://files.pythonhosted.org/packages/2f/e0/f4aa6d2374cc6b53b23f36bd0d5814e1db2769b25931b9908723fa295bb0/django_import_export-4.4.0-py3-none-any.whl", hash = "sha256:2d9b234c0f024d3377167f4d9c5a506e095c5bad98e06d30700e1d0752829e3d", size = 157449, upload-time = "2026-01-10T20:57:33.141Z" },
581 ]
582
583 [[package]]
584 name = "django-ratelimit"
585 version = "4.1.0"
586 source = { registry = "https://pypi.org/simple" }
587 sdist = { url = "https://files.pythonhosted.org/packages/6f/8f/94038fe739b095aca3e4708ecc8a4e77f1fcfd87bed5d6baff43d4c80bc4/django-ratelimit-4.1.0.tar.gz", hash = "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", size = 11551, upload-time = "2023-07-24T20:34:32.374Z" }
588 wheels = [
589 { url = "https://files.pythonhosted.org/packages/fb/78/2c59b30cd8bc8068d02349acb6aeed5c4e05eb01cdf2107ccd76f2e81487/django_ratelimit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92", size = 11608, upload-time = "2023-07-24T20:34:31.362Z" },
590 ]
591
592 [[package]]
593 name = "django-ses"
594 version = "4.7.2"
595 source = { registry = "https://pypi.org/simple" }
596 dependencies = [
597 { name = "boto3" },
598 { name = "django" },
599 ]
600 sdist = { url = "https://files.pythonhosted.org/packages/7f/25/25838da8e213c9f125b26a25360f0bb8ac57f07c24977451f3e7a0d63ddd/django_ses-4.7.2.tar.gz", hash = "sha256:a36f2af0e4ce060bf36053ed4c94feac1703ea3351e677c6f6421abd01433a35", size = 71828, upload-time = "2026-02-20T19:22:35.078Z" }
601 wheels = [
602 { url = "https://files.pythonhosted.org/packages/84/f2/15d4bd54bd01e68e8a116e0b66243c91cb62a3fa0d780a39d2af6654e8ae/django_ses-4.7.2-py3-none-any.whl", hash = "sha256:f3db567fb6f43c01d7d890f5c991e1ebbfa48220de0be24d497ba6332004abcb", size = 37796, upload-time = "2026-02-20T19:22:32.819Z" },
603 ]
604
605 [[package]]
606 name = "django-simple-history"
607 version = "3.11.0"
608 source = { registry = "https://pypi.org/simple" }
609 dependencies = [
610 { name = "django" },
611 ]
612 sdist = { url = "https://files.pythonhosted.org/packages/a8/11/410049f1454b99a78f719d3403fc89437c2a38ee092e939d5ab8d4846738/django_simple_history-3.11.0.tar.gz", hash = "sha256:2c587479cf2c3071e9aa555d0d11b73676994db4910770958f57659ade2deffe", size = 234862, upload-time = "2025-12-11T13:50:55.022Z" }
613 wheels = [
614 { url = "https://files.pythonhosted.org/packages/6e/c2/e9854a3438cfc80891ab4d3826b7c61a0fe5ba3a4da89104a8f5c9afb5df/django_simple_history-3.11.0-py3-none-any.whl", hash = "sha256:f3c298db49e418ffce7fb709a5e83108452ea2179ec5c4b9232484c25427192a", size = 81868, upload-time = "2025-12-11T13:50:53.71Z" },
615 ]
616
617 [[package]]
618 name = "django-storages"
619 version = "1.14.6"
620 source = { registry = "https://pypi.org/simple" }
621 dependencies = [
622 { name = "django" },
623 ]
624 sdist = { url = "https://files.pythonhosted.org/packages/ff/d6/2e50e378fff0408d558f36c4acffc090f9a641fd6e084af9e54d45307efa/django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9", size = 87587, upload-time = "2025-04-02T02:34:55.103Z" }
625 wheels = [
626 { url = "https://files.pythonhosted.org/packages/1f/21/3cedee63417bc5553eed0c204be478071c9ab208e5e259e97287590194f1/django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9", size = 33095, upload-time = "2025-04-02T02:34:53.291Z" },
627 ]
628
629 [package.optional-dependencies]
630 s3 = [
631 { name = "boto3" },
632 ]
633
634 [[package]]
635 name = "django-timezone-field"
636 version = "7.2.1"
637 source = { registry = "https://pypi.org/simple" }
638 dependencies = [
639 { name = "django" },
640 ]
641 sdist = { url = "https://files.pythonhosted.org/packages/da/05/9b93a66452cdb8a08ab26f08d5766d2332673e659a8b2aeb73f2a904d421/django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e", size = 13096, upload-time = "2025-12-06T23:50:44.591Z" }
642 wheels = [
643 { url = "https://files.pythonhosted.org/packages/41/7f/d885667401515b467f84569c56075bc9add72c9fd425fca51a25f4c997e1/django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44", size = 13284, upload-time = "2025-12-06T23:50:43.302Z" },
644 ]
645
646 [[package]]
647 name = "dnspython"
648 version = "2.8.0"
649 source = { registry = "https://pypi.org/simple" }
650 sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
651 wheels = [
652 { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
653 ]
654
655 [[package]]
656 name = "filelock"
657 version = "3.25.2"
658 source = { registry = "https://pypi.org/simple" }
659 sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
660 wheels = [
661 { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
662 ]
663
664 [[package]]
665 name = "fossilrepo"
666 version = "0.1.0"
667 source = { editable = "." }
668 dependencies = [
669 { name = "boto3" },
670 { name = "celery", extra = ["redis"] },
671 { name liard"
672 version = "4.2.4"
673 version = 1
674 revision = 3
675 requires-python = ">=3.12"
676
677 [[package]]
678 name = "amqp"
679 version = "5.3.1"
680 source = { registry = "https://pypi.org/simple" }
681 dependencies = [
682 { name = "vine" },
683 ]
684 sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
685 wheels = [
686 { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
687 ]
688
689 [[package]]
690 name = "asgiref"
691 version = "3.11.1"
692 source = { registry = "https://pypi.org/simple" }
693 sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4cversio},
694 { name = "django-ses", specifier = ">=4.1" },
695 { name = "django-simple-history", specifier = ">=3.7" },
696 { name = "django-storages", extras = ["s3"], specifier = ">=1.14" },
697 { name = "freezegun", marker = "extra == 'dev'", specifier = ">=1.4" },
698 { name = "gunicorn", specifier = ">=23.0" },
699 { name = "markdown", specifier = ">=3.6" },
700 { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" },
701 { name = "psycopg2-binary", specifier = ">=2.9" },
702 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
703 { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
704 { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
705 { name = "redis", specifier = ">=5.0" },
706 { name = "requests", specifier = ">=2.31" },
707 { name = "rich", specifier = ">=13.0" },
708 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
709 { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
710 { name = "whitenoise", specifier = ">=6.7" },
711 ]
712 provides-extras = ["dev"]
713
714 [[package]]
715 name = "freezegun"
716 version = "1.5.5"
717 source = { registry = "https://pypi.orich size = 342432, upl8e1c137dfebd5759a2ist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
718 wheels = [
719 { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
720 ]
721
722 [[package]]
723 name = "gunicorn"
724 version = "25.2.0"
725 source = { registry = "https://pypi.org/simple" }
726 dependencies = [
727 { name = "packaging" },
728 ]
729 sdist = { url = "https://files.pythonhosted.org/packages/dd/13/dd3f8e40ea3ee907a6cbf3d1f1f81afcc3ecd0087d313baabfe95372f15c/gunicorn-25.2.0.tar.gz", hash = "sha256:10bd7adb36d44945d97d0a1fdf9a0fb086ae9c7b39e56b4dece8555a6bf4a09c", size = 632709, up56:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
730 ]
731
732 [[package]]
733 name = "markdown-it-py"
734 version = "4.0.0"
735 source = { registry = "https://pypi.org/simple" }
736 dependencies = [
737 { name = "mdurl" },
738 ]
739 sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
740 wheels = [
741 { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
742 ]
743
744 [[package]]
745 name = "mdurl"
746 version = "0.1.2"
747 source = { registry = "https://pypi.org/simple" }
748 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" }
749 wheels = [
750 { 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" },
751 ]
752
753 [[package]]
754 name = "msgpack"
755 version = "1.1.2"
756 source = { registry = "https://pypi.org/simple" }
757 sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
758 wheels = [
759 { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
760 { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
761 { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
762 { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
763 { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
764 { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
765 { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
766 { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
767 { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
768 { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
769 { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
770 { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
771 { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
772 { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
773 { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
774 { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
775 { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
776 { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
777 { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
778 { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
779 { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
780 { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
781 { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
782 { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
783 { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
784 { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
785 { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
786 { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
787 { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
788 { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
789 { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
790 { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
791 { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
792 { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
793 { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
794 { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
795 { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
796 { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
797 { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
798 { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
799 ]
800
801 [[package]]
802 name = "packageurl-python"
803 version = "0.17.6"
804 source = { registry = "https://pypi.org/simple" }
805 sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
806 wheels = [
807 { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
808 ]
809
810 [[package]]
811 name = "packaging"
812 version = "26.0"
813 source = { registry = "https://pypi.org/simple" }
814 sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
815 wheels = [
816 { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
817 ]
818
819 [[package]]
820 name = "pip"
821 version = "26.0.1"
822 source = { registry = "https://pypi.org/simple" }
823 sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
824 wheels = [
825 { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
826 ]
827
828 [[package]]
829 name = "pip-api"
830 version = "0.0.34"
831 source = { registry = "https://pypi.org/simple" }
832 dependencies = [
833 { name = "pip" },
834 ]
835 sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
836 wheels = [
837 { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
838 ]
839
840 [[package]]
841 name = "pip-audit"
842 version = "2.10.0"
843 source = { registry = "https://pypi.org/simple" }
844 dependencies = [
845 { name = "cachecontrol", extra = ["filecache"] },
846 { name = "cyclonedx-python-lib" },
847 { name = "packaging" },
848 { name = "pip-api" },
849 { name = "pip-requirements-parser" },
850 { name = "platformdirs" },
851 { name = "requests" },
852 { name = "rich" },
853 { name = "tomli" },
854 { name = "tomli-w" },
855 ]
856 sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" }
857 wheels = [
858 { url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" },
859 ]
860
861 [[package]]
862 name = "pip-requirements-parser"
863 version = "32.0.1"
864 source = { registry = "https://pypi.org/simple" }
865 dependencies = [
866 { name = "packaging" },
867 { name = "pyparsing" },
868 ]
869 sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
870 wheels = [
871 { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
872 ]
873
874 [[package]]
875 name = "platformdirs"
876 version = "4.9.4"
877 source = { registry = "https://pypi.org/simple" }
878 sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
879 wheels = [
880 { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
881 ]
882
883 [[package]]
884 name = "pluggy"
885 version = "1.6.0"
886 source = { registry = "https://pypi.org/simple" }
887 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" }
888 wheels = [
889 { 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" },
890 ]
891
892 [[package]]
893 name = "prompt-toolkit"
894 version = "3.0.52"
895 source = { registry = "https://pypi.org/simple" }
896 dependencies = [
897 { name = "wcwidth" },
898 ]
899 sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
900 wheels = [
901 { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
902 ]
903
904 [[package]]
905 name = "psycopg2-binary"
906 version = "2.9.11"
907 source = { registry = "https://pypi.org/simple" }
908 sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
909 wheels = [
910 { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
911 { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
912 { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
913 { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
914 { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
915 { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
916 { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
917 { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
918 { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
919 { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
920 { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
921 { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
922 { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
923 { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
924 { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
925 { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
926 { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
927 { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
928 { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
929 { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
930 { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
931 { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
932 { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
933 { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
934 { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
935 { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
936 { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
937 { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
938 { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
939 { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
940 { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
941 { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
942 { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
943 ]
944
945 [[package]]
946 name = "py-serializable"
947 version = "2.1.0"
948 source = { registry = "https://pypi.org/simple" }
949 dependencies = [
950 { name = "defusedxml" },
951 ]
952 sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
953 wheels = [
954 { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
955 ]
956
957 [[package]]
958 name = "pycparser"
959 version = "3.0"
960 source = { registry = "https://pypi.org/simple" }
961 sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
962 wheels = [
963 { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
964 ]
965
966 [[package]]
967 name = "pygments"
968 version = "2.19.2"
969 source = { registry = "https://pypi.org/simple" }
970 sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
971 wheels = [
972 { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
973 ]
974
975 [[package]]
976 name = "pyparsing"
977 version = "3.3.2"
978 source = { registry = "https://pypi.org/simple" }
979 sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
980 wheels = [
981 { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
982 ]
983
984 [[package]]
985 name = "pytest"
986 version = "9.0.2"
987 source = { registry = "https://pypi.org/simple" }
988 dependencies = [
989 { name = "colorama", marker = "sys_platform == 'win32'" },
990 { name = "iniconfig" },
991 { name = "packaging" },
992 { name = "pluggy" },
993 { name = "pygments" },
994 ]
995 sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
996 wheels = [
997 { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
998 ]
999
1000 [[package]]
1001 name = "pytest-cov"
1002 version = "7.1.0"
1003 source = { registry = "https://pypi.org/simple" }
1004 dependencies = [
1005 { name = "coverage" },
1006 { name = "pluggy" },
1007 { name = "pytest" },
1008 ]
1009 sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
1010 wheels = [
1011 { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
1012 ]
1013
1014 [[package]]
1015 name = "pytest-django"
1016 version = "4.12.0"
1017 source = { registry = "https://pypi.org/simple" }
1018 dependencies = [
1019 { name = "pytest" },
1020 ]
1021 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" }
1022 wheels = [
1023 { 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" },
1024 ]
1025
1026 [[package]]
1027 name = "python-crontab"
1028 version = "3.3.0"
1029 source = { registry = "https://pypi.org/simple" }
1030 sdist = { url = "https://files.pythonhosted.org/packages/99/7f/c54fb7e70b59844526aa4ae321e927a167678660ab51dda979955eafb89a/python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b", size = 57626, upload-time = "2025-07-13T20:05:35.535Z" }
1031 wheels = [
1032 { url = "https://files.pythonhosted.org/packages/47/42/bb4afa5b088f64092036221843fc989b7db9d9d302494c1f8b024ee78a46/python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884", size = 27533, upload-time = "2025-07-13T20:05:34.266Z" },
1033 ]
1034
1035 [[package]]
1036 name = "python-dateutil"
1037 version = "2.9.0.post0"
1038 source = { registry = "https://pypi.org/simple" }
1039 dependencies = [
1040 { name = "six" },
1041 ]
1042 sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
1043 wheels = [
1044 { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
1045 ]
1046
1047 [[package]]
1048 name = "redis"
1049 version = "6.4.0"
1050 source = { registry = "https://pypi.org/simple" }
1051 sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
1052 wheels = [
1053 { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
1054 ]
1055
1056 [[package]]
1057 name = "requests"
1058 version = "2.33.0"
1059 source = { registry = "https://pypi.org/simple" }
1060 dependencies = [
1061 { name = "certifi" },
1062 { name = "charset-normalizer" },
1063 { name = "idna" },
1064 { name = "urllib3" },
1065 ]
1066 sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
1067 wheels = [
1068 { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
1069 ]
1070
1071 [[package]]
1072 name = "rich"
1073 version = "14.3.3"
1074 source = { registry = "https://pypi.org/simple" }
1075 dependencies = [
1076 { name = "markdown-it-py" },
1077 { name = "pygments" },
1078 ]
1079 sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
1080 wheels = [
1081 { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
1082 ]
1083
1084 [[package]]
1085 name = "ruff"
1086 version = "0.15.7"
1087 source = { registry = "https://pypi.org/simple" }
1088 sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
1089 wheels = [
1090 { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
1091 { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
1092 { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
1093 { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
1094 { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
1095 { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
1096 { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
1097 { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
1098 { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
1099 { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
1100 { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
1101 { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
1102 { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
1103 { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
1104 { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
1105 { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
1106 { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
1107 ]
1108
1109 [[package]]
1110 name = "s3transfer"
1111 version = "0.16.0"
1112 source = { registry = "https://pypi.org/simple" }
1113 dependencies = [
1114 { name = "botocore" },
1115 ]
1116 sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
1117 wheels = [
1118 { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
1119 ]
1120
1121 [[package]]
1122 name = "sentry-sdk"
1123 version = "2.56.0"
1124 source = { registry = "https://pypi.org/simple" }
1125 dependencies = [
1126 { name = "certifi" },
1127 { name = "urllib3" },
1128 ]
1129 sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" }
1130 wheels = [
1131 { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" },
1132 ]
1133
1134 [package.optional-dependencies]
1135 django = [
1136 { name = "django" },
1137 ]
1138
1139 [[package]]
1140 name = "six"
1141 version = "1.17.0"
1142 source = { registry = "https://pypi.org/simple" }
1143 sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
1144 wheels = [
1145 { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
1146 ]
1147
1148 [[package]]
1149 name = "sortedcontainers"
1150 version = "2.4.0"
1151 source = { registry = "https://pypi.org/simple" }
1152 sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
1153 wheels = [
1154 { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
1155 ]
1156
1157 [[package]]
1158 name = "sqlparse"
1159 version = "0.5.5"
1160 source = { registry = "https://pypi.org/simple" }
1161 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" }
1162 wheels = [
1163 { 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" },
1164 ]
1165
1166 [[package]]
1167 name = "tablib"
1168 version = "3.9.0"
1169 source = { registry = "https://pypi.org/simple" }
1170 sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" }
1171 wheels = [
1172 { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" },
1173 ]
1174
1175 [[package]]
1176 name = "tomli"
1177 version = "2.4.1"
1178 source = { registry = "https://pypi.org/simple" }
1179 sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
1180 wheels = [
1181 { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
1182 { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
1183 { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
1184 { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
1185 { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
1186 { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
1187 { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
1188 { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
1189 { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
1190 { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
1191 { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
1192 { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
1193 { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
1194 { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
1195 { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
1196 { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
1197 { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
1198 { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
1199 { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
1200 { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
1201 { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
1202 { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
1203 { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
1204 { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
1205 { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
1206 { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
1207 { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
1208 { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
1209 { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
1210 { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
1211 { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
1212 { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
1213 { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
1214 { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
1215 { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
1216 { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
1217 { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
1218 ]
1219
1220 [[package]]
1221 name = "tomli-w"
1222 version = "1.2.0"
1223 source = { registry = "https://pypi.org/simple" }
1224 sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
1225 wheels = [
1226 { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
1227 ]
1228
1229 [[package]]
1230 name = "typing-extensions"
1231 version = "4.15.0"
1232 source = { registry = "https://pypi.org/simple" }
1233 sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
1234 wheels = [
1235 { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
1236 ]
1237
1238 [[package]]
1239 name = "tzdata"
1240 version = "2025.3"
1241 source = { registry = "https://pypi.org/simple" }
1242 sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
1243 wheels = [
1244 { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
1245 ]
1246
1247 [[package]]
1248 name = "tzlocal"
1249 version = "5.3.1"
1250 source = { registry = "https://pypi.org/simple" }
1251 dependencies = [
1252 { name = "tzdata", marker = "sys_platform == 'win32'" },
1253 ]
1254 sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
1255 wheels = [
1256 { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
1257 ]
1258
1259 [[package]]
1260 name = "urllib3"
1261 version = "2.6.3"
1262 source = { registry = "https://pypi.org/simple" }
1263 sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
1264 wheels = [
1265 { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
1266 ]
1267
1268 [[package]]
1269 name = "vine"
1270 version = "5.1.0"
1271 source = { registry = "https://pypi.org/simple" }
1272 sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
1273 wheels = [
1274 { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
1275 ]
1276
1277 [[package]]
1278 name = "wcwidth"
1279 version = "0.6.0"
1280 source = { registry = "https://pypi.org/simple" }
1281 sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
1282 wheels = [
1283 { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
1284 ]
1285
1286 [[package]]
1287 name = "whitenoise"
1288 version = "6.12.0"
1289 source = { registry = "https://pypi.org/simple" }
1290 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" }
1291 wheels = [
1292 { 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" },
1293 ]
1294 5.1,<6.0" },
1295 { name = "django-celery-beat", specifier = ">=2.7" },
1296 { name = "django-celery-results", specifier = ">=2.5" },
1297 { name = "django-constance", extras = ["database"], specifier = ">=4.1" },
1298 { name = "django-cors-headers", specifier = ">=4.4" },
1299 { name = "django-health-check", specifier = ">=3.18" },
1300 { name = "django-import-export", specifier = ">=4.0" },
1301 { name = "django-ratelimit", specifier = ">=4.1" },
1302 { name = "django-ses", specifier = ">=4.1" },
1303 { name = "django-simple-history", specifier = ">=3.7" },
1304 { name = "django-storages", extras = ["s3"], specifier = ">=1.14" },
1305 { name = "freezegun", marker = "extra == 'dev'", specifier = ">=1.4" },
1306 { name = "gunicorn", specifier = ">=23.0" },
1307 { name = "markdown", specifier = ">=3.6" },
1308 { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" },
1309 { name = "psycopg2-binary", specifier = ">=2.9" },
1310 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
1311 { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
1312 { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
1313 { name = "redis", specifier = ">=5.0" },
1314 { name = "requests", specifier = ">=2.31" },
1315 { name = "rich", specifier = ">=13.0" },
1316 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
1317 { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
1318 { name = "whitenoise", specifier = ">=6.7" },
1319 ]
1320 provides-extras = ["dev"]
1321
1322 [[package]]
1323 name = "freezegun"
1324 version = "1.5.5"
1325 source = { registry = "https://pypi.org/simple" }
1326 dependencies = [
1327 { name = "python-dateutil" },
1328 ]
1329 sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
1330 wheels = [
1331 { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
1332 ]
1333
1334 [[package]]
1335 name = "gunicorn"
1336 version = "25.2.0"
1337 source = { registry = "https://pypi.org/simple" }
1338 dependencies = [
1339 { name = "packaging" },
1340 ]
1341 sdist = { url = "https://files.pythonhosted.org/packages/dd/13/dd3f8e40ea3ee907a6cbf3d1f1f81afcc3ecd0087d313baabfe95372f15c/gunicorn-25.2.0.tar.gz", hash = "sha256:10bd7adb36d44945d97d0a1fdf9a0fb086ae9c7b39e56b4dece8555a6bf4a09c", size = 632709, upload-time = "2026-03-24T22:49:54.433Z" }
1342 wheels = [
1343 { url = "https://files.pythonhosted.org/packages/11/53/fb024445837e02cd5cf989cf349bfac6f3f433c05184ea5d49c8ade751c6/gunicorn-25.2.0-py3-none-any.whl", hash = "sha256:88f5b444d0055bf298435384af7294f325e2273fd37ba9f9ff7b98e0a1e5dfdc", size = 211659, upload-time = "2026-03-24T22:49:52.528Z" },
1344 ]
1345
1346 [[package]]
1347 name = "idna"
1348 version = "3.11"
1349 source = { registry = "https://pypi.org/simple" }
1350 sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c0

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button