Compare commits

..

17 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c736799a2a Initial plan 2026-02-03 19:59:12 +00:00
Trenton H
c57a14299d Merge branch 'dev' into feature-date-parse-plugin 2026-02-03 10:30:35 -08:00
Trenton H
460da8a4d5 Merge branch 'dev' into feature-date-parse-plugin 2026-02-02 20:27:10 -08:00
Trenton H
bb6ad8aef6 Restores using OcrConfig for the moment 2026-02-02 13:32:12 -08:00
Trenton H
ddf1d0c3ae Defines the public interface better 2026-02-02 13:26:24 -08:00
Trenton H
3c7cb73d32 Draft up documentation on how to create a plugin 2026-02-02 13:26:24 -08:00
Trenton H
f17090c9c6 One more tuple here 2026-02-02 13:26:24 -08:00
Trenton H
481e518445 Resolves Sonarr issues 2026-02-02 13:26:24 -08:00
Trenton H
c4a730baaa To enable cleanup, use as a context manager 2026-02-02 13:26:24 -08:00
Trenton H
1cc38a45ad Restores environment 2026-02-02 13:26:24 -08:00
Trenton H
a2c3bf4c29 Adds example type checking configuration, with a default broard ignore and a tight scoped check 2026-02-02 13:26:24 -08:00
Trenton H
332d0b0a41 Two more missed 2026-02-02 13:26:24 -08:00
Trenton H
666113ad96 Got to update the tests too 2026-02-02 13:26:24 -08:00
Trenton H
e7ac73135b Change the contract, just take the actual filename, not the file path 2026-02-02 13:26:24 -08:00
Trenton H
c55ad50b52 Forgot the marker again 2026-02-02 13:26:24 -08:00
Trenton H
27c793c55e Hooks up the class and fixes up the old testing. Includes ocr to date parser conversion we now do 2026-02-02 13:26:22 -08:00
Trenton H
9c334c4deb Copy over the code and tests, to see if this even works 2026-02-02 13:25:59 -08:00
77 changed files with 1437 additions and 4991 deletions

View File

@@ -91,12 +91,12 @@ Additional tasks are available for common maintenance operations:
## Committing from the Host Machine ## Committing from the Host Machine
The DevContainer automatically installs Git pre-commit hooks during setup. However, these hooks are configured for use inside the container. The DevContainer automatically installs pre-commit hooks during setup. However, these hooks are configured for use inside the container.
If you want to commit changes from your host machine (outside the DevContainer), you need to set up prek on your host. This installs it as a standalone tool. If you want to commit changes from your host machine (outside the DevContainer), you need to set up pre-commit on your host. This installs it as a standalone tool.
```bash ```bash
uv tool install prek && prek install uv tool install pre-commit && pre-commit install
``` ```
After this, you can commit either from inside the DevContainer or from your host machine. After this, you can commit either from inside the DevContainer or from your host machine.

View File

@@ -7,7 +7,7 @@
"containerEnv": { "containerEnv": {
"UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache" "UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache"
}, },
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run prek install'", "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'",
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [

View File

@@ -28,4 +28,3 @@
./resources ./resources
# Other stuff # Other stuff
**/*.drawio.png **/*.drawio.png
.mypy_baseline

View File

@@ -37,6 +37,6 @@ NOTE: PRs that do not address the following will not be merged, please do not sk
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes. - [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers. - [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development). - [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
- [ ] I have run all Git `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks). - [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
- [ ] I have made corresponding changes to the documentation as needed. - [ ] I have made corresponding changes to the documentation as needed.
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR. - [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.

View File

@@ -47,7 +47,7 @@ updates:
- "*pytest*" - "*pytest*"
- "ruff" - "ruff"
- "mkdocs-material" - "mkdocs-material"
- "prek*" - "pre-commit*"
# Django & DRF Ecosystem # Django & DRF Ecosystem
django-ecosystem: django-ecosystem:
patterns: patterns:

View File

@@ -99,47 +99,3 @@ jobs:
run: | run: |
docker compose --file docker/compose/docker-compose.ci-test.yml logs docker compose --file docker/compose/docker-compose.ci-test.yml logs
docker compose --file docker/compose/docker-compose.ci-test.yml down docker compose --file docker/compose/docker-compose.ci-test.yml down
typing:
name: Check project typing
runs-on: ubuntu-24.04
env:
DEFAULT_PYTHON: "3.12"
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@v7.2.1
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install Python dependencies
run: |
uv sync \
--python ${{ steps.setup-python.outputs.python-version }} \
--group testing \
--group typing \
--frozen
- name: List installed Python dependencies
run: |
uv pip list
- name: Cache Mypy
uses: actions/cache@v5.0.3
with:
path: .mypy_cache
# Keyed by OS, Python version, and dependency hashes
key: ${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}
restore-keys: |
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
${{ runner.os }}-mypy-
- name: Check typing
run: |
uv run mypy \
--show-error-codes \
--warn-unused-configs \
src/ | uv run mypy-baseline filter

View File

@@ -10,15 +10,15 @@ concurrency:
group: lint-${{ github.event.pull_request.number || github.ref }} group: lint-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
lint: pre-commit:
name: Linting via prek name: Pre-commit Checks
runs-on: ubuntu-slim runs-on: ubuntu-24.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6.0.2 uses: actions/checkout@v6
- name: Install Python - name: Install Python
uses: actions/setup-python@v6.2.0 uses: actions/setup-python@v6
with: with:
python-version: "3.14" python-version: "3.11"
- name: Run prek - name: Run pre-commit
uses: j178/prek-action@v1.1.0 uses: pre-commit/action@v3.0.1

View File

@@ -211,7 +211,7 @@ jobs:
uv run \ uv run \
--python ${{ steps.setup-python.outputs.python-version }} \ --python ${{ steps.setup-python.outputs.python-version }} \
--dev \ --dev \
prek run --files changelog.md || true pre-commit run --files changelog.md || true
git config --global user.name "github-actions" git config --global user.name "github-actions"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
# This file configures pre-commit hooks. # This file configures pre-commit hooks.
# See https://pre-commit.com/ for general information # See https://pre-commit.com/ for general information
# See https://pre-commit.com/hooks.html for a listing of possible hooks # See https://pre-commit.com/hooks.html for a listing of possible hooks
# We actually run via https://github.com/j178/prek which is compatible
repos: repos:
# General hooks # General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -50,12 +49,12 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0' - 'prettier-plugin-organize-imports@4.1.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.0 rev: v0.14.14
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.12.1" rev: "v2.11.1"
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
# Dockerfile hooks # Dockerfile hooks

View File

@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.9.29-python3.12-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.9.26-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6

View File

@@ -4,7 +4,7 @@
# correct networking for the tests # correct networking for the tests
services: services:
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.26 image: docker.io/gotenberg/gotenberg:8.25
hostname: gotenberg hostname: gotenberg
container_name: gotenberg container_name: gotenberg
network_mode: host network_mode: host
@@ -35,7 +35,7 @@ services:
- "3143:3143" # IMAP - "3143:3143" # IMAP
restart: unless-stopped restart: unless-stopped
nginx: nginx:
image: docker.io/nginx:1.29.5-alpine image: docker.io/nginx:1.29-alpine
hostname: nginx hostname: nginx
container_name: nginx container_name: nginx
ports: ports:

View File

@@ -72,7 +72,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.26 image: docker.io/gotenberg/gotenberg:8.25
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.

View File

@@ -66,7 +66,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.26 image: docker.io/gotenberg/gotenberg:8.25
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.

View File

@@ -55,7 +55,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.26 image: docker.io/gotenberg/gotenberg:8.25
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.

View File

@@ -81,7 +81,7 @@ first-time setup.
5. Install pre-commit hooks: 5. Install pre-commit hooks:
```bash ```bash
$ uv run prek install $ uv run pre-commit install
``` ```
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance: 6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
@@ -217,7 +217,7 @@ commit. See [above](#code-formatting-with-pre-commit-hooks) for installation ins
command such as command such as
```bash ```bash
$ git ls-files -- '*.ts' | xargs prek run prettier --files $ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
``` ```
Front end testing uses Jest and Playwright. Unit tests and e2e tests, Front end testing uses Jest and Playwright. Unit tests and e2e tests,

View File

@@ -27,9 +27,9 @@ dependencies = [
# WARNING: django does not use semver. # WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.10", "django~=5.2.10",
"django-allauth[mfa,socialaccount]~=65.14.0", "django-allauth[mfa,socialaccount]~=65.13.1",
"django-auditlog~=3.4.1", "django-auditlog~=3.4.1",
"django-cachalot~=2.9.0", "django-cachalot~=2.8.0",
"django-celery-results~=2.6.0", "django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0", "django-cors-headers~=4.9.0",
@@ -42,7 +42,7 @@ dependencies = [
"djangorestframework~=3.16", "djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0", "djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.28", "drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.1.1", "drf-spectacular-sidecar~=2025.10.1",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
"faiss-cpu>=1.10", "faiss-cpu>=1.10",
"filelock~=3.20.0", "filelock~=3.20.0",
@@ -76,7 +76,7 @@ dependencies = [
"sentence-transformers>=4.1", "sentence-transformers>=4.1",
"setproctitle~=1.3.4", "setproctitle~=1.3.4",
"tika-client~=0.10.0", "tika-client~=0.10.0",
"torch~=2.10.0", "torch~=2.9.1",
"tqdm~=4.67.1", "tqdm~=4.67.1",
"watchfiles>=1.1.1", "watchfiles>=1.1.1",
"whitenoise~=6.11", "whitenoise~=6.11",
@@ -94,7 +94,7 @@ optional-dependencies.postgres = [
"psycopg-pool==3.3", "psycopg-pool==3.3",
] ]
optional-dependencies.webserver = [ optional-dependencies.webserver = [
"granian[uvloop]~=2.7.0", "granian[uvloop]~=2.6.0",
] ]
[dependency-groups] [dependency-groups]
@@ -127,8 +127,9 @@ testing = [
] ]
lint = [ lint = [
"prek~=0.3.0", "pre-commit~=4.5.1",
"ruff~=0.15.0", "pre-commit-uv~=4.2.0",
"ruff~=0.14.0",
] ]
typing = [ typing = [
@@ -137,11 +138,8 @@ typing = [
"django-stubs[compatible-mypy]", "django-stubs[compatible-mypy]",
"djangorestframework-stubs[compatible-mypy]", "djangorestframework-stubs[compatible-mypy]",
"lxml-stubs", "lxml-stubs",
"microsoft-python-type-stubs @ git+https://github.com/microsoft/python-type-stubs.git",
"mypy", "mypy",
"mypy-baseline",
"types-bleach", "types-bleach",
"types-channels",
"types-colorama", "types-colorama",
"types-dateparser", "types-dateparser",
"types-markdown", "types-markdown",
@@ -161,11 +159,6 @@ environments = [
"sys_platform == 'linux'", "sys_platform == 'linux'",
] ]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.uv.sources] [tool.uv.sources]
# Markers are chosen to select these almost exclusively when building the Docker image # Markers are chosen to select these almost exclusively when building the Docker image
psycopg-c = [ psycopg-c = [
@@ -181,6 +174,11 @@ torch = [
{ index = "pytorch-cpu" }, { index = "pytorch-cpu" },
] ]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.ruff] [tool.ruff]
target-version = "py310" target-version = "py310"
line-length = 88 line-length = 88
@@ -315,13 +313,6 @@ markers = [
PAPERLESS_DISABLE_DBHANDLER = "true" PAPERLESS_DISABLE_DBHANDLER = "true"
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache" PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
[tool.coverage.report]
exclude_also = [
"if settings.AUDIT_LOG_ENABLED:",
"if AUDIT_LOG_ENABLED:",
"if TYPE_CHECKING:",
]
[tool.coverage.run] [tool.coverage.run]
source = [ source = [
"src/", "src/",
@@ -333,8 +324,19 @@ omit = [
"paperless/auth.py", "paperless/auth.py",
] ]
[tool.coverage.report]
exclude_also = [
"if settings.AUDIT_LOG_ENABLED:",
"if AUDIT_LOG_ENABLED:",
"if TYPE_CHECKING:",
]
[tool.mypy] [tool.mypy]
mypy_path = "src" mypy_path = "src"
files = [
"src/documents/plugins/date_parsing",
"src/documents/tests/date_parsing",
]
plugins = [ plugins = [
"mypy_django_plugin.main", "mypy_django_plugin.main",
"mypy_drf_plugin.main", "mypy_drf_plugin.main",
@@ -346,10 +348,28 @@ disallow_untyped_defs = true
warn_redundant_casts = true warn_redundant_casts = true
warn_unused_ignores = true warn_unused_ignores = true
# This prevents errors from imports, but allows type-checking logic to work
follow_imports = "silent"
[[tool.mypy.overrides]]
module = [
"documents.*",
"paperless.*",
"paperless_ai.*",
"paperless_mail.*",
"paperless_tesseract.*",
"paperless_remote.*",
"paperless_text.*",
"paperless_tika.*",
]
ignore_errors = true
[[tool.mypy.overrides]]
module = [
"documents.plugins.date_parsing.*",
"documents.tests.date_parsing.*",
]
ignore_errors = false
[tool.django-stubs] [tool.django-stubs]
django_settings_module = "paperless.settings" django_settings_module = "paperless.settings"
[tool.mypy-baseline]
baseline_path = ".mypy-baseline.txt"
sort_baseline = true
ignore_categories = [ "note" ]

View File

@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
test('test slim sidebar', async ({ page }) => { test('test slim sidebar', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' }) await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await page.goto('/dashboard') await page.goto('/dashboard')
await page.locator('.sidebar-slim-toggler').click() await page.locator('#sidebarMenu').getByRole('button').click()
await expect( await expect(
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard') page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
).toBeHidden() ).toBeHidden()
await page.locator('.sidebar-slim-toggler').click() await page.locator('#sidebarMenu').getByRole('button').click()
await expect( await expect(
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard') page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
).toBeVisible() ).toBeVisible()

View File

@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' }) await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard') await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Correspondents' })
).not.toBeAttached() ).not.toBeAttached()
await page.goto('/attributes/correspondents') await page.goto('/correspondents')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i /You don't have permissions to do that/i
) )
@@ -44,10 +44,8 @@ test('should not allow user to view correspondents', async ({ page }) => {
test('should not allow user to view tags', async ({ page }) => { test('should not allow user to view tags', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' }) await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard') await page.goto('/dashboard')
await expect( await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
page.getByRole('link', { name: 'Attributes' }) await page.goto('/tags')
).not.toBeAttached()
await page.goto('/attributes/tags')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i /You don't have permissions to do that/i
) )
@@ -57,9 +55,9 @@ test('should not allow user to view document types', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' }) await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard') await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Document Types' })
).not.toBeAttached() ).not.toBeAttached()
await page.goto('/attributes/documenttypes') await page.goto('/documenttypes')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i /You don't have permissions to do that/i
) )
@@ -69,9 +67,9 @@ test('should not allow user to view storage paths', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' }) await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard') await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Storage Paths' })
).not.toBeAttached() ).not.toBeAttached()
await page.goto('/attributes/storagepaths') await page.goto('/storagepaths')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i /You don't have permissions to do that/i
) )

View File

@@ -5,14 +5,14 @@
<trans-unit id="ngb.alert.close" datatype="html"> <trans-unit id="ngb.alert.close" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/alert/alert.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.slide-number" datatype="html"> <trans-unit id="ngb.carousel.slide-number" datatype="html">
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">131,135</context> <context context-type="linenumber">131,135</context>
</context-group> </context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note> <note priority="1" from="description">Currently selected slide number read by screen reader</note>
@@ -20,114 +20,114 @@
<trans-unit id="ngb.carousel.previous" datatype="html"> <trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">159,162</context> <context context-type="linenumber">159,162</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.next" datatype="html"> <trans-unit id="ngb.carousel.next" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">202,203</context> <context context-type="linenumber">202,203</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-month" datatype="html"> <trans-unit id="ngb.datepicker.select-month" datatype="html">
<source>Select month</source> <source>Select month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-year" datatype="html"> <trans-unit id="ngb.datepicker.select-year" datatype="html">
<source>Select year</source> <source>Select year</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.previous-month" datatype="html"> <trans-unit id="ngb.datepicker.previous-month" datatype="html">
<source>Previous month</source> <source>Previous month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">83,85</context> <context context-type="linenumber">83,85</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.next-month" datatype="html"> <trans-unit id="ngb.datepicker.next-month" datatype="html">
<source>Next month</source> <source>Next month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first" datatype="html"> <trans-unit id="ngb.pagination.first" datatype="html">
<source>««</source> <source>««</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous" datatype="html"> <trans-unit id="ngb.pagination.previous" datatype="html">
<source>«</source> <source>«</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html"> <trans-unit id="ngb.pagination.next" datatype="html">
<source>»</source> <source>»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last" datatype="html"> <trans-unit id="ngb.pagination.last" datatype="html">
<source>»»</source> <source>»»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html"> <trans-unit id="ngb.pagination.first-aria" datatype="html">
<source>First</source> <source>First</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html"> <trans-unit id="ngb.pagination.previous-aria" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html"> <trans-unit id="ngb.pagination.next-aria" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html"> <trans-unit id="ngb.pagination.last-aria" datatype="html">
<source>Last</source> <source>Last</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -135,105 +135,105 @@
<source><x id="INTERPOLATION" equiv-text="barConfig); <source><x id="INTERPOLATION" equiv-text="barConfig);
pu"/></source> pu"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/progressbar/progressbar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">41,42</context> <context context-type="linenumber">41,42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html"> <trans-unit id="ngb.timepicker.HH" datatype="html">
<source>HH</source> <source>HH</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.hours" datatype="html"> <trans-unit id="ngb.timepicker.hours" datatype="html">
<source>Hours</source> <source>Hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html"> <trans-unit id="ngb.timepicker.MM" datatype="html">
<source>MM</source> <source>MM</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.minutes" datatype="html"> <trans-unit id="ngb.timepicker.minutes" datatype="html">
<source>Minutes</source> <source>Minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html"> <trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<source>Increment hours</source> <source>Increment hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> <trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<source>Decrement hours</source> <source>Decrement hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> <trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<source>Increment minutes</source> <source>Increment minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<source>Decrement minutes</source> <source>Decrement minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.SS" datatype="html"> <trans-unit id="ngb.timepicker.SS" datatype="html">
<source>SS</source> <source>SS</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.seconds" datatype="html"> <trans-unit id="ngb.timepicker.seconds" datatype="html">
<source>Seconds</source> <source>Seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> <trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<source>Increment seconds</source> <source>Increment seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<source>Decrement seconds</source> <source>Decrement seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.PM" datatype="html"> <trans-unit id="ngb.timepicker.PM" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html"> <trans-unit id="ngb.toast.close-aria" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/toast/toast-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/toast/toast-config.ts</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -241,18 +241,18 @@
<source>Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</source> <source>Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">95</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">99</context> <context context-type="linenumber">104</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1931214133925051574" datatype="html"> <trans-unit id="1931214133925051574" datatype="html">
<source>Open document</source> <source>Open document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">97</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
@@ -279,21 +279,21 @@
<source>Could not add <x id="PH" equiv-text="status.filename"/>: <x id="PH_1" equiv-text="status.message"/></source> <source>Could not add <x id="PH" equiv-text="status.filename"/>: <x id="PH_1" equiv-text="status.message"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">119</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1218124467712564468" datatype="html"> <trans-unit id="1218124467712564468" datatype="html">
<source>Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</source> <source>Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">129</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6570363013146073520" datatype="html"> <trans-unit id="6570363013146073520" datatype="html">
<source>Dashboard</source> <source>Dashboard</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">141</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
@@ -312,7 +312,7 @@
<source>Documents</source> <source>Documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">147</context> <context context-type="linenumber">152</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
@@ -367,7 +367,7 @@
<source>Settings</source> <source>Settings</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">164</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
@@ -386,53 +386,78 @@
<context context-type="linenumber">260</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2501522447884928778" datatype="html">
<source>Prev</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
</trans-unit>
<trans-unit id="3885497195825665706" datatype="html">
<source>Next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">171</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">109</context>
</context-group>
</trans-unit>
<trans-unit id="1241348629231510663" datatype="html">
<source>End</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="5890330709052835856" datatype="html"> <trans-unit id="5890330709052835856" datatype="html">
<source>The dashboard can be used to show saved views, such as an &apos;Inbox&apos;. Views are found under Manage &gt; Saved Views once you have created some.</source> <source>The dashboard can be used to show saved views, such as an &apos;Inbox&apos;. Views are found under Manage &gt; Saved Views once you have created some.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">178</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9075755296812854717" datatype="html"> <trans-unit id="9075755296812854717" datatype="html">
<source>Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.</source> <source>Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">175</context> <context context-type="linenumber">185</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7495498057594070122" datatype="html"> <trans-unit id="7495498057594070122" datatype="html">
<source>The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.</source> <source>The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1334220418719920556" datatype="html"> <trans-unit id="1334220418719920556" datatype="html">
<source>The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.</source> <source>The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">187</context> <context context-type="linenumber">197</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5427326625898532358" datatype="html"> <trans-unit id="5427326625898532358" datatype="html">
<source>Any combination of filters can be saved as a &apos;view&apos; which can then be displayed on the dashboard and / or sidebar.</source> <source>Any combination of filters can be saved as a &apos;view&apos; which can then be displayed on the dashboard and / or sidebar.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">193</context> <context context-type="linenumber">203</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2804886236408698479" datatype="html"> <trans-unit id="2804886236408698479" datatype="html">
<source>Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.</source> <source>Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">198</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7851939076947092983" datatype="html"> <trans-unit id="7851939076947092983" datatype="html">
<source>Manage e-mail accounts and rules for automatically importing documents.</source> <source>Manage e-mail accounts and rules for automatically importing documents.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">206</context> <context context-type="linenumber">216</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
@@ -443,14 +468,14 @@
<source>Workflows give you more control over the document pipeline.</source> <source>Workflows give you more control over the document pipeline.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">214</context> <context context-type="linenumber">224</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4680387114119209483" datatype="html"> <trans-unit id="4680387114119209483" datatype="html">
<source>File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.</source> <source>File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">222</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@@ -461,28 +486,28 @@
<source>Check out the settings for various tweaks to the web app.</source> <source>Check out the settings for various tweaks to the web app.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">240</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7172877665285340082" datatype="html"> <trans-unit id="7172877665285340082" datatype="html">
<source>Thank you! 🙏</source> <source>Thank you! 🙏</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">238</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7354947513482088740" datatype="html"> <trans-unit id="7354947513482088740" datatype="html">
<source>There are &lt;em&gt;tons&lt;/em&gt; more features and info we didn&apos;t cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.</source> <source>There are &lt;em&gt;tons&lt;/em&gt; more features and info we didn&apos;t cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">250</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4270528545616947218" datatype="html"> <trans-unit id="4270528545616947218" datatype="html">
<source>Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!</source> <source>Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">242</context> <context context-type="linenumber">252</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9063918187161876141" datatype="html"> <trans-unit id="9063918187161876141" datatype="html">
@@ -7435,17 +7460,6 @@
<context context-type="linenumber">106</context> <context context-type="linenumber">106</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3885497195825665706" datatype="html">
<source>Next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">109</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">405</context>
</context-group>
</trans-unit>
<trans-unit id="5028777105388019087" datatype="html"> <trans-unit id="5028777105388019087" datatype="html">
<source>Details</source> <source>Details</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -11230,20 +11244,6 @@
<context context-type="linenumber">39</context> <context context-type="linenumber">39</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2501522447884928778" datatype="html">
<source>Prev</source>
<context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">404</context>
</context-group>
</trans-unit>
<trans-unit id="1241348629231510663" datatype="html">
<source>End</source>
<context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">406</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -11,15 +11,15 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^21.1.3", "@angular/cdk": "^21.1.2",
"@angular/common": "~21.1.3", "@angular/common": "~21.1.2",
"@angular/compiler": "~21.1.3", "@angular/compiler": "~21.1.2",
"@angular/core": "~21.1.3", "@angular/core": "~21.1.2",
"@angular/forms": "~21.1.3", "@angular/forms": "~21.1.2",
"@angular/localize": "~21.1.3", "@angular/localize": "~21.1.2",
"@angular/platform-browser": "~21.1.3", "@angular/platform-browser": "~21.1.2",
"@angular/platform-browser-dynamic": "~21.1.3", "@angular/platform-browser-dynamic": "~21.1.2",
"@angular/router": "~21.1.3", "@angular/router": "~21.1.2",
"@ng-bootstrap/ng-bootstrap": "^20.0.0", "@ng-bootstrap/ng-bootstrap": "^20.0.0",
"@ng-select/ng-select": "^21.2.0", "@ng-select/ng-select": "^21.2.0",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
@@ -32,7 +32,7 @@
"ngx-color": "^10.1.0", "ngx-color": "^10.1.0",
"ngx-cookie-service": "^21.1.0", "ngx-cookie-service": "^21.1.0",
"ngx-device-detector": "^11.0.0", "ngx-device-detector": "^11.0.0",
"ngx-ui-tour-ng-bootstrap": "^18.0.0", "ngx-ui-tour-ng-bootstrap": "^17.0.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0", "utif": "^3.1.0",
@@ -42,20 +42,20 @@
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^21.0.3", "@angular-builders/custom-webpack": "^21.0.3",
"@angular-builders/jest": "^21.0.3", "@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.1.3", "@angular-devkit/core": "^21.1.2",
"@angular-devkit/schematics": "^21.1.3", "@angular-devkit/schematics": "^21.1.2",
"@angular-eslint/builder": "21.2.0", "@angular-eslint/builder": "21.2.0",
"@angular-eslint/eslint-plugin": "21.2.0", "@angular-eslint/eslint-plugin": "21.2.0",
"@angular-eslint/eslint-plugin-template": "21.2.0", "@angular-eslint/eslint-plugin-template": "21.2.0",
"@angular-eslint/schematics": "21.2.0", "@angular-eslint/schematics": "21.2.0",
"@angular-eslint/template-parser": "21.2.0", "@angular-eslint/template-parser": "21.2.0",
"@angular/build": "^21.1.3", "@angular/build": "^21.1.2",
"@angular/cli": "~21.1.3", "@angular/cli": "~21.1.2",
"@angular/compiler-cli": "~21.1.3", "@angular/compiler-cli": "~21.1.2",
"@codecov/webpack-plugin": "^1.9.1", "@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.2.1", "@types/node": "^25.2.0",
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0", "@typescript-eslint/parser": "^8.54.0",
"@typescript-eslint/utils": "^8.54.0", "@typescript-eslint/utils": "^8.54.0",
@@ -68,7 +68,7 @@
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"webpack": "^5.105.0" "webpack": "^5.103.0"
}, },
"packageManager": "pnpm@10.17.1", "packageManager": "pnpm@10.17.1",
"pnpm": { "pnpm": {

1149
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,13 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component' import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component' import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component' import { DocumentListComponent } from './components/document-list/document-list.component'
import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component' import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { MailComponent } from './components/manage/mail/mail.component' import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component' import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component' import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { NotFoundComponent } from './components/not-found/not-found.component' import { NotFoundComponent } from './components/not-found/not-found.component'
import { DirtyDocGuard } from './guards/dirty-doc.guard' import { DirtyDocGuard } from './guards/dirty-doc.guard'
@@ -101,77 +105,53 @@ export const routes: Routes = [
componentName: 'DocumentAsnComponent', componentName: 'DocumentAsnComponent',
}, },
}, },
{
path: 'attributes',
component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
{ action: PermissionAction.View, type: PermissionType.StoragePath },
{ action: PermissionAction.View, type: PermissionType.CustomField },
],
componentName: 'DocumentAttributesComponent',
},
},
{
path: 'attributes/:section',
component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
{ action: PermissionAction.View, type: PermissionType.StoragePath },
{ action: PermissionAction.View, type: PermissionType.CustomField },
],
componentName: 'DocumentAttributesComponent',
},
},
{
path: 'documentproperties',
redirectTo: '/attributes',
pathMatch: 'full',
},
{
path: 'documentproperties/:section',
redirectTo: '/attributes/:section',
pathMatch: 'full',
},
{ {
path: 'tags', path: 'tags',
redirectTo: '/attributes/tags', component: TagListComponent,
pathMatch: 'full', canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Tag,
},
componentName: 'TagListComponent',
}, },
{
path: 'correspondents',
redirectTo: '/attributes/correspondents',
pathMatch: 'full',
}, },
{ {
path: 'documenttypes', path: 'documenttypes',
redirectTo: '/attributes/documenttypes', component: DocumentTypeListComponent,
pathMatch: 'full', canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
componentName: 'DocumentTypeListComponent',
},
},
{
path: 'correspondents',
component: CorrespondentListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
componentName: 'CorrespondentListComponent',
},
}, },
{ {
path: 'storagepaths', path: 'storagepaths',
redirectTo: '/attributes/storagepaths', component: StoragePathListComponent,
pathMatch: 'full', canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.StoragePath,
},
componentName: 'StoragePathListComponent',
},
}, },
{ {
path: 'logs', path: 'logs',
@@ -259,8 +239,15 @@ export const routes: Routes = [
}, },
{ {
path: 'customfields', path: 'customfields',
redirectTo: '/attributes/customfields', component: CustomFieldsComponent,
pathMatch: 'full', canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.CustomField,
},
componentName: 'CustomFieldsComponent',
},
}, },
{ {
path: 'workflows', path: 'workflows',

View File

@@ -9,11 +9,7 @@ import {
import { Router, RouterModule } from '@angular/router' import { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
provideUiTour,
TourNgBootstrap,
TourService,
} from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { routes } from './app-routing.module' import { routes } from './app-routing.module'
import { AppComponent } from './app.component' import { AppComponent } from './app.component'
@@ -44,12 +40,12 @@ describe('AppComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TourNgBootstrapModule,
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
NgbModalModule, NgbModalModule,
AppComponent, AppComponent,
ToastsComponent, ToastsComponent,
FileDropComponent, FileDropComponent,
TourNgBootstrap,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [ providers: [
@@ -57,7 +53,6 @@ describe('AppComponent', () => {
DirtySavedViewGuard, DirtySavedViewGuard,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -1,6 +1,6 @@
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core' import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router, RouterOutlet } from '@angular/router' import { Router, RouterOutlet } from '@angular/router'
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { first, Subscription } from 'rxjs' import { first, Subscription } from 'rxjs'
import { ToastsComponent } from './components/common/toasts/toasts.component' import { ToastsComponent } from './components/common/toasts/toasts.component'
import { FileDropComponent } from './components/file-drop/file-drop.component' import { FileDropComponent } from './components/file-drop/file-drop.component'
@@ -21,7 +21,12 @@ import { WebsocketStatusService } from './services/websocket-status.service'
selector: 'pngx-root', selector: 'pngx-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
imports: [FileDropComponent, ToastsComponent, TourNgBootstrap, RouterOutlet], imports: [
FileDropComponent,
ToastsComponent,
TourNgBootstrapModule,
RouterOutlet,
],
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
private settings = inject(SettingsService) private settings = inject(SettingsService)
@@ -162,7 +167,12 @@ export class AppComponent implements OnInit, OnDestroy {
}) })
} }
this.tourService.initialize([ const prevBtnTitle = $localize`Prev`
const nextBtnTitle = $localize`Next`
const endBtnTitle = $localize`End`
this.tourService.initialize(
[
{ {
anchorId: 'tour.dashboard', anchorId: 'tour.dashboard',
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`, content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
@@ -195,8 +205,8 @@ export class AppComponent implements OnInit, OnDestroy {
}, },
{ {
anchorId: 'tour.tags', anchorId: 'tour.tags',
content: $localize`Attributes like tags, correspondents, document types, storage paths and custom fields can all be managed here. They can also be created from the document edit view.`, content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
route: '/attributes/tags', route: '/tags',
backdropConfig: { backdropConfig: {
offset: 0, offset: 0,
}, },
@@ -246,7 +256,19 @@ export class AppComponent implements OnInit, OnDestroy {
offset: 0, offset: 0,
}, },
}, },
]) ],
{
enableBackdrop: true,
backdropConfig: {
offset: 10,
},
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
isOptional: true,
useLegacyTitle: true,
}
)
this.tourService.start$.subscribe(() => { this.tourService.start$.subscribe(() => {
this.renderer.addClass(document.body, 'tour-active') this.renderer.addClass(document.body, 'tour-active')

View File

@@ -16,7 +16,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { import {
@@ -148,7 +147,6 @@ describe('SettingsComponent', () => {
PermissionsGuard, PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -175,60 +175,44 @@
<span i18n>Manage</span> <span i18n>Manage</span>
</h6> </h6>
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
@if (canManageAttributes) { <li class="nav-item app-link"
<li class="nav-item app-link" tourAnchor="tour.tags"> *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<div class="d-flex align-items-center attributes-row"> <a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="stack"></i-bs><span>&nbsp;<ng-container i18n>Attributes</ng-container></span>
</a>
@if (!slimSidebarEnabled) {
<button
type="button"
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
(click)="toggleAttributesSections($event)"
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
i18n-aria-label
>
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
</button>
}
</div>
<div
class="attributes-submenu ms-2"
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
>
<ul class="nav flex-column">
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-1" name="tags"></i-bs><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-1" name="person"></i-bs><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span> <i-bs class="me-1" name="person"></i-bs><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()"> tourAnchor="tour.tags">
<i-bs class="me-1" name="hash"></i-bs><span>&nbsp;<ng-container i18n>Document types</ng-container></span> <a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="tags"></i-bs><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item app-link"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="hash"></i-bs><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
<i-bs class="me-1" name="folder"></i-bs><span>&nbsp;<ng-container i18n>Storage paths</ng-container></span> ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="folder"></i-bs><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
<i-bs class="me-1" name="ui-radios"></i-bs><span>&nbsp;<ng-container i18n>Custom fields</ng-container></span> ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="ui-radios"></i-bs><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a> </a>
</li> </li>
</ul>
</div>
</li>
}
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"

View File

@@ -177,15 +177,6 @@ main {
} }
} }
.attributes-row .attributes-expand-btn {
opacity: 0.2;
transition: opacity 0.15s ease-in-out;
}
.attributes-row:hover .attributes-expand-btn {
opacity: 1;
}
.sidebar-heading { .sidebar-heading {
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase; text-transform: uppercase;

View File

@@ -16,7 +16,6 @@ import { ActivatedRoute, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
@@ -28,10 +27,7 @@ import {
DjangoMessagesService, DjangoMessagesService,
} from 'src/app/services/django-messages.service' } from 'src/app/services/django-messages.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { import { PermissionsService } from 'src/app/services/permissions.service'
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service' import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service' import { SearchService } from 'src/app/services/rest/search.service'
@@ -161,7 +157,6 @@ describe('AppFrameComponent', () => {
PermissionsGuard, PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()
@@ -261,7 +256,7 @@ describe('AppFrameComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showError') const toastSpy = jest.spyOn(toastService, 'showError')
component.toggleSlimSidebar() component.toggleSlimSidebar()
httpTestingController httpTestingController
.match(`${environment.apiBaseUrl}ui_settings/`)[0] .expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush('error', { .flush('error', {
status: 500, status: 500,
statusText: 'error', statusText: 'error',
@@ -376,36 +371,4 @@ describe('AppFrameComponent', () => {
it('should call maybeRefreshDocumentCounts after saved views reload', () => { it('should call maybeRefreshDocumentCounts after saved views reload', () => {
expect(maybeRefreshSpy).toHaveBeenCalled() expect(maybeRefreshSpy).toHaveBeenCalled()
}) })
it('should indicate attributes management availability when any permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation((action, type) => {
return type === PermissionType.Tag
})
expect(component.canManageAttributes).toBe(true)
})
it('should persist attributes section collapse state', () => {
const setSpy = jest.spyOn(settingsService, 'set')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.attributesSectionsCollapsed = true
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
['attributes']
)
})
it('should collapse attributes sections when enabling slim sidebar', () => {
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)
component.toggleSlimSidebar()
expect(component.attributesSectionsCollapsed).toBe(true)
})
}) })

View File

@@ -16,12 +16,12 @@ import {
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard' import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -69,7 +69,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
NgbNavModule, NgbNavModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
DragDropModule, DragDropModule,
TourNgBootstrap, TourNgBootstrapModule,
], ],
}) })
export class AppFrameComponent export class AppFrameComponent
@@ -141,20 +141,11 @@ export class AppFrameComponent
toggleSlimSidebar(): void { toggleSlimSidebar(): void {
this.slimSidebarAnimating = true this.slimSidebarAnimating = true
this.slimSidebarEnabled = !this.slimSidebarEnabled this.slimSidebarEnabled = !this.slimSidebarEnabled
if (this.slimSidebarEnabled) {
this.attributesSectionsCollapsed = true
}
setTimeout(() => { setTimeout(() => {
this.slimSidebarAnimating = false this.slimSidebarAnimating = false
}, 200) // slightly longer than css animation for slim sidebar }, 200) // slightly longer than css animation for slim sidebar
} }
toggleAttributesSections(event?: Event): void {
event?.preventDefault()
event?.stopPropagation()
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
}
get versionString(): string { get versionString(): string {
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}` return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
} }
@@ -176,31 +167,6 @@ export class AppFrameComponent
) )
} }
get canManageAttributes(): boolean {
return (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Tag
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Correspondent
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.DocumentType
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.StoragePath
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
)
}
get slimSidebarEnabled(): boolean { get slimSidebarEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR) return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
} }
@@ -220,31 +186,6 @@ export class AppFrameComponent
}) })
} }
get attributesSectionsCollapsed(): boolean {
return this.settingsService
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
?.includes(CollapsibleSection.ATTRIBUTES)
}
set attributesSectionsCollapsed(collapsed: boolean) {
// TODO: refactor to be able to toggle individual sections, if implemented
this.settingsService.set(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
collapsed ? [CollapsibleSection.ATTRIBUTES] : []
)
this.settingsService
.storeSettings()
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
console.warn(error)
},
})
}
get aiEnabled(): boolean { get aiEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED) return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
} }

View File

@@ -3,14 +3,14 @@ import { Component, Input, inject } from '@angular/core'
import { Title } from '@angular/platform-browser' import { Title } from '@angular/platform-browser'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@Component({ @Component({
selector: 'pngx-page-header', selector: 'pngx-page-header',
templateUrl: './page-header.component.html', templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss'], styleUrls: ['./page-header.component.scss'],
imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrap], imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrapModule],
}) })
export class PageHeaderComponent { export class PageHeaderComponent {
private titleService = inject(Title) private titleService = inject(Title)

View File

@@ -5,12 +5,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
provideUiTour,
TourNgBootstrap,
TourService,
} from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
@@ -79,7 +75,7 @@ describe('DashboardComponent', () => {
imports: [ imports: [
NgbAlertModule, NgbAlertModule,
RouterTestingModule, RouterTestingModule,
TourNgBootstrap, TourNgBootstrapModule,
DragDropModule, DragDropModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
DashboardComponent, DashboardComponent,
@@ -115,7 +111,6 @@ describe('DashboardComponent', () => {
}, },
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -8,7 +8,7 @@ import {
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
@@ -36,7 +36,7 @@ import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.
WelcomeWidgetComponent, WelcomeWidgetComponent,
IfPermissionsDirective, IfPermissionsDirective,
DragDropModule, DragDropModule,
TourNgBootstrap, TourNgBootstrapModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
RouterModule, RouterModule,
], ],

View File

@@ -9,7 +9,6 @@ import {
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
@@ -62,7 +61,6 @@ describe('UploadFileWidgetComponent', () => {
}, },
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -7,7 +7,7 @@ import {
NgbProgressbarModule, NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component' import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -33,7 +33,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
NgbAlertModule, NgbAlertModule,
NgbProgressbarModule, NgbProgressbarModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
TourNgBootstrap, TourNgBootstrapModule,
], ],
}) })
export class UploadFileWidgetComponent extends ComponentWithPermissions { export class UploadFileWidgetComponent extends ComponentWithPermissions {

View File

@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { WelcomeWidgetComponent } from './welcome-widget.component' import { WelcomeWidgetComponent } from './welcome-widget.component'
@@ -12,7 +11,7 @@ describe('WelcomeWidgetComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [PermissionsGuard, provideUiTour()], providers: [PermissionsGuard],
imports: [NgbAlertModule, WelcomeWidgetComponent, WidgetFrameComponent], imports: [NgbAlertModule, WelcomeWidgetComponent, WidgetFrameComponent],
}).compileComponents() }).compileComponents()

View File

@@ -16,7 +16,6 @@ import {
NgbModalRef, NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { Subject, of, throwError } from 'rxjs' import { Subject, of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { import {
@@ -106,7 +105,6 @@ describe('DocumentListComponent', () => {
PermissionsGuard, PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -21,7 +21,7 @@ import {
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs' import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
import { import {
DEFAULT_DISPLAY_FIELDS, DEFAULT_DISPLAY_FIELDS,
@@ -99,7 +99,7 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
NgbPaginationModule, NgbPaginationModule,
NgClass, NgClass,
RouterModule, RouterModule,
TourNgBootstrap, TourNgBootstrapModule,
], ],
}) })
export class DocumentListComponent export class DocumentListComponent

View File

@@ -21,7 +21,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select' import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { Correspondent } from 'src/app/data/correspondent' import { Correspondent } from 'src/app/data/correspondent'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
@@ -252,7 +251,6 @@ describe('FilterEditorComponent', () => {
SettingsService, SettingsService,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -16,7 +16,7 @@ import {
NgbTypeaheadModule, NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { Observable, Subject, from } from 'rxjs' import { Observable, Subject, from } from 'rxjs'
import { import {
catchError, catchError,
@@ -251,7 +251,7 @@ const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [
NgbTypeaheadModule, NgbTypeaheadModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
TourNgBootstrap, TourNgBootstrapModule,
], ],
}) })
export class FilterEditorComponent export class FilterEditorComponent

View File

@@ -9,7 +9,7 @@ import { of } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { CorrespondentListComponent } from './correspondent-list.component' import { CorrespondentListComponent } from './correspondent-list.component'
describe('CorrespondentListComponent', () => { describe('CorrespondentListComponent', () => {

View File

@@ -1,4 +1,4 @@
import { NgClass, NgTemplateOutlet } from '@angular/common' import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@@ -7,7 +7,6 @@ import {
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { CorrespondentEditDialogComponent } from 'src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { Correspondent } from 'src/app/data/correspondent' import { Correspondent } from 'src/app/data/correspondent'
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type' import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -15,16 +14,21 @@ import { SortableDirective } from 'src/app/directives/sortable.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { PermissionType } from 'src/app/services/permissions.service' import { PermissionType } from 'src/app/services/permissions.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { ManagementListComponent } from '../management-list.component' import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ManagementListComponent } from '../management-list/management-list.component'
@Component({ @Component({
selector: 'pngx-correspondent-list', selector: 'pngx-correspondent-list',
templateUrl: './../management-list.component.html', templateUrl: './../management-list/management-list.component.html',
styleUrls: ['./../management-list.component.scss'], styleUrls: ['./../management-list/management-list.component.scss'],
providers: [{ provide: CustomDatePipe }], providers: [{ provide: CustomDatePipe }],
imports: [ imports: [
SortableDirective, SortableDirective,
IfPermissionsDirective, IfPermissionsDirective,
PageHeaderComponent,
TitleCasePipe,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
@@ -33,6 +37,7 @@ import { ManagementListComponent } from '../management-list.component'
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
ClearableBadgeComponent,
], ],
}) })
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> { export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {

View File

@@ -1,3 +1,15 @@
<pngx-page-header
title="Custom Fields"
i18n-title
info="Customize the data fields that can be attached to documents."
i18n-info
infoLink="usage/#custom-fields"
>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Field</ng-container>
</button>
</pngx-page-header>
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">

View File

@@ -26,9 +26,9 @@ import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { PageHeaderComponent } from '../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { CustomFieldsComponent } from './custom-fields.component' import { CustomFieldsComponent } from './custom-fields.component'
const fields: CustomField[] = [ const fields: CustomField[] = [
@@ -110,7 +110,10 @@ describe('CustomFieldsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload') const reloadSpy = jest.spyOn(component, 'reload')
component.editField() const createButton = fixture.debugElement
.queryAll(By.css('button'))
.find((btn) => btn.nativeElement.textContent.trim().includes('Add Field'))
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent const editDialog = modal.componentInstance as CustomFieldEditDialogComponent

View File

@@ -7,10 +7,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs' import { delay, takeUntil, tap } from 'rxjs'
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field' import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import { import {
CustomFieldQueryLogicalOperator, CustomFieldQueryLogicalOperator,
@@ -25,12 +21,18 @@ import { DocumentService } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({ @Component({
selector: 'pngx-custom-fields', selector: 'pngx-custom-fields',
templateUrl: './custom-fields.component.html', templateUrl: './custom-fields.component.html',
styleUrls: ['./custom-fields.component.scss'], styleUrls: ['./custom-fields.component.scss'],
imports: [ imports: [
PageHeaderComponent,
IfPermissionsDirective, IfPermissionsDirective,
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,

View File

@@ -1,78 +0,0 @@
<pngx-page-header
[title]="activeTabLabel"
info="Manage tags, correspondents, document types, storage paths, and custom fields."
i18n-info
[infoLink]="activeInfoLink"
[loading]="activeHeaderLoading"
>
@if (activeAttributeList) {
<div ngbDropdown class="btn-group flex-fill d-sm-none">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
<i-bs name="text-indent-left"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Select</ng-container></div>
@if (activeAttributeList.selectedObjects.size > 0) {
<pngx-clearable-badge [selected]="activeAttributeList.selectedObjects.size > 0" [number]="activeAttributeList.selectedObjects.size" (cleared)="activeAttributeList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
}
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
<button ngbDropdownItem (click)="activeAttributeList.selectNone()" i18n>Select none</button>
<button ngbDropdownItem (click)="activeAttributeList.selectPage(true)" i18n>Select page</button>
<button ngbDropdownItem (click)="activeAttributeList.selectAll()" i18n>Select all</button>
</div>
</div>
<div class="d-none d-sm-flex flex-fill me-3">
<div class="input-group input-group-sm">
<span class="input-group-text border-0" i18n>Select:</span>
</div>
<div class="btn-group btn-group-sm flex-nowrap">
@if (activeAttributeList.selectedObjects.size > 0) {
<button class="btn btn-sm btn-outline-secondary" (click)="activeAttributeList.selectNone()">
<i-bs name="slash-circle"></i-bs>&nbsp;<ng-container i18n>None</ng-container>
</button>
}
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectPage(true)">
<i-bs name="file-earmark-check"></i-bs>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectAll()">
<i-bs name="check-all"></i-bs>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.setPermissions()"
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Change) || activeAttributeList.selectedObjects.size === 0">
<i-bs name="person-fill-lock"></i-bs>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeAttributeList.delete()"
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Delete) || activeAttributeList.selectedObjects.size === 0">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeAttributeList.openCreateDialog()"
*pngxIfPermissions="{ action: PermissionAction.Add, type: activeAttributeList.permissionType }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Create</ng-container>
</button>
} @else if (activeCustomFields) {
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeCustomFields.editField()"
*pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Field</ng-container>
</button>
}
</pngx-page-header>
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-underline">
@for (section of visibleSections; track section.id) {
<li [ngbNavItem]="section.id">
<a ngbNavLink >
<i-bs class="me-2" [name]="section.icon"></i-bs>{{ section.label }}
</a>
</li>
}
</ul>
<div class="my-3 shadow-sm">
<ng-container
[ngComponentOutlet]="activeSection?.component"
#activeOutlet="ngComponentOutlet"
></ng-container>
</div>

View File

@@ -1,34 +0,0 @@
// .attributes-tabs {
// border-bottom-color: rgba(var(--bs-body-color-rgb), 0.12);
// }
// .attributes-tabs .nav-link {
// position: relative;
// padding-bottom: 0.45rem;
// color: rgba(var(--bs-body-color-rgb));
// transition: color 140ms ease;
// }
// .attributes-tabs .nav-link::after {
// content: '';
// position: absolute;
// left: 0.2rem;
// right: 0.2rem;
// bottom: 0;
// height: var(--bs-nav-underline-border-width);
// border-radius: 1px;
// background: transparent;
// transition: background-color 140ms ease;
// }
// .attributes-tabs .nav-link:hover {
// color: rgba(var(--bs-body-color-rgb), 0.95);
// }
// .attributes-tabs .nav-link.active {
// color: var(--bs-primary);
// }
// .attributes-tabs .nav-link.active::after {
// background: var(--bs-primary);
// }

View File

@@ -1,143 +0,0 @@
import { Component } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
ActivatedRoute,
convertToParamMap,
ParamMap,
Router,
} from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { DocumentAttributesComponent } from './document-attributes.component'
@Component({
selector: 'pngx-dummy-section',
template: '',
standalone: true,
})
class DummySectionComponent {}
describe('DocumentAttributesComponent', () => {
let component: DocumentAttributesComponent
let fixture: ComponentFixture<DocumentAttributesComponent>
let router: Router
let paramMapSubject: Subject<ParamMap>
let permissionsService: PermissionsService
beforeEach(async () => {
paramMapSubject = new Subject<ParamMap>()
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
DocumentAttributesComponent,
DummySectionComponent,
],
providers: [
{
provide: ActivatedRoute,
useValue: {
paramMap: paramMapSubject.asObservable(),
},
},
{
provide: PermissionsService,
useValue: {
currentUserCan: jest.fn(),
},
},
],
}).compileComponents()
fixture = TestBed.createComponent(DocumentAttributesComponent)
component = fixture.componentInstance
router = TestBed.inject(Router)
permissionsService = TestBed.inject(PermissionsService)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
;(component as any).sections = [
{
id: 1,
path: 'tags',
label: 'Tags',
icon: 'tags',
permissionType: PermissionType.Tag,
kind: 'attributeList',
component: DummySectionComponent,
},
{
id: 2,
path: 'customfields',
label: 'Custom fields',
icon: 'ui-radios',
permissionType: PermissionType.CustomField,
kind: 'customFields',
component: DummySectionComponent,
},
]
})
it('should navigate to default section when no section is provided', () => {
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
(action, type) => {
return action === PermissionAction.View && type === PermissionType.Tag
}
)
fixture.detectChanges()
paramMapSubject.next(convertToParamMap({}))
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], {
replaceUrl: true,
})
expect(component.activeNavID).toBe(1)
})
it('should set active section from route param when valid', () => {
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
(action, type) => {
return (
action === PermissionAction.View &&
type === PermissionType.CustomField
)
}
)
fixture.detectChanges()
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
expect(component.activeNavID).toBe(2)
expect(router.navigate).not.toHaveBeenCalled()
})
it('should redirect to dashboard when no sections are visible', () => {
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(false)
fixture.detectChanges()
paramMapSubject.next(convertToParamMap({}))
expect(router.navigate).toHaveBeenCalledWith(['/dashboard'], {
replaceUrl: true,
})
})
it('should navigate when a nav change occurs', () => {
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
() => true
)
fixture.detectChanges()
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
component.onNavChange({ nextId: 2 } as any)
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'customfields'])
})
})

View File

@@ -1,234 +0,0 @@
import { NgComponentOutlet } from '@angular/common'
import {
Component,
inject,
OnDestroy,
OnInit,
Type,
ViewChild,
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import {
NgbDropdownModule,
NgbNavChangeEvent,
NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, takeUntil } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { CustomFieldsComponent } from './custom-fields/custom-fields.component'
import { CorrespondentListComponent } from './management-list/correspondent-list/correspondent-list.component'
import { DocumentTypeListComponent } from './management-list/document-type-list/document-type-list.component'
import { ManagementListComponent } from './management-list/management-list.component'
import { StoragePathListComponent } from './management-list/storage-path-list/storage-path-list.component'
import { TagListComponent } from './management-list/tag-list/tag-list.component'
enum DocumentAttributesNavIDs {
Tags = 1,
Correspondents = 2,
DocumentTypes = 3,
StoragePaths = 4,
CustomFields = 5,
}
type DocumentAttributesSectionKind = 'attributeList' | 'customFields'
interface DocumentAttributesSection {
id: DocumentAttributesNavIDs
path: string
label: string
icon: string
infoLink?: string
permissionType: PermissionType
kind: DocumentAttributesSectionKind
component: Type<any>
}
@Component({
selector: 'pngx-document-attributes',
templateUrl: './document-attributes.component.html',
styleUrls: ['./document-attributes.component.scss'],
imports: [
PageHeaderComponent,
NgbNavModule,
NgbDropdownModule,
NgComponentOutlet,
NgxBootstrapIconsModule,
IfPermissionsDirective,
ClearableBadgeComponent,
],
})
export class DocumentAttributesComponent implements OnInit, OnDestroy {
private readonly permissionsService = inject(PermissionsService)
private readonly activatedRoute = inject(ActivatedRoute)
private readonly router = inject(Router)
private readonly unsubscribeNotifier = new Subject<void>()
protected readonly PermissionAction = PermissionAction
protected readonly PermissionType = PermissionType
readonly sections: DocumentAttributesSection[] = [
{
id: DocumentAttributesNavIDs.Tags,
path: 'tags',
label: $localize`Tags`,
icon: 'tags',
infoLink: 'usage/#terms-and-definitions',
permissionType: PermissionType.Tag,
kind: 'attributeList',
component: TagListComponent,
},
{
id: DocumentAttributesNavIDs.Correspondents,
path: 'correspondents',
label: $localize`Correspondents`,
icon: 'person',
infoLink: 'usage/#terms-and-definitions',
permissionType: PermissionType.Correspondent,
kind: 'attributeList',
component: CorrespondentListComponent,
},
{
id: DocumentAttributesNavIDs.DocumentTypes,
path: 'documenttypes',
label: $localize`Document types`,
icon: 'hash',
infoLink: 'usage/#terms-and-definitions',
permissionType: PermissionType.DocumentType,
kind: 'attributeList',
component: DocumentTypeListComponent,
},
{
id: DocumentAttributesNavIDs.StoragePaths,
path: 'storagepaths',
label: $localize`Storage paths`,
icon: 'folder',
infoLink: 'usage/#terms-and-definitions',
permissionType: PermissionType.StoragePath,
kind: 'attributeList',
component: StoragePathListComponent,
},
{
id: DocumentAttributesNavIDs.CustomFields,
path: 'customfields',
label: $localize`Custom fields`,
icon: 'ui-radios',
infoLink: 'usage/#custom-fields',
permissionType: PermissionType.CustomField,
kind: 'customFields',
component: CustomFieldsComponent,
},
]
@ViewChild('activeOutlet', { read: NgComponentOutlet })
private activeOutlet?: NgComponentOutlet
activeNavID: number = null
get visibleSections(): DocumentAttributesSection[] {
return this.sections.filter((section) =>
this.permissionsService.currentUserCan(
PermissionAction.View,
section.permissionType
)
)
}
get activeSection(): DocumentAttributesSection | null {
return (
this.visibleSections.find((section) => section.id === this.activeNavID) ??
null
)
}
get activeAttributeList(): ManagementListComponent<any> | null {
if (this.activeSection?.kind !== 'attributeList') return null
const instance = this.activeOutlet?.componentInstance
return instance instanceof ManagementListComponent ? instance : null
}
get activeCustomFields(): CustomFieldsComponent | null {
if (this.activeSection?.kind !== 'customFields') return null
const instance = this.activeOutlet?.componentInstance
return instance instanceof CustomFieldsComponent ? instance : null
}
get activeTabLabel(): string {
return this.activeSection?.label ?? ''
}
get activeInfoLink(): string {
return this.activeSection?.infoLink ?? null
}
get activeHeaderLoading(): boolean {
return (
this.activeAttributeList?.loading ??
this.activeCustomFields?.loading ??
false
)
}
ngOnInit(): void {
this.activatedRoute.paramMap
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((paramMap) => {
const section = paramMap.get('section')
const navIDFromSection =
this.getNavIDForSection(section) ?? this.getDefaultNavID()
if (navIDFromSection == null) {
this.router.navigate(['/dashboard'], { replaceUrl: true })
return
}
if (this.activeNavID !== navIDFromSection) {
this.activeNavID = navIDFromSection
}
if (!section || this.getNavIDForSection(section) == null) {
this.router.navigate(
['attributes', this.getSectionForNavID(this.activeNavID)],
{ replaceUrl: true }
)
}
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next()
this.unsubscribeNotifier.complete()
}
onNavChange(navChangeEvent: NgbNavChangeEvent): void {
const nextSection = this.getSectionForNavID(navChangeEvent.nextId)
if (!nextSection) {
return
}
this.router.navigate(['attributes', nextSection])
}
private getDefaultNavID(): DocumentAttributesNavIDs | null {
return this.visibleSections[0]?.id ?? null
}
private getNavIDForSection(section: string): DocumentAttributesNavIDs | null {
const path = section?.toLowerCase()
if (!path) return null
const found = this.visibleSections.find((s) => s.path === path)
return found?.id ?? null
}
private getSectionForNavID(navID: number): string | null {
const section = this.visibleSections.find((s) => s.id === navID)
return section?.path ?? null
}
}

View File

@@ -9,7 +9,7 @@ import { of } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { DocumentTypeListComponent } from './document-type-list.component' import { DocumentTypeListComponent } from './document-type-list.component'
describe('DocumentTypeListComponent', () => { describe('DocumentTypeListComponent', () => {

View File

@@ -1,4 +1,4 @@
import { NgClass, NgTemplateOutlet } from '@angular/common' import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@@ -7,21 +7,25 @@ import {
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentTypeEditDialogComponent } from 'src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { DocumentType } from 'src/app/data/document-type' import { DocumentType } from 'src/app/data/document-type'
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type' import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { PermissionType } from 'src/app/services/permissions.service' import { PermissionType } from 'src/app/services/permissions.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { ManagementListComponent } from '../management-list.component' import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ManagementListComponent } from '../management-list/management-list.component'
@Component({ @Component({
selector: 'pngx-document-type-list', selector: 'pngx-document-type-list',
templateUrl: './../management-list.component.html', templateUrl: './../management-list/management-list.component.html',
styleUrls: ['./../management-list.component.scss'], styleUrls: ['./../management-list/management-list.component.scss'],
imports: [ imports: [
SortableDirective, SortableDirective,
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective, IfPermissionsDirective,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
ClearableBadgeComponent,
], ],
}) })
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> { export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {

View File

@@ -1,3 +1,50 @@
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions" [loading]="loading">
<div ngbDropdown class="btn-group flex-fill d-sm-none">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
<i-bs name="text-indent-left"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Select</ng-container></div>
@if (selectedObjects.size > 0) {
<pngx-clearable-badge [selected]="selectedObjects.size > 0" [number]="selectedObjects.size" (cleared)="selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
}
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
<button ngbDropdownItem (click)="selectNone()" i18n>Select none</button>
<button ngbDropdownItem (click)="selectPage(true)" i18n>Select page</button>
<button ngbDropdownItem (click)="selectAll()" i18n>Select all</button>
</div>
</div>
<div class="d-none d-sm-flex flex-fill me-3">
<div class="input-group input-group-sm">
<span class="input-group-text border-0" i18n>Select:</span>
</div>
<div class="btn-group btn-group-sm flex-nowrap">
@if (selectedObjects.size > 0) {
<button class="btn btn-sm btn-outline-secondary" (click)="selectNone()">
<i-bs name="slash-circle"></i-bs>&nbsp;<ng-container i18n>None</ng-container>
</button>
}
<button class="btn btn-sm btn-outline-primary" (click)="selectPage(true)">
<i-bs name="file-earmark-check"></i-bs>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="selectAll()">
<i-bs name="check-all"></i-bs>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
<i-bs name="person-fill-lock"></i-bs>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Create</ng-container>
</button>
</pngx-page-header>
<div class="row mb-3"> <div class="row mb-3">
<div class="col mb-2 mb-xl-0"> <div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">

View File

@@ -44,12 +44,12 @@ import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-fil
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogComponent } from '../../../common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
import { PageHeaderComponent } from '../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { PermissionsDialogComponent } from '../../../common/permissions-dialog/permissions-dialog.component' import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { TagListComponent } from '../tag-list/tag-list.component'
import { ManagementListComponent } from './management-list.component' import { ManagementListComponent } from './management-list.component'
import { TagListComponent } from './tag-list/tag-list.component'
const tags: Tag[] = [ const tags: Tag[] = [
{ {

View File

@@ -16,10 +16,6 @@ import {
takeUntil, takeUntil,
tap, tap,
} from 'rxjs/operators' } from 'rxjs/operators'
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
import { import {
MATCH_AUTO, MATCH_AUTO,
MATCH_NONE, MATCH_NONE,
@@ -44,6 +40,10 @@ import {
} from 'src/app/services/rest/abstract-name-filter-service' } from 'src/app/services/rest/abstract-name-filter-service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
export interface ManagementListColumn { export interface ManagementListColumn {
key: string key: string

View File

@@ -10,7 +10,7 @@ import { StoragePath } from 'src/app/data/storage-path'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { StoragePathListComponent } from './storage-path-list.component' import { StoragePathListComponent } from './storage-path-list.component'
describe('StoragePathListComponent', () => { describe('StoragePathListComponent', () => {

View File

@@ -1,4 +1,4 @@
import { NgClass, NgTemplateOutlet } from '@angular/common' import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@@ -7,21 +7,25 @@ import {
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { StoragePathEditDialogComponent } from 'src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type' import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { PermissionType } from 'src/app/services/permissions.service' import { PermissionType } from 'src/app/services/permissions.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { ManagementListComponent } from '../management-list.component' import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ManagementListComponent } from '../management-list/management-list.component'
@Component({ @Component({
selector: 'pngx-storage-path-list', selector: 'pngx-storage-path-list',
templateUrl: './../management-list.component.html', templateUrl: './../management-list/management-list.component.html',
styleUrls: ['./../management-list.component.scss'], styleUrls: ['./../management-list/management-list.component.scss'],
imports: [ imports: [
SortableDirective, SortableDirective,
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective, IfPermissionsDirective,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
ClearableBadgeComponent,
], ],
}) })
export class StoragePathListComponent extends ManagementListComponent<StoragePath> { export class StoragePathListComponent extends ManagementListComponent<StoragePath> {

View File

@@ -9,7 +9,7 @@ import { of } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TagListComponent } from './tag-list.component' import { TagListComponent } from './tag-list.component'
describe('TagListComponent', () => { describe('TagListComponent', () => {

View File

@@ -1,4 +1,4 @@
import { NgClass, NgTemplateOutlet } from '@angular/common' import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@@ -7,21 +7,25 @@ import {
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type' import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive' import { SortableDirective } from 'src/app/directives/sortable.directive'
import { PermissionType } from 'src/app/services/permissions.service' import { PermissionType } from 'src/app/services/permissions.service'
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { ManagementListComponent } from '../management-list.component' import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ManagementListComponent } from '../management-list/management-list.component'
@Component({ @Component({
selector: 'pngx-tag-list', selector: 'pngx-tag-list',
templateUrl: './../management-list.component.html', templateUrl: './../management-list/management-list.component.html',
styleUrls: ['./../management-list.component.scss'], styleUrls: ['./../management-list/management-list.component.scss'],
imports: [ imports: [
SortableDirective, SortableDirective,
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective, IfPermissionsDirective,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
ClearableBadgeComponent,
], ],
}) })
export class TagListComponent extends ManagementListComponent<Tag> { export class TagListComponent extends ManagementListComponent<Tag> {

View File

@@ -19,10 +19,6 @@ export enum GlobalSearchType {
TITLE_CONTENT = 'title-content', TITLE_CONTENT = 'title-content',
} }
export enum CollapsibleSection {
ATTRIBUTES = 'attributes',
}
export const PAPERLESS_GREEN_HEX = '#17541f' export const PAPERLESS_GREEN_HEX = '#17541f'
export const SETTINGS_KEYS = { export const SETTINGS_KEYS = {
@@ -55,8 +51,6 @@ export const SETTINGS_KEYS = {
NOTES_ENABLED: 'general-settings:notes-enabled', NOTES_ENABLED: 'general-settings:notes-enabled',
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled', AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
SLIM_SIDEBAR: 'general-settings:slim-sidebar', SLIM_SIDEBAR: 'general-settings:slim-sidebar',
ATTRIBUTES_SECTIONS_COLLAPSED:
'general-settings:attributes-sections-collapsed',
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled', UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
UPDATE_CHECKING_BACKEND_SETTING: UPDATE_CHECKING_BACKEND_SETTING:
'general-settings:update-checking:backend-setting', 'general-settings:update-checking:backend-setting',
@@ -118,11 +112,6 @@ export const SETTINGS: UiSetting[] = [
type: 'boolean', type: 'boolean',
default: false, default: false,
}, },
{
key: SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
type: 'array',
default: [],
},
{ {
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
type: 'number', type: 'number',

View File

@@ -4,7 +4,6 @@ import { TestBed } from '@angular/core/testing'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { routes } from '../app-routing.module' import { routes } from '../app-routing.module'
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
import { DocumentListComponent } from '../components/document-list/document-list.component' import { DocumentListComponent } from '../components/document-list/document-list.component'
@@ -31,7 +30,6 @@ describe('DirtySavedViewGuard', () => {
DocumentListComponent, DocumentListComponent,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideUiTour(),
], ],
}) })

View File

@@ -1,10 +1,10 @@
import { TestBed } from '@angular/core/testing' import { TestBed } from '@angular/core/testing'
import { ActivatedRoute, RouterState } from '@angular/router' import { ActivatedRoute, RouterState } from '@angular/router'
import { provideUiTour, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { import {
PermissionAction, PermissionAction,
PermissionsService,
PermissionType, PermissionType,
PermissionsService,
} from '../services/permissions.service' } from '../services/permissions.service'
import { ToastService } from '../services/toast.service' import { ToastService } from '../services/toast.service'
import { PermissionsGuard } from './permissions.guard' import { PermissionsGuard } from './permissions.guard'
@@ -45,7 +45,6 @@ describe('PermissionsGuard', () => {
}, },
TourService, TourService,
ToastService, ToastService,
provideUiTour(),
], ],
}) })
@@ -96,52 +95,4 @@ describe('PermissionsGuard', () => {
expect(canActivate).toHaveProperty('root') // returns UrlTree expect(canActivate).toHaveProperty('root') // returns UrlTree
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
}) })
it('should activate when any required permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation((action, type) => {
return type === PermissionType.Tag
})
const canActivate = guard.canActivate(
{
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
],
},
} as any,
routerState.snapshot
)
expect(canActivate).toBeTruthy()
})
it('should not activate when no required permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation(() => false)
const canActivate = guard.canActivate(
{
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
],
},
} as any,
routerState.snapshot
)
expect(canActivate).toHaveProperty('root')
})
}) })

View File

@@ -20,20 +20,12 @@ export class PermissionsGuard {
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot state: RouterStateSnapshot
): boolean | UrlTree { ): boolean | UrlTree {
const requiredPermissionAny: { action: any; type: any }[] =
route.data.requiredPermissionAny
if ( if (
(route.data.requireAdmin && !this.permissionsService.isAdmin()) || (route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
(route.data.requiredPermission && (route.data.requiredPermission &&
!this.permissionsService.currentUserCan( !this.permissionsService.currentUserCan(
route.data.requiredPermission.action, route.data.requiredPermission.action,
route.data.requiredPermission.type route.data.requiredPermission.type
)) ||
(Array.isArray(requiredPermissionAny) &&
requiredPermissionAny.length > 0 &&
!requiredPermissionAny.some((p) =>
this.permissionsService.currentUserCan(p.action, p.type)
)) ))
) { ) {
// Check if tour is running 1 = TourState.ON // Check if tour is running 1 = TourState.ON

View File

@@ -126,7 +126,6 @@ import {
sliders2Vertical, sliders2Vertical,
sortAlphaDown, sortAlphaDown,
sortAlphaUpAlt, sortAlphaUpAlt,
stack,
stars, stars,
tag, tag,
tagFill, tagFill,
@@ -146,6 +145,7 @@ import {
} from 'ngx-bootstrap-icons' } from 'ngx-bootstrap-icons'
import { ColorSliderModule } from 'ngx-color/slider' import { ColorSliderModule } from 'ngx-color/slider'
import { CookieService } from 'ngx-cookie-service' import { CookieService } from 'ngx-cookie-service'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { AppRoutingModule } from './app/app-routing.module' import { AppRoutingModule } from './app/app-routing.module'
import { AppComponent } from './app/app.component' import { AppComponent } from './app/app.component'
import { DirtyDocGuard } from './app/guards/dirty-doc.guard' import { DirtyDocGuard } from './app/guards/dirty-doc.guard'
@@ -195,7 +195,6 @@ import localeUk from '@angular/common/locales/uk'
import localeVi from '@angular/common/locales/vi' import localeVi from '@angular/common/locales/vi'
import localeZh from '@angular/common/locales/zh' import localeZh from '@angular/common/locales/zh'
import localeZhHant from '@angular/common/locales/zh-Hant' import localeZhHant from '@angular/common/locales/zh-Hant'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { CorrespondentNamePipe } from './app/pipes/correspondent-name.pipe' import { CorrespondentNamePipe } from './app/pipes/correspondent-name.pipe'
import { DocumentTypeNamePipe } from './app/pipes/document-type-name.pipe' import { DocumentTypeNamePipe } from './app/pipes/document-type-name.pipe'
import { StoragePathNamePipe } from './app/pipes/storage-path-name.pipe' import { StoragePathNamePipe } from './app/pipes/storage-path-name.pipe'
@@ -345,7 +344,6 @@ const icons = {
sliders2Vertical, sliders2Vertical,
sortAlphaDown, sortAlphaDown,
sortAlphaUpAlt, sortAlphaUpAlt,
stack,
stars, stars,
tagFill, tagFill,
tag, tag,
@@ -376,6 +374,7 @@ bootstrapApplication(AppComponent, {
PdfViewerModule, PdfViewerModule,
NgSelectModule, NgSelectModule,
ColorSliderModule, ColorSliderModule,
TourNgBootstrapModule,
DragDropModule, DragDropModule,
NgxBootstrapIconsModule.pick(icons) NgxBootstrapIconsModule.pick(icons)
), ),
@@ -398,16 +397,5 @@ bootstrapApplication(AppComponent, {
withInterceptors([withCsrfInterceptor, withApiVersionInterceptor]), withInterceptors([withCsrfInterceptor, withApiVersionInterceptor]),
withFetch() withFetch()
), ),
provideUiTour({
enableBackdrop: true,
backdropConfig: {
offset: 10,
},
prevBtnTitle: $localize`Prev`,
nextBtnTitle: $localize`Next`,
endBtnTitle: $localize`End`,
isOptional: true,
useLegacyTitle: true,
}),
], ],
}).catch((err) => console.error(err)) }).catch((err) => console.error(err))

View File

@@ -16,7 +16,6 @@ from pikepdf import Pdf
from documents.converters import convert_from_tiff_to_pdf from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
from documents.models import Document
from documents.models import Tag from documents.models import Tag
from documents.plugins.base import ConsumeTaskPlugin from documents.plugins.base import ConsumeTaskPlugin
from documents.plugins.base import StopConsumeTaskError from documents.plugins.base import StopConsumeTaskError
@@ -130,24 +129,6 @@ class BarcodePlugin(ConsumeTaskPlugin):
self._tiff_conversion_done = False self._tiff_conversion_done = False
self.barcodes: list[Barcode] = [] self.barcodes: list[Barcode] = []
def _apply_detected_asn(self, detected_asn: int) -> None:
"""
Apply a detected ASN to metadata if allowed.
"""
if (
self.metadata.skip_asn_if_exists
and Document.global_objects.filter(
archive_serial_number=detected_asn,
).exists()
):
logger.info(
f"Found ASN in barcode {detected_asn} but skipping because it already exists.",
)
return
logger.info(f"Found ASN in barcode: {detected_asn}")
self.metadata.asn = detected_asn
def run(self) -> None: def run(self) -> None:
# Some operations may use PIL, override pixel setting if needed # Some operations may use PIL, override pixel setting if needed
maybe_override_pixel_limit() maybe_override_pixel_limit()
@@ -225,8 +206,13 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Update/overwrite an ASN if possible # Update/overwrite an ASN if possible
# After splitting, as otherwise each split document gets the same ASN # After splitting, as otherwise each split document gets the same ASN
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None: if (
self._apply_detected_asn(located_asn) self.settings.barcode_enable_asn
and not self.metadata.skip_asn
and (located_asn := self.asn) is not None
):
logger.info(f"Found ASN in barcode: {located_asn}")
self.metadata.asn = located_asn
def cleanup(self) -> None: def cleanup(self) -> None:
self.temp_dir.cleanup() self.temp_dir.cleanup()

View File

@@ -7,6 +7,7 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Literal from typing import Literal
from celery import chain
from celery import chord from celery import chord
from celery import group from celery import group
from celery import shared_task from celery import shared_task
@@ -37,42 +38,6 @@ if TYPE_CHECKING:
logger: logging.Logger = logging.getLogger("paperless.bulk_edit") logger: logging.Logger = logging.getLogger("paperless.bulk_edit")
@shared_task(bind=True)
def restore_archive_serial_numbers_task(
self,
backup: dict[int, int | None],
*args,
**kwargs,
) -> None:
restore_archive_serial_numbers(backup)
def release_archive_serial_numbers(doc_ids: list[int]) -> dict[int, int | None]:
"""
Clears ASNs on documents that are about to be replaced so new documents
can be assigned ASNs without uniqueness collisions. Returns a backup map
of doc_id -> previous ASN for potential restoration.
"""
qs = Document.objects.filter(
id__in=doc_ids,
archive_serial_number__isnull=False,
).only("pk", "archive_serial_number")
backup = dict(qs.values_list("pk", "archive_serial_number"))
qs.update(archive_serial_number=None)
logger.info(f"Released archive serial numbers for documents {list(backup.keys())}")
return backup
def restore_archive_serial_numbers(backup: dict[int, int | None]) -> None:
"""
Restores ASNs using the provided backup map, intended for
rollback when replacement consumption fails.
"""
for doc_id, asn in backup.items():
Document.objects.filter(pk=doc_id).update(archive_serial_number=asn)
logger.info(f"Restored archive serial numbers for documents {list(backup.keys())}")
def set_correspondent( def set_correspondent(
doc_ids: list[int], doc_ids: list[int],
correspondent: Correspondent, correspondent: Correspondent,
@@ -340,10 +305,10 @@ def reprocess(doc_ids: list[int]) -> Literal["OK"]:
def set_permissions( def set_permissions(
doc_ids: list[int], doc_ids: list[int],
set_permissions: dict, set_permissions,
*, *,
owner: User | None = None, owner=None,
merge: bool = False, merge=False,
) -> Literal["OK"]: ) -> Literal["OK"]:
qs = Document.objects.filter(id__in=doc_ids).select_related("owner") qs = Document.objects.filter(id__in=doc_ids).select_related("owner")
@@ -421,7 +386,6 @@ def merge(
merged_pdf = pikepdf.new() merged_pdf = pikepdf.new()
version: str = merged_pdf.pdf_version version: str = merged_pdf.pdf_version
handoff_asn: int | None = None
# use doc_ids to preserve order # use doc_ids to preserve order
for doc_id in doc_ids: for doc_id in doc_ids:
doc = qs.get(id=doc_id) doc = qs.get(id=doc_id)
@@ -437,8 +401,6 @@ def merge(
version = max(version, pdf.pdf_version) version = max(version, pdf.pdf_version)
merged_pdf.pages.extend(pdf.pages) merged_pdf.pages.extend(pdf.pages)
affected_docs.append(doc.id) affected_docs.append(doc.id)
if handoff_asn is None and doc.archive_serial_number is not None:
handoff_asn = doc.archive_serial_number
except Exception as e: except Exception as e:
logger.exception( logger.exception(
f"Error merging document {doc.id}, it will not be included in the merge: {e}", f"Error merging document {doc.id}, it will not be included in the merge: {e}",
@@ -464,8 +426,6 @@ def merge(
DocumentMetadataOverrides.from_document(metadata_document) DocumentMetadataOverrides.from_document(metadata_document)
) )
overrides.title = metadata_document.title + " (merged)" overrides.title = metadata_document.title + " (merged)"
if metadata_document.archive_serial_number is not None:
handoff_asn = metadata_document.archive_serial_number
else: else:
overrides = DocumentMetadataOverrides() overrides = DocumentMetadataOverrides()
else: else:
@@ -473,11 +433,8 @@ def merge(
if user is not None: if user is not None:
overrides.owner_id = user.id overrides.owner_id = user.id
if not delete_originals: # Avoid copying or detecting ASN from merged PDFs to prevent collision
overrides.skip_asn_if_exists = True overrides.skip_asn = True
if delete_originals and handoff_asn is not None:
overrides.asn = handoff_asn
logger.info("Adding merged document to the task queue.") logger.info("Adding merged document to the task queue.")
@@ -490,18 +447,10 @@ def merge(
) )
if delete_originals: if delete_originals:
backup = release_archive_serial_numbers(affected_docs)
logger.info( logger.info(
"Queueing removal of original documents after consumption of merged document", "Queueing removal of original documents after consumption of merged document",
) )
try: chain(consume_task, delete.si(affected_docs)).delay()
consume_task.apply_async(
link=[delete.si(affected_docs)],
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else: else:
consume_task.delay() consume_task.delay()
@@ -545,8 +494,6 @@ def split(
overrides.title = f"{doc.title} (split {idx + 1})" overrides.title = f"{doc.title} (split {idx + 1})"
if user is not None: if user is not None:
overrides.owner_id = user.id overrides.owner_id = user.id
if not delete_originals:
overrides.skip_asn_if_exists = True
logger.info( logger.info(
f"Adding split document with pages {split_doc} to the task queue.", f"Adding split document with pages {split_doc} to the task queue.",
) )
@@ -561,20 +508,10 @@ def split(
) )
if delete_originals: if delete_originals:
backup = release_archive_serial_numbers([doc.id])
logger.info( logger.info(
"Queueing removal of original document after consumption of the split documents", "Queueing removal of original document after consumption of the split documents",
) )
try: chord(header=consume_tasks, body=delete.si([doc.id])).delay()
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).apply_async(
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else: else:
group(consume_tasks).delay() group(consume_tasks).delay()
@@ -614,7 +551,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
def edit_pdf( def edit_pdf(
doc_ids: list[int], doc_ids: list[int],
operations: list[dict[str, int]], operations: list[dict],
*, *,
delete_original: bool = False, delete_original: bool = False,
update_document: bool = False, update_document: bool = False,
@@ -677,10 +614,7 @@ def edit_pdf(
) )
if user is not None: if user is not None:
overrides.owner_id = user.id overrides.owner_id = user.id
if not delete_original:
overrides.skip_asn_if_exists = True
if delete_original and len(pdf_docs) == 1:
overrides.asn = doc.archive_serial_number
for idx, pdf in enumerate(pdf_docs, start=1): for idx, pdf in enumerate(pdf_docs, start=1):
filepath: Path = ( filepath: Path = (
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
@@ -699,17 +633,7 @@ def edit_pdf(
) )
if delete_original: if delete_original:
backup = release_archive_serial_numbers([doc.id]) chord(header=consume_tasks, body=delete.si([doc.id])).delay()
try:
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).apply_async(
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else: else:
group(consume_tasks).delay() group(consume_tasks).delay()

View File

@@ -697,7 +697,7 @@ class ConsumerPlugin(
pk=self.metadata.storage_path_id, pk=self.metadata.storage_path_id,
) )
if self.metadata.asn is not None: if self.metadata.asn is not None and not self.metadata.skip_asn:
document.archive_serial_number = self.metadata.asn document.archive_serial_number = self.metadata.asn
if self.metadata.owner_id: if self.metadata.owner_id:
@@ -865,8 +865,8 @@ class AsnCheckPlugin(
""" """
Check that if override_asn is given, it is unique and within a valid range Check that if override_asn is given, it is unique and within a valid range
""" """
if self.metadata.asn is None: if self.metadata.skip_asn or self.metadata.asn is None:
# if ASN is None # if skip is set or ASN is None
return return
# Validate the range is above zero and less than uint32_t max # Validate the range is above zero and less than uint32_t max
# otherwise, Whoosh can't handle it in the index # otherwise, Whoosh can't handle it in the index

View File

@@ -30,7 +30,7 @@ class DocumentMetadataOverrides:
change_users: list[int] | None = None change_users: list[int] | None = None
change_groups: list[int] | None = None change_groups: list[int] | None = None
custom_fields: dict | None = None custom_fields: dict | None = None
skip_asn_if_exists: bool = False skip_asn: bool = False
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides": def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
""" """
@@ -50,8 +50,8 @@ class DocumentMetadataOverrides:
self.storage_path_id = other.storage_path_id self.storage_path_id = other.storage_path_id
if other.owner_id is not None: if other.owner_id is not None:
self.owner_id = other.owner_id self.owner_id = other.owner_id
if other.skip_asn_if_exists: if other.skip_asn:
self.skip_asn_if_exists = True self.skip_asn = True
# merge # merge
if self.tag_ids is None: if self.tag_ids is None:

View File

@@ -65,12 +65,8 @@ def match_correspondents(document: Document, classifier: DocumentClassifier, use
return list( return list(
filter( filter(
lambda o: ( lambda o: matches(o, document)
matches(o, document) or (o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO),
or (
o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO
)
),
correspondents, correspondents,
), ),
) )
@@ -96,12 +92,8 @@ def match_document_types(document: Document, classifier: DocumentClassifier, use
return list( return list(
filter( filter(
lambda o: ( lambda o: matches(o, document)
matches(o, document) or (o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO),
or (
o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO
)
),
document_types, document_types,
), ),
) )
@@ -122,12 +114,10 @@ def match_tags(document: Document, classifier: DocumentClassifier, user=None):
return list( return list(
filter( filter(
lambda o: ( lambda o: matches(o, document)
matches(o, document)
or ( or (
o.matching_algorithm == MatchingModel.MATCH_AUTO o.matching_algorithm == MatchingModel.MATCH_AUTO
and o.pk in predicted_tag_ids and o.pk in predicted_tag_ids
)
), ),
tags, tags,
), ),
@@ -155,12 +145,8 @@ def match_storage_paths(document: Document, classifier: DocumentClassifier, user
return list( return list(
filter( filter(
lambda o: ( lambda o: matches(o, document)
matches(o, document) or (o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO),
or (
o.pk == pred_id and o.matching_algorithm == MatchingModel.MATCH_AUTO
)
),
storage_paths, storage_paths,
), ),
) )

View File

@@ -252,10 +252,8 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
""" """
self._assert_query_match_predicate( self._assert_query_match_predicate(
["url_field", "exact", "https://docs.paperless-ngx.com/"], ["url_field", "exact", "https://docs.paperless-ngx.com/"],
lambda document: ( lambda document: "url_field" in document
"url_field" in document and document["url_field"] == "https://docs.paperless-ngx.com/",
and document["url_field"] == "https://docs.paperless-ngx.com/"
),
) )
def test_filter_by_multiple_fields(self) -> None: def test_filter_by_multiple_fields(self) -> None:
@@ -279,26 +277,22 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
def test_exact(self) -> None: def test_exact(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "exact", "paperless"], ["string_field", "exact", "paperless"],
lambda document: ( lambda document: "string_field" in document
"string_field" in document and document["string_field"] == "paperless" and document["string_field"] == "paperless",
),
) )
def test_in(self) -> None: def test_in(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "in", ["paperless", "Paperless"]], ["string_field", "in", ["paperless", "Paperless"]],
lambda document: ( lambda document: "string_field" in document
"string_field" in document and document["string_field"] in ("paperless", "Paperless"),
and document["string_field"] in ("paperless", "Paperless")
),
) )
def test_isnull(self) -> None: def test_isnull(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "isnull", True], ["string_field", "isnull", True],
lambda document: ( lambda document: "string_field" in document
"string_field" in document and document["string_field"] is None and document["string_field"] is None,
),
) )
def test_exists(self) -> None: def test_exists(self) -> None:
@@ -318,16 +312,14 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# or the name of the option. They function exactly the same. # or the name of the option. They function exactly the same.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["select_field", "exact", "def-456"], ["select_field", "exact", "def-456"],
lambda document: ( lambda document: "select_field" in document
"select_field" in document and document["select_field"] == "def-456" and document["select_field"] == "def-456",
),
) )
# This is the same as: # This is the same as:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["select_field", "exact", "B"], ["select_field", "exact", "B"],
lambda document: ( lambda document: "select_field" in document
"select_field" in document and document["select_field"] == "def-456" and document["select_field"] == "def-456",
),
) )
# ==========================================================# # ==========================================================#
@@ -336,31 +328,25 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
def test_icontains(self) -> None: def test_icontains(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "icontains", "aper"], ["string_field", "icontains", "aper"],
lambda document: ( lambda document: "string_field" in document
"string_field" in document
and document["string_field"] is not None and document["string_field"] is not None
and "aper" in document["string_field"].lower() and "aper" in document["string_field"].lower(),
),
) )
def test_istartswith(self) -> None: def test_istartswith(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "istartswith", "paper"], ["string_field", "istartswith", "paper"],
lambda document: ( lambda document: "string_field" in document
"string_field" in document
and document["string_field"] is not None and document["string_field"] is not None
and document["string_field"].lower().startswith("paper") and document["string_field"].lower().startswith("paper"),
),
) )
def test_iendswith(self) -> None: def test_iendswith(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["string_field", "iendswith", "less"], ["string_field", "iendswith", "less"],
lambda document: ( lambda document: "string_field" in document
"string_field" in document
and document["string_field"] is not None and document["string_field"] is not None
and document["string_field"].lower().endswith("less") and document["string_field"].lower().endswith("less"),
),
) )
def test_url_field_istartswith(self) -> None: def test_url_field_istartswith(self) -> None:
@@ -368,11 +354,9 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# Just showing one of them here. # Just showing one of them here.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["url_field", "istartswith", "http://"], ["url_field", "istartswith", "http://"],
lambda document: ( lambda document: "url_field" in document
"url_field" in document
and document["url_field"] is not None and document["url_field"] is not None
and document["url_field"].startswith("http://") and document["url_field"].startswith("http://"),
),
) )
# ==========================================================# # ==========================================================#
@@ -381,51 +365,41 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
def test_gt(self) -> None: def test_gt(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["date_field", "gt", date(2024, 8, 22).isoformat()], ["date_field", "gt", date(2024, 8, 22).isoformat()],
lambda document: ( lambda document: "date_field" in document
"date_field" in document
and document["date_field"] is not None and document["date_field"] is not None
and document["date_field"] > date(2024, 8, 22) and document["date_field"] > date(2024, 8, 22),
),
) )
def test_gte(self) -> None: def test_gte(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["date_field", "gte", date(2024, 8, 22).isoformat()], ["date_field", "gte", date(2024, 8, 22).isoformat()],
lambda document: ( lambda document: "date_field" in document
"date_field" in document
and document["date_field"] is not None and document["date_field"] is not None
and document["date_field"] >= date(2024, 8, 22) and document["date_field"] >= date(2024, 8, 22),
),
) )
def test_lt(self) -> None: def test_lt(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["integer_field", "lt", 0], ["integer_field", "lt", 0],
lambda document: ( lambda document: "integer_field" in document
"integer_field" in document
and document["integer_field"] is not None and document["integer_field"] is not None
and document["integer_field"] < 0 and document["integer_field"] < 0,
),
) )
def test_lte(self) -> None: def test_lte(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["integer_field", "lte", 0], ["integer_field", "lte", 0],
lambda document: ( lambda document: "integer_field" in document
"integer_field" in document
and document["integer_field"] is not None and document["integer_field"] is not None
and document["integer_field"] <= 0 and document["integer_field"] <= 0,
),
) )
def test_range(self) -> None: def test_range(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["float_field", "range", [-0.05, 0.05]], ["float_field", "range", [-0.05, 0.05]],
lambda document: ( lambda document: "float_field" in document
"float_field" in document
and document["float_field"] is not None and document["float_field"] is not None
and -0.05 <= document["float_field"] <= 0.05 and -0.05 <= document["float_field"] <= 0.05,
),
) )
def test_date_modifier(self) -> None: def test_date_modifier(self) -> None:
@@ -433,23 +407,19 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# with the part of the date you are comparing with. # with the part of the date you are comparing with.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["date_field", "year__gte", 2024], ["date_field", "year__gte", 2024],
lambda document: ( lambda document: "date_field" in document
"date_field" in document
and document["date_field"] is not None and document["date_field"] is not None
and document["date_field"].year >= 2024 and document["date_field"].year >= 2024,
),
) )
def test_gt_monetary(self) -> None: def test_gt_monetary(self) -> None:
self._assert_query_match_predicate( self._assert_query_match_predicate(
["monetary_field", "gt", "99"], ["monetary_field", "gt", "99"],
lambda document: ( lambda document: "monetary_field" in document
"monetary_field" in document
and document["monetary_field"] is not None and document["monetary_field"] is not None
and ( and (
document["monetary_field"] == "USD100.00" # With currency symbol document["monetary_field"] == "USD100.00" # With currency symbol
or document["monetary_field"] == "101.00" # No currency symbol or document["monetary_field"] == "101.00" # No currency symbol
)
), ),
) )
@@ -460,30 +430,24 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# Document link field "contains" performs a subset check. # Document link field "contains" performs a subset check.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["documentlink_field", "contains", [1, 2]], ["documentlink_field", "contains", [1, 2]],
lambda document: ( lambda document: "documentlink_field" in document
"documentlink_field" in document
and document["documentlink_field"] is not None and document["documentlink_field"] is not None
and set(document["documentlink_field"]) >= {1, 2} and set(document["documentlink_field"]) >= {1, 2},
),
) )
# The order of IDs don't matter - this is the same as above. # The order of IDs don't matter - this is the same as above.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["documentlink_field", "contains", [2, 1]], ["documentlink_field", "contains", [2, 1]],
lambda document: ( lambda document: "documentlink_field" in document
"documentlink_field" in document
and document["documentlink_field"] is not None and document["documentlink_field"] is not None
and set(document["documentlink_field"]) >= {1, 2} and set(document["documentlink_field"]) >= {1, 2},
),
) )
def test_document_link_contains_empty_set(self) -> None: def test_document_link_contains_empty_set(self) -> None:
# An empty set is a subset of any set. # An empty set is a subset of any set.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["documentlink_field", "contains", []], ["documentlink_field", "contains", []],
lambda document: ( lambda document: "documentlink_field" in document
"documentlink_field" in document and document["documentlink_field"] is not None,
and document["documentlink_field"] is not None
),
) )
def test_document_link_contains_no_reverse_link(self) -> None: def test_document_link_contains_no_reverse_link(self) -> None:
@@ -491,11 +455,9 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# doesn't have a document link field and thus has no reverse link. # doesn't have a document link field and thus has no reverse link.
self._assert_query_match_predicate( self._assert_query_match_predicate(
["documentlink_field", "contains", [self.documents[6].id]], ["documentlink_field", "contains", [self.documents[6].id]],
lambda document: ( lambda document: "documentlink_field" in document
"documentlink_field" in document
and document["documentlink_field"] is not None and document["documentlink_field"] is not None
and set(document["documentlink_field"]) >= {self.documents[6].id} and set(document["documentlink_field"]) >= {self.documents[6].id},
),
match_nothing_ok=True, match_nothing_ok=True,
) )
@@ -508,12 +470,10 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
"AND", "AND",
[["date_field", "year__exact", 2024], ["date_field", "month__lt", 9]], [["date_field", "year__exact", 2024], ["date_field", "month__lt", 9]],
], ],
lambda document: ( lambda document: "date_field" in document
"date_field" in document
and document["date_field"] is not None and document["date_field"] is not None
and document["date_field"].year == 2024 and document["date_field"].year == 2024
and document["date_field"].month < 9 and document["date_field"].month < 9,
),
) )
def test_logical_or(self) -> None: def test_logical_or(self) -> None:
@@ -523,9 +483,8 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
"OR", "OR",
[["string_field", "exact", ""], ["string_field", "isnull", True]], [["string_field", "exact", ""], ["string_field", "isnull", True]],
], ],
lambda document: ( lambda document: "string_field" in document
"string_field" in document and not bool(document["string_field"]) and not bool(document["string_field"]),
),
) )
def test_logical_not(self) -> None: def test_logical_not(self) -> None:
@@ -536,11 +495,8 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
"NOT", "NOT",
["string_field", "exact", "paperless"], ["string_field", "exact", "paperless"],
], ],
lambda document: ( lambda document: not (
not ( "string_field" in document and document["string_field"] == "paperless"
"string_field" in document
and document["string_field"] == "paperless"
)
), ),
) )

View File

@@ -603,21 +603,23 @@ class TestPDFActions(DirectoriesMixin, TestCase):
expected_filename, expected_filename,
) )
self.assertEqual(consume_file_args[1].title, None) self.assertEqual(consume_file_args[1].title, None)
# No metadata_document_id, delete_originals False, so ASN should be None self.assertTrue(consume_file_args[1].skip_asn)
self.assertIsNone(consume_file_args[1].asn)
# With metadata_document_id overrides # With metadata_document_id overrides
result = bulk_edit.merge(doc_ids, metadata_document_id=metadata_document_id) result = bulk_edit.merge(doc_ids, metadata_document_id=metadata_document_id)
consume_file_args, _ = mock_consume_file.call_args consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].title, "B (merged)") self.assertEqual(consume_file_args[1].title, "B (merged)")
self.assertEqual(consume_file_args[1].created, self.doc2.created) self.assertEqual(consume_file_args[1].created, self.doc2.created)
self.assertTrue(consume_file_args[1].skip_asn)
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@mock.patch("documents.bulk_edit.delete.si") @mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chain")
def test_merge_and_delete_originals( def test_merge_and_delete_originals(
self, self,
mock_chain,
mock_consume_file, mock_consume_file,
mock_delete_documents, mock_delete_documents,
): ):
@@ -631,12 +633,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
- Document deletion task should be called - Document deletion task should be called
""" """
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id] doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
self.doc1.archive_serial_number = 101
self.doc2.archive_serial_number = 102
self.doc3.archive_serial_number = 103
self.doc1.save()
self.doc2.save()
self.doc3.save()
result = bulk_edit.merge(doc_ids, delete_originals=True) result = bulk_edit.merge(doc_ids, delete_originals=True)
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@@ -647,8 +643,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_consume_file.assert_called() mock_consume_file.assert_called()
mock_delete_documents.assert_called() mock_delete_documents.assert_called()
consume_sig = mock_consume_file.return_value mock_chain.assert_called_once()
consume_sig.apply_async.assert_called_once()
consume_file_args, _ = mock_consume_file.call_args consume_file_args, _ = mock_consume_file.call_args
self.assertEqual( self.assertEqual(
@@ -656,7 +651,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
expected_filename, expected_filename,
) )
self.assertEqual(consume_file_args[1].title, None) self.assertEqual(consume_file_args[1].title, None)
self.assertEqual(consume_file_args[1].asn, 101) self.assertTrue(consume_file_args[1].skip_asn)
delete_documents_args, _ = mock_delete_documents.call_args delete_documents_args, _ = mock_delete_documents.call_args
self.assertEqual( self.assertEqual(
@@ -664,92 +659,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
doc_ids, doc_ids,
) )
self.doc1.refresh_from_db()
self.doc2.refresh_from_db()
self.doc3.refresh_from_db()
self.assertIsNone(self.doc1.archive_serial_number)
self.assertIsNone(self.doc2.archive_serial_number)
self.assertIsNone(self.doc3.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
def test_merge_and_delete_originals_restore_on_failure(
self,
mock_consume_file,
mock_delete_documents,
) -> None:
"""
GIVEN:
- Existing documents
WHEN:
- Merge action with deleting documents is called with 1 document
- Error occurs when queuing consume file task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc1.id]
self.doc1.archive_serial_number = 111
self.doc1.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_consume_file.return_value = sig
with self.assertRaises(Exception):
bulk_edit.merge(doc_ids, delete_originals=True)
self.doc1.refresh_from_db()
self.assertEqual(self.doc1.archive_serial_number, 111)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
def test_merge_and_delete_originals_metadata_handoff(
self,
mock_consume_file,
mock_delete_documents,
) -> None:
"""
GIVEN:
- Existing documents with ASNs
WHEN:
- Merge with delete_originals=True and metadata_document_id set
THEN:
- Handoff ASN uses metadata document ASN
"""
doc_ids = [self.doc1.id, self.doc2.id]
self.doc1.archive_serial_number = 101
self.doc2.archive_serial_number = 202
self.doc1.save()
self.doc2.save()
result = bulk_edit.merge(
doc_ids,
metadata_document_id=self.doc2.id,
delete_originals=True,
)
self.assertEqual(result, "OK")
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].asn, 202)
def test_restore_archive_serial_numbers_task(self) -> None:
"""
GIVEN:
- Existing document with no archive serial number
WHEN:
- Restore archive serial number task is called with backup data
THEN:
- Document archive serial number is restored
"""
self.doc1.archive_serial_number = 444
self.doc1.save()
Document.objects.filter(pk=self.doc1.id).update(archive_serial_number=None)
backup: dict[int, int | None] = {self.doc1.id: 444}
bulk_edit.restore_archive_serial_numbers_task(backup)
self.doc1.refresh_from_db()
self.assertEqual(self.doc1.archive_serial_number, 444)
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_merge_with_archive_fallback(self, mock_consume_file) -> None: def test_merge_with_archive_fallback(self, mock_consume_file) -> None:
""" """
@@ -818,7 +727,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(mock_consume_file.call_count, 2) self.assertEqual(mock_consume_file.call_count, 2)
consume_file_args, _ = mock_consume_file.call_args consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].title, "B (split 2)") self.assertEqual(consume_file_args[1].title, "B (split 2)")
self.assertIsNone(consume_file_args[1].asn)
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@@ -843,8 +751,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
""" """
doc_ids = [self.doc2.id] doc_ids = [self.doc2.id]
pages = [[1, 2], [3]] pages = [[1, 2], [3]]
self.doc2.archive_serial_number = 200
self.doc2.save()
result = bulk_edit.split(doc_ids, pages, delete_originals=True) result = bulk_edit.split(doc_ids, pages, delete_originals=True)
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@@ -862,42 +768,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
doc_ids, doc_ids,
) )
self.doc2.refresh_from_db()
self.assertIsNone(self.doc2.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chord")
def test_split_restore_on_failure(
self,
mock_chord,
mock_consume_file,
mock_delete_documents,
) -> None:
"""
GIVEN:
- Existing documents
WHEN:
- Split action with deleting documents is called with 1 document and 2 page groups
- Error occurs when queuing chord task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc2.id]
pages = [[1, 2]]
self.doc2.archive_serial_number = 222
self.doc2.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_chord.return_value = sig
result = bulk_edit.split(doc_ids, pages, delete_originals=True)
self.assertEqual(result, "OK")
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.archive_serial_number, 222)
@mock.patch("documents.tasks.consume_file.delay") @mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.Pdf.save") @mock.patch("pikepdf.Pdf.save")
def test_split_with_errors(self, mock_save_pdf, mock_consume_file) -> None: def test_split_with_errors(self, mock_save_pdf, mock_consume_file) -> None:
@@ -1107,55 +977,13 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_chord.return_value.delay.return_value = None mock_chord.return_value.delay.return_value = None
doc_ids = [self.doc2.id] doc_ids = [self.doc2.id]
operations = [{"page": 1}, {"page": 2}] operations = [{"page": 1}, {"page": 2}]
self.doc2.archive_serial_number = 250
self.doc2.save()
result = bulk_edit.edit_pdf(doc_ids, operations, delete_original=True) result = bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_chord.assert_called_once() mock_chord.assert_called_once()
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].asn, 250)
self.doc2.refresh_from_db()
self.assertIsNone(self.doc2.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chord")
def test_edit_pdf_restore_on_failure(
self,
mock_chord: mock.Mock,
mock_consume_file: mock.Mock,
mock_delete_documents: mock.Mock,
) -> None:
"""
GIVEN:
- Existing document
WHEN:
- edit_pdf is called with delete_original=True
- Error occurs when queuing chord task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc2.id]
operations = [{"page": 1}]
self.doc2.archive_serial_number = 333
self.doc2.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_chord.return_value = sig
with self.assertRaises(Exception):
bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.archive_serial_number, 333)
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay") @mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
def test_edit_pdf_with_update_document( def test_edit_pdf_with_update_document(self, mock_update_document) -> None:
self,
mock_update_document: mock.Mock,
) -> None:
""" """
GIVEN: GIVEN:
- A single existing PDF document - A single existing PDF document
@@ -1185,11 +1013,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_without_metadata( def test_edit_pdf_without_metadata(self, mock_consume_file, mock_group) -> None:
self,
mock_consume_file: mock.Mock,
mock_group: mock.Mock,
) -> None:
""" """
GIVEN: GIVEN:
- Existing document - Existing document
@@ -1208,11 +1032,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_open_failure( def test_edit_pdf_open_failure(self, mock_consume_file, mock_group) -> None:
self,
mock_consume_file: mock.Mock,
mock_group: mock.Mock,
) -> None:
""" """
GIVEN: GIVEN:
- Existing document - Existing document

View File

@@ -14,7 +14,6 @@ from django.test import override_settings
from django.utils import timezone from django.utils import timezone
from guardian.core import ObjectPermissionChecker from guardian.core import ObjectPermissionChecker
from documents.barcodes import BarcodePlugin
from documents.consumer import ConsumerError from documents.consumer import ConsumerError
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
@@ -413,6 +412,14 @@ class TestConsumer(
self.assertEqual(document.archive_serial_number, 123) self.assertEqual(document.archive_serial_number, 123)
self._assert_first_last_send_progress() self._assert_first_last_send_progress()
def testMetadataOverridesSkipAsnPropagation(self) -> None:
overrides = DocumentMetadataOverrides()
incoming = DocumentMetadataOverrides(skip_asn=True)
overrides.update(incoming)
self.assertTrue(overrides.skip_asn)
def testOverrideTitlePlaceholders(self) -> None: def testOverrideTitlePlaceholders(self) -> None:
c = Correspondent.objects.create(name="Correspondent Name") c = Correspondent.objects.create(name="Correspondent Name")
dt = DocumentType.objects.create(name="DocType Name") dt = DocumentType.objects.create(name="DocType Name")
@@ -1264,46 +1271,3 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
r"sample\.pdf: Error while executing post-consume script: Command '\[.*\]' returned non-zero exit status \d+\.", r"sample\.pdf: Error while executing post-consume script: Command '\[.*\]' returned non-zero exit status \d+\.",
): ):
consumer.run_post_consume_script(doc) consumer.run_post_consume_script(doc)
class TestMetadataOverrides(TestCase):
def test_update_skip_asn_if_exists(self) -> None:
base = DocumentMetadataOverrides()
incoming = DocumentMetadataOverrides(skip_asn_if_exists=True)
base.update(incoming)
self.assertTrue(base.skip_asn_if_exists)
class TestBarcodeApplyDetectedASN(TestCase):
"""
GIVEN:
- Existing Documents with ASN 123
WHEN:
- A BarcodePlugin which detected an ASN
THEN:
- If skip_asn_if_exists is set, and ASN exists, do not set ASN
- If skip_asn_if_exists is set, and ASN does not exist, set ASN
"""
def test_apply_detected_asn_skips_existing_when_flag_set(self) -> None:
doc = Document.objects.create(
checksum="X1",
title="D1",
archive_serial_number=123,
)
metadata = DocumentMetadataOverrides(skip_asn_if_exists=True)
plugin = BarcodePlugin(
input_doc=mock.Mock(),
metadata=metadata,
status_mgr=mock.Mock(),
base_tmp_dir=Path(tempfile.gettempdir()),
task_id="test-task",
)
plugin._apply_detected_asn(123)
self.assertIsNone(plugin.metadata.asn)
doc.hard_delete()
plugin._apply_detected_asn(123)
self.assertEqual(plugin.metadata.asn, 123)

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-03 20:10+0000\n" "POT-Creation-Date: 2026-02-03 17:32+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -1323,7 +1323,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2330 documents/views.py:2839 #: documents/serialisers.py:2330 documents/views.py:2838
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1587,20 +1587,20 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:2851 #: documents/views.py:2850
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:2894 #: documents/views.py:2893
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:2951 #: documents/views.py:2950
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:2961 #: documents/views.py:2960
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""

View File

@@ -1362,7 +1362,7 @@ def _get_nltk_language_setting(ocr_lang: str) -> str | None:
The common intersection between all languages in those 3 is handled here The common intersection between all languages in those 3 is handled here
""" """
ocr_lang = ocr_lang.split("+", maxsplit=1)[0] ocr_lang = ocr_lang.split("+")[0]
iso_code_to_nltk = { iso_code_to_nltk = {
"dan": "danish", "dan": "danish",
"nld": "dutch", "nld": "dutch",

View File

@@ -1,5 +1,6 @@
import dataclasses import dataclasses
import email.contentmanager import email.contentmanager
import random
import time import time
import uuid import uuid
from collections import namedtuple from collections import namedtuple
@@ -147,7 +148,11 @@ class BogusMailBox(AbstractContextManager):
if "TO" in criteria: if "TO" in criteria:
to_ = criteria[criteria.index("TO") + 1].strip('"') to_ = criteria[criteria.index("TO") + 1].strip('"')
msg = filter(lambda m: any(to_ in to_addr for to_addr in m.to), msg) msg = []
for m in self.messages:
for to_addrs in m.to:
if to_ in to_addrs:
msg.append(m)
if "UNFLAGGED" in criteria: if "UNFLAGGED" in criteria:
msg = filter(lambda m: not m.flagged, msg) msg = filter(lambda m: not m.flagged, msg)
@@ -199,7 +204,7 @@ def fake_magic_from_buffer(buffer, *, mime=False):
class MessageBuilder: class MessageBuilder:
def __init__(self) -> None: def __init__(self) -> None:
self._next_uid = 1 self._used_uids = set()
def create_message( def create_message(
self, self,
@@ -252,8 +257,10 @@ class MessageBuilder:
# TODO: Unsure how to add a uid to the actual EmailMessage. This hacks it in, # TODO: Unsure how to add a uid to the actual EmailMessage. This hacks it in,
# based on how imap_tools uses regex to extract it. # based on how imap_tools uses regex to extract it.
# This should be a large enough pool # This should be a large enough pool
uid = self._next_uid uid = random.randint(1, 10000)
self._next_uid += 1 while uid in self._used_uids:
uid = random.randint(1, 10000)
self._used_uids.add(uid)
imap_msg._raw_uid_data = f"UID {uid}".encode() imap_msg._raw_uid_data = f"UID {uid}".encode()

480
uv.lock generated
View File

@@ -315,11 +315,11 @@ wheels = [
[[package]] [[package]]
name = "babel" name = "babel"
version = "2.18.0" version = "2.17.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
] ]
[[package]] [[package]]
@@ -576,6 +576,15 @@ wheels = [
{ 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" }, { 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" },
] ]
[[package]]
name = "cfgv"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]] [[package]]
name = "channels" name = "channels"
version = "4.3.2" version = "4.3.2"
@@ -922,7 +931,7 @@ wheels = [
[[package]] [[package]]
name = "dateparser" name = "dateparser"
version = "1.3.0" version = "1.2.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -930,9 +939,9 @@ dependencies = [
{ name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152, upload-time = "2026-02-04T16:00:06.162Z" } sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840, upload-time = "2025-06-26T09:29:23.211Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688, upload-time = "2026-02-04T16:00:04.652Z" }, { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" },
] ]
[[package]] [[package]]
@@ -968,6 +977,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" },
] ]
[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]] [[package]]
name = "distro" name = "distro"
version = "1.9.0" version = "1.9.0"
@@ -979,28 +997,28 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.2.11" version = "5.2.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" } sdist = { url = "https://files.pythonhosted.org/packages/e6/e5/2671df24bf0ded831768ef79532e5a7922485411a5696f6d979568591a37/django-5.2.10.tar.gz", hash = "sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe", size = 10880754, upload-time = "2026-01-06T18:55:26.817Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/f1a7cd896daec85832136ab509d9b2a6daed4939dbe26313af3e95fc5f5e/django-5.2.10-py3-none-any.whl", hash = "sha256:cf85067a64250c95d5f9067b056c5eaa80591929f7e16fbcd997746e40d6c45c", size = 8290820, upload-time = "2026-01-06T18:55:20.009Z" },
] ]
[[package]] [[package]]
name = "django-allauth" name = "django-allauth"
version = "65.14.0" version = "65.13.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/23/9b/061a6ac65c602eb721b13fbf9c665b20fb900f113a03ec8521b5fcf16b83/django_allauth-65.14.0.tar.gz", hash = "sha256:5529227aba2b1377d900e9274a3f24496c645e65400fbae3cad5789944bc4d0b", size = 1991909, upload-time = "2026-01-17T18:43:12.928Z" } sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/42a048ba1dedbb6b553f5376a6126b1c753c10c70d1edab8f94c560c8066/django_allauth-65.13.1.tar.gz", hash = "sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15", size = 1983857, upload-time = "2025-11-20T16:34:40.811Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/c8/2f959ff8466913d95ba72eb4a29bd7998d28a559786033a97b5bbdda2b81/django_allauth-65.14.0-py3-none-any.whl", hash = "sha256:448f5f7877f95fcbe1657256510fe7822d7871f202521a29e23ef937f3325a97", size = 1793052, upload-time = "2026-01-17T18:43:08.954Z" }, { url = "https://files.pythonhosted.org/packages/d8/98/9d44ae1468abfdb521d651fb67f914165c7812dfdd97be16190c9b1cc246/django_allauth-65.13.1-py3-none-any.whl", hash = "sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6", size = 1787349, upload-time = "2025-11-20T16:34:37.354Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1029,14 +1047,14 @@ wheels = [
[[package]] [[package]]
name = "django-cachalot" name = "django-cachalot"
version = "2.9.0" version = "2.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6c/d6/2f4033cc3443b7ce15459247488050cd0b8b56f15a03fcf2650b2483026e/django_cachalot-2.9.0.tar.gz", hash = "sha256:67d3d19a3f6deab7dc5b081b5f78ea926318f64a7b3afb06f28e61d8d31e02b3", size = 76922, upload-time = "2026-01-28T05:23:30.318Z" } sdist = { url = "https://files.pythonhosted.org/packages/f5/53/1f781e58028a43028d6c799f2eab15eff65e841e3e288d6f2953e36f01a4/django_cachalot-2.8.0.tar.gz", hash = "sha256:30456720ac9f3fabeb90ce898530fe01130c25a1eca911cd016cfaeab251d627", size = 74673, upload-time = "2025-04-17T00:05:36.387Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/bf/af8ad2aa5a402f278b444ca70729fb12ee96ddb89c19c32a2d7c5189358f/django_cachalot-2.9.0-py3-none-any.whl", hash = "sha256:b80ac4930613a7849988ea772a53598d262a15eaf55e5ec8c78accae7fdd99ff", size = 57814, upload-time = "2026-01-28T05:23:28.741Z" }, { url = "https://files.pythonhosted.org/packages/9a/05/f5846fd186189ac0a1deddb9c67450c838e5c8ceceb35b5260c61f622599/django_cachalot-2.8.0-py3-none-any.whl", hash = "sha256:315da766a5356c7968318326f7b0579f64571ad909f64cad0601f38153ca4e16", size = 55671, upload-time = "2025-04-17T00:05:34.641Z" },
] ]
[[package]] [[package]]
@@ -1264,14 +1282,14 @@ wheels = [
[[package]] [[package]]
name = "drf-spectacular-sidecar" name = "drf-spectacular-sidecar"
version = "2026.1.1" version = "2025.10.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1e/81/c7b0e3ccbd5a039c4f4fcfecf88391a666ca1406a953886e2f39295b1c90/drf_spectacular_sidecar-2026.1.1.tar.gz", hash = "sha256:6f7c173a8ddbbbdafc7a27e028614b65f07a89ca90f996a432d57460463b56be", size = 2468060, upload-time = "2026-01-01T11:27:12.682Z" } sdist = { url = "https://files.pythonhosted.org/packages/c3/e4/99cd1b1c8c69788bd6cb6a2459674f8c75728e79df23ac7beddd094bf805/drf_spectacular_sidecar-2025.10.1.tar.gz", hash = "sha256:506a5a21ce1ad7211c28acb4e2112e213f6dc095a2052ee6ed6db1ffe8eb5a7b", size = 2420998, upload-time = "2025-10-01T11:23:27.092Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/96/38725edda526f3e9e597f531beeec94b0ef433d9494f06a13b7636eecb6e/drf_spectacular_sidecar-2026.1.1-py3-none-any.whl", hash = "sha256:af8df62f1b594ec280351336d837eaf2402ab25a6bc2a1fad7aee9935821070f", size = 2489520, upload-time = "2026-01-01T11:27:11.056Z" }, { url = "https://files.pythonhosted.org/packages/ab/87/70c67391e4ce68715d4dfae8dd33caeda2552af22f436ba55b8867a040fe/drf_spectacular_sidecar-2025.10.1-py3-none-any.whl", hash = "sha256:f1de343184d1a938179ce363d318258fe1e5f02f2f774625272364835f1c42bd", size = 2440241, upload-time = "2025-10-01T11:23:25.743Z" },
] ]
[[package]] [[package]]
@@ -1287,7 +1305,7 @@ name = "exceptiongroup"
version = "1.3.1" version = "1.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'darwin')" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [ wheels = [
@@ -1524,83 +1542,83 @@ wheels = [
[[package]] [[package]]
name = "granian" name = "granian"
version = "2.7.0" version = "2.6.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/43/75/bdea4ab49a02772a3007e667284764081d401169e96d0270d95509e3e240/granian-2.7.0.tar.gz", hash = "sha256:bee8e8a81a259e6f08613c973062df9db5f8451b521bb0259ed8f27d3e2bab23", size = 127963, upload-time = "2026-02-02T11:39:57.525Z" } sdist = { url = "https://files.pythonhosted.org/packages/aa/22/93016f4f9e9115ba981f51fc17c7c369a34772f11a93043320a2a3d5c6ea/granian-2.6.1.tar.gz", hash = "sha256:d209065b12f18b6d7e78f1c16ff9444e5367dddeb41e3225c2cf024762740590", size = 115480, upload-time = "2026-01-07T11:08:55.927Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/c5/0c4469c7eb6d42e8ad3706e8f918fdbed1e79f4b2061749ed89568b84746/granian-2.7.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:442ced696212b1964579a89bb40bd504822ad87326f95488d8117fff0c21cba9", size = 4581109, upload-time = "2026-02-02T11:37:55.991Z" }, { url = "https://files.pythonhosted.org/packages/54/3f/9a78d70beaa2dafc54c2147dd03ce1b75a97d12b61e9319eacb6ad536e30/granian-2.6.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b8a9f6006142ed64082ec726a9de40f4eb2ebe65f842c199f254b253362b8ab4", size = 3073952, upload-time = "2026-01-07T11:06:51.277Z" },
{ url = "https://files.pythonhosted.org/packages/4f/dd/7954ec98c82e0b3e954a9b5263fcd77258b502bf0e021f2c5e8a84da87b6/granian-2.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c07ebe3f8b7284264475ac4ee58839c0f3f1d3f292b40f8ea50363bf8277ef5", size = 4210308, upload-time = "2026-02-02T11:37:57.987Z" }, { url = "https://files.pythonhosted.org/packages/dd/40/52e01a382d58ba416d12e165a2ac1e3270367267ced12ddfe1dd1fb03b65/granian-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:811a8eec0f96c7a1c9f991a2922f7c21561ecb2c52263666e14474aeea37ff6b", size = 2827739, upload-time = "2026-01-07T11:06:52.637Z" },
{ url = "https://files.pythonhosted.org/packages/70/e8/62a48b7f0092f465cd24c71e7c990c55554095c6cad5c97975009e5ef223/granian-2.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7817854489e944e8d0cdfe4695b5605a3b042ae169fc43505c6f6c5b05d8c3a", size = 5129886, upload-time = "2026-02-02T11:37:59.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/65/439a866076b378611eeebc3ecb285cc606aad4046b3f3b26035cc82d8c8b/granian-2.6.1-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:3b78caa06c44d73551038aba9918d03d59f4d37e2bf39623e5572800d110e121", size = 3327232, upload-time = "2026-01-07T11:06:54.813Z" },
{ url = "https://files.pythonhosted.org/packages/09/28/43d8fcfad80ddb38a9311538de93f5225a5e28db8b7fc31146199f6d8cdd/granian-2.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d9f4dc1e575d15ea8c0f433dc4190efa69d259bf59a136f009b28de223ef177", size = 4576645, upload-time = "2026-02-02T11:38:00.456Z" }, { url = "https://files.pythonhosted.org/packages/7e/7b/e9611920b7a4c3fe4881fa204322eb3944decd57ce3b38db2a3da00bb876/granian-2.6.1-cp310-cp310-manylinux_2_24_i686.whl", hash = "sha256:7cce9865bab5c2ca29d80224caad390b15c385ec23903bf0781f4cc6cee006c6", size = 3140534, upload-time = "2026-01-07T11:06:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/a4/66/5409d0049b0878775e50b8f4e1bc71cb1780f895632e5c7d08c558b3fec2/granian-2.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:960908d8a2fb3cc5472fdd3de52a342d6fd76a12146e2447b38706bb01c31f76", size = 4975287, upload-time = "2026-02-02T11:38:02.136Z" }, { url = "https://files.pythonhosted.org/packages/46/a7/3840c2a9ed7fcf1c606dc8796aa3c4e43bcc4169ba04d2f840d576d8a238/granian-2.6.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:b6df9ffdcbd85e4151661e878127186a6a54a8ba4048cf5f883d9093ae628b99", size = 3372279, upload-time = "2026-01-07T11:06:57.483Z" },
{ url = "https://files.pythonhosted.org/packages/58/eb/a6c1992d2902db13cca3637758b5e1064e70ee2342f6d8b66b80ba805f1f/granian-2.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b974e28265e2001539446f456973e6dc8aaeee8c61d31233d0a55d34c2f5482c", size = 4827993, upload-time = "2026-02-02T11:38:05.036Z" }, { url = "https://files.pythonhosted.org/packages/3e/e8/4b78ed5fab45e83c69f6e1ea8805dbc638e7694d7946e97495c507026e6b/granian-2.6.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66e5e25bc901c4bd54ab0f6449318c678e49ef6dcfd4fd62f15188680ed9a24d", size = 3239371, upload-time = "2026-01-07T11:06:59.49Z" },
{ url = "https://files.pythonhosted.org/packages/0a/8a/0dbafb77124c3534048879fd7466f880fcff3606e63ebdac94633a3694c3/granian-2.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a83a107ce9bcb9609fa3e808f39e3d940cd3933f6c54d7b6c37053767a862b9a", size = 4939190, upload-time = "2026-02-02T11:38:06.426Z" }, { url = "https://files.pythonhosted.org/packages/76/7d/a9e70763e9c99158edcdaca74c4b4fd4d57a46d2ea39a0ef32df4e8262f3/granian-2.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5429fb374accfa48f6c6459b3e0278f3ad58bba701127b62163086b70c076d11", size = 3309145, upload-time = "2026-01-07T11:07:01.166Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c5/ec1910ca216429e091dd39d07d9053592a38a0e75d25bdcf79cf77fc8d79/granian-2.7.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:aa4876903c4c80b26c23231ff71437b1c7489504c4e1c18642ab6898926e2e38", size = 5292735, upload-time = "2026-02-02T11:38:09.455Z" }, { url = "https://files.pythonhosted.org/packages/95/07/c899ba39d1be5810e25981d7c72a53437e2d099393fad9c8e6c273690f05/granian-2.6.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:8276b24da71771bc282057f9d144452695d54a99522adcf3a02175e613929d09", size = 3492752, upload-time = "2026-01-07T11:07:03.018Z" },
{ url = "https://files.pythonhosted.org/packages/a0/0f/778a5cb91502f1d813d135b0a0c99a6b7d46cdb00af0b9d7e3b8c1b557c5/granian-2.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:be9638b47c9554104eebc99ef9313d016f37ee6e37e03d1d65f1fa495880856d", size = 5087382, upload-time = "2026-02-02T11:38:11.272Z" }, { url = "https://files.pythonhosted.org/packages/1c/99/590ad3ad2f97c0e2f32558f0cf630ab72aa7c71f94e865084f3fb89b7cc5/granian-2.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6a3248c965f6ca71dca1285c7f5e16e4209a779a218fc2f64fe2f6cbe5ace709", size = 3498818, upload-time = "2026-01-07T11:07:04.536Z" },
{ url = "https://files.pythonhosted.org/packages/8a/28/a3ee3f2220c0b9045f8caa2a2cb7484618961b7500f88594349a7889d391/granian-2.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e76afb483d7f42a0b911bdb447d282f70ad7a96caabd4c99cdc300117c5f8977", size = 4580966, upload-time = "2026-02-02T11:38:14.077Z" }, { url = "https://files.pythonhosted.org/packages/66/a3/12e30c4a16761f6db3cff71a93351678dca535c6348d3c1f65f6461c8848/granian-2.6.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66cb53293a721bf2da651150cb5ba501d5536c3bec284dcbcb10a9f044a4b23e", size = 3073572, upload-time = "2026-01-07T11:07:07.447Z" },
{ url = "https://files.pythonhosted.org/packages/1b/60/b53da9c255f6853a5516d0f8a3e7325c24123f0f7e77856558c49810f4ce/granian-2.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:628523302274f95ca967f295a9aa7bc4ade5e1eced42afc60d06dfe20f2da07a", size = 4210344, upload-time = "2026-02-02T11:38:15.34Z" }, { url = "https://files.pythonhosted.org/packages/11/c0/0582a42e5b3e3c9e34eb9060585ed6cd11807852d645c19a0a79953be571/granian-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fef1c4b75d3827101a103b134abf43076703b6143b788f022e821dc4180b602", size = 2827569, upload-time = "2026-01-07T11:07:08.964Z" },
{ url = "https://files.pythonhosted.org/packages/a2/bb/c3380106565bc99edfb90baafa1a8081a4334709ce0200d207ddda36275e/granian-2.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a62560b64a17e1cbae61038285d5fa8a32613ada9a46f05047dc607ea7d38f23", size = 5130258, upload-time = "2026-02-02T11:38:17.175Z" }, { url = "https://files.pythonhosted.org/packages/db/b8/306dad81288330c5c1043434ac57a246a7cd3a70cd5877570efcd7888879/granian-2.6.1-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:2375c346cafd2afd944a8b014f7dd882b416161ffe321c126d6d87984499396c", size = 3326925, upload-time = "2026-01-07T11:07:10.288Z" },
{ url = "https://files.pythonhosted.org/packages/a2/8f/2c3348d6d33807e3b818ac07366b5251e811ce2548fbe82e0b55982d8a13/granian-2.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47b8e0e9497d24466d6511443cc18f22f18405aab5a7e2fece1dd38206af88c4", size = 4576496, upload-time = "2026-02-02T11:38:18.577Z" }, { url = "https://files.pythonhosted.org/packages/ad/bb/f76654e4e5679d000335101922653c809adacaa675f861646aef95e9673c/granian-2.6.1-cp311-cp311-manylinux_2_24_i686.whl", hash = "sha256:6c0e9367956c1cdd23b41d571159e59b5530c8f44ff4c340fe69065ffd1bfe70", size = 3140557, upload-time = "2026-01-07T11:07:11.764Z" },
{ url = "https://files.pythonhosted.org/packages/f6/71/d1d146170a23f3523d8629b47f849b30ba0d513eb519188ce5d7bfd1b916/granian-2.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc6039c61a07b2d36462c487b66b131ae3fd862bdc8fb81d6e5c206c1a2b683c", size = 4975062, upload-time = "2026-02-02T11:38:20.084Z" }, { url = "https://files.pythonhosted.org/packages/1c/0d/2e6ab1ce28fbb45f8e747d33db06ea870c1eee580c584592a8ceb49c0a59/granian-2.6.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:4eacfe0bf355a88486933e6f846c2ecc0c2b0cf020a989750765294da4216b0c", size = 3372055, upload-time = "2026-01-07T11:07:13.584Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/f3acbf8c41cd10ff81109bd9078d3228f23e52bab8673763c65739a87e30/granian-2.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f3b0442beb11b035ee09959726f44b3730d0b55688110defd1d9a9a6c7486955", size = 4827755, upload-time = "2026-02-02T11:38:21.817Z" }, { url = "https://files.pythonhosted.org/packages/5d/05/9f104225ef0ceef6770e12d476077656c7930cde84474797c4a9807a4d3d/granian-2.6.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c6ac45432028c7799a083917cda567e377cf42dbcad45c24b66dd03b72b1e1d6", size = 3239306, upload-time = "2026-01-07T11:07:15.01Z" },
{ url = "https://files.pythonhosted.org/packages/9f/f8/503135b89539feea2be495b47858c22409ba77ffcb71920ae0727c674189/granian-2.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:741d0b58a5133cc5902b3129a8a4c55143f0f8769a80e7aa80caadc64c9f1d8b", size = 4939033, upload-time = "2026-02-02T11:38:23.033Z" }, { url = "https://files.pythonhosted.org/packages/8d/ec/73ead13fe326ac548fda5f85f471e16015629672e8acc3d4ccc07e9b313a/granian-2.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aaac0304c7c68e6b798d15dd96a7b6ae477ab89d519c398d470da460f7ddda0", size = 3309025, upload-time = "2026-01-07T11:07:16.445Z" },
{ url = "https://files.pythonhosted.org/packages/99/90/aaabe2c1162d07a6af55532b6f616199aa237805ef1d732fa78d9883d217/granian-2.7.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:02a6fe6a19f290b70bc23feeb3809511becdaff2263b0469f02c28772af97652", size = 5292980, upload-time = "2026-02-02T11:38:24.823Z" }, { url = "https://files.pythonhosted.org/packages/f4/fb/0f474c6d437464d440924f3261295c046365dc9514cdd898d152b5a6c0bd/granian-2.6.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a356685207987c07fb19f76a04d7bac6da0322584ede37adb1af7a607f8c8e35", size = 3492393, upload-time = "2026-01-07T11:07:18.202Z" },
{ url = "https://files.pythonhosted.org/packages/eb/aa/d1eb7342676893ab0ec1e66cceca4450bec3f29c488db2a92af5b4211d4d/granian-2.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8239b1a661271428c3e358e4bdcaaaf877a432cc593e93fc6b5a612ae521b06a", size = 5087230, upload-time = "2026-02-02T11:38:26.09Z" }, { url = "https://files.pythonhosted.org/packages/30/92/dbd3793e3b02d0a09422dacd456d739039eba4147d2c716e601f87287fde/granian-2.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2928057de92ef90c2d87e3de5e34af4e50d746c5adfb035a6bbef490ec465af", size = 3498644, upload-time = "2026-01-07T11:07:19.943Z" },
{ url = "https://files.pythonhosted.org/packages/97/1a/b6d7840bfd9cd9bed627b138e6e8e49d1961997adba30ee39ad75d07ed58/granian-2.7.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d9c42562dcbf52848d0a9d0db58f8f2e790586eb0c363b8ad1b30fe0bd362117", size = 4572728, upload-time = "2026-02-02T11:38:30.143Z" }, { url = "https://files.pythonhosted.org/packages/50/d1/9d191ea0b4f01a0d2437600b32a025e687189bae072878ec161f358eb465/granian-2.6.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:801bcf7efc3fdd12a08016ed94b1a386480c9a5185eb8e017fd83db1b2d210b4", size = 3070339, upload-time = "2026-01-07T11:07:22.618Z" },
{ url = "https://files.pythonhosted.org/packages/15/93/f8f7224d9eaaaf4dbf493035a85287fa2e27c17e5f7aacc01821d8aa66b4/granian-2.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3421bd5c90430073e1f3f88fc63bc8d0a8ee547a9a5c06d577a281f384160bd", size = 4195034, upload-time = "2026-02-02T11:38:32.007Z" }, { url = "https://files.pythonhosted.org/packages/c3/1e/be0ba55a2b21aeadeb8774721964740130fdd3dd7337d8a5ec130a0c48c0/granian-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:853fb869a50d742576bb4f974f321242a71a4d8eed918939397b317ab32c6a2d", size = 2819049, upload-time = "2026-01-07T11:07:23.877Z" },
{ url = "https://files.pythonhosted.org/packages/4b/db/66843a35e1b6345da2a1c71839fb9aa7eb0f17d380fbf4cb5c7e06eb6f85/granian-2.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b8057dc81772932e208f2327b5e347459eb78896118e27af9845801e267cec5", size = 5123768, upload-time = "2026-02-02T11:38:33.449Z" }, { url = "https://files.pythonhosted.org/packages/78/c7/d8adb472dc71b212281a82d3ea00858809f2844a79b45e63bbb3a09921b7/granian-2.6.1-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:327a6090496c1deebd9e315f973bdbfc5c927e5574588bba918bfe2127bbd578", size = 3322325, upload-time = "2026-01-07T11:07:25.304Z" },
{ url = "https://files.pythonhosted.org/packages/10/ce/631c5c1f7a4e6b8c98ec857b3e6795fe64e474b6f48df388ac701a21f3fe/granian-2.7.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5e70f438b1a4787d76566770e98bf7732407efa02802f38f10c960247107d7", size = 4562424, upload-time = "2026-02-02T11:38:34.815Z" }, { url = "https://files.pythonhosted.org/packages/de/2f/c3ce9e4f19163f35c5c57c45af2ad353abcc6091a44625caec56e065ca4a/granian-2.6.1-cp312-cp312-manylinux_2_24_i686.whl", hash = "sha256:4c91f0eefc34d809773762a9b81c1c48e20ff74c0f1be876d1132d82c0f74609", size = 3136460, upload-time = "2026-01-07T11:07:26.682Z" },
{ url = "https://files.pythonhosted.org/packages/28/41/19bdfa3719e22c4dcf6fa1a53323551a37aa58a4ca7a768db6a0ba714ab0/granian-2.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213dd224a47c7bfcbb91718c7eeb56d6067825a28dcae50f537964e2dafb729a", size = 5006002, upload-time = "2026-02-02T11:38:36.76Z" }, { url = "https://files.pythonhosted.org/packages/3d/87/91b57eb5407a12bfe779acfa3fbb2be329aec14e6d88acf293fe910c19e5/granian-2.6.1-cp312-cp312-manylinux_2_24_x86_64.whl", hash = "sha256:c5754de57b56597d5998b7bb40aa9d0dc4e1dbeb5aea3309945126ed71b41c6d", size = 3386850, upload-time = "2026-01-07T11:07:27.989Z" },
{ url = "https://files.pythonhosted.org/packages/3c/5b/3b40f489e2449eb58df93ad38f42d1a6c2910502a4bc8017c047e16d637c/granian-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bb5be27c0265268d43bab9a878ac27a20b4288843ffc9fda1009b8226673f629", size = 4825073, upload-time = "2026-02-02T11:38:37.998Z" }, { url = "https://files.pythonhosted.org/packages/f0/43/b61a6f3bfc2f35e504e42789776a269cbdc0cdafdb10597bd6534e93ba3d/granian-2.6.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e849d6467ebe77d0a75eb4175f7cc06b1150dbfce0259932a4270c765b4de6c4", size = 3240693, upload-time = "2026-01-07T11:07:29.52Z" },
{ url = "https://files.pythonhosted.org/packages/04/92/b6de6f8c4146409efb58aee75277b810d54de03a1687d33f1f3f1feb3395/granian-2.7.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a6ff95aede82903c06eb560a32b10e9235fdafc4568c8fe7dcac28d62be5ffa2", size = 4928628, upload-time = "2026-02-02T11:38:39.481Z" }, { url = "https://files.pythonhosted.org/packages/d1/1d/c40bd8dd99b855190d67127e0610f082cfbc7898dbd41f1ade015c2041f7/granian-2.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5a265867203e30d3c54d9d99783346040681ba2aaec70fcbe63de0e295e7882f", size = 3312703, upload-time = "2026-01-07T11:07:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/39/21/d8a191dcfbf8422b868ab847829670075ba3e4325611e0a9fd2dc909a142/granian-2.7.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e44f0c1676b27582df26d47cf466fedebd72f520edc2025f125c83ff58af77f9", size = 5282898, upload-time = "2026-02-02T11:38:40.815Z" }, { url = "https://files.pythonhosted.org/packages/a1/ca/589c042afc3287b36dfeed6df56074cc831a94e5217bcbd7c1af20812fe2/granian-2.6.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:03f0a1505e7862183203d7d7c1e2b29349bd63a18858ced49aec4d7aadb98fc8", size = 3483737, upload-time = "2026-01-07T11:07:32.726Z" },
{ url = "https://files.pythonhosted.org/packages/d0/46/2746f1a4f0f093576fb64b63c3f022f254c6d2c4cc66d37dd881608397ce/granian-2.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9241b72f95ceb57e2bbce55e0f61c250c1c02e9d2f8531b027dd3dc204209fdd", size = 5118453, upload-time = "2026-02-02T11:38:42.716Z" }, { url = "https://files.pythonhosted.org/packages/6f/51/72eb037bac01db9623fa5fb128739bfb5679fb90e6da2645c5a3d8a4168d/granian-2.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:703ed57ba134ab16f15d49f7d644329db1cb0f7f8114ec3f08fb8039850e308a", size = 3514745, upload-time = "2026-01-07T11:07:34.706Z" },
{ url = "https://files.pythonhosted.org/packages/f8/df/b68626242fb4913df0968ee5662f5a394857b3d6fc4ee17c94be69664491/granian-2.7.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:bc61451791c8963232e4921c6805e7c2e366635e1e658267b1854889116ff6d7", size = 4572200, upload-time = "2026-02-02T11:38:46.194Z" }, { url = "https://files.pythonhosted.org/packages/4b/3f/40975a573dc9a80382121694d71379fffab568012f411038043ed454cdd0/granian-2.6.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8af4c75ffa2c8c77a3f5558b5ff71a9d97a57e08387ef954f560f2412a0b3db9", size = 3069408, upload-time = "2026-01-07T11:07:38.4Z" },
{ url = "https://files.pythonhosted.org/packages/c0/15/2fe28bca0751d9dc46e5c7e9e4b0c4fd1a55e3e8ba062f28292322ee160b/granian-2.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e274a0d6a01c475b9135212106ca5b69f5ec2f67f4ca6ce812d185d80255cdf5", size = 4195415, upload-time = "2026-02-02T11:38:47.78Z" }, { url = "https://files.pythonhosted.org/packages/a0/2f/64b0d58344eedfb7f27c48d7a40e840cd714a8898bcaf3289cecad02f030/granian-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b473cdaf3f19ddc16e511b2c2d1e98b9ce7c13fd105e9095ecb268a6a5286a32", size = 2818749, upload-time = "2026-01-07T11:07:39.946Z" },
{ url = "https://files.pythonhosted.org/packages/07/2a/d4dc40e58a55835cac5296f5090cc3ce2d43332ad486bbf78b3a00e46199/granian-2.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34bd28075adae3453c596ee20089e0288379e3fdf1cec8bafff89bb175ea0eb4", size = 5122981, upload-time = "2026-02-02T11:38:49.55Z" }, { url = "https://files.pythonhosted.org/packages/6b/1e/e33ae736adbef0633307f13092490021688df33885c9a320b50b83dbc076/granian-2.6.1-cp313-cp313-manylinux_2_24_armv7l.whl", hash = "sha256:b8ca2ac7261bcb8e57a35f8e7202aa891807420d51e3e61fd0913088d379e0fd", size = 3321824, upload-time = "2026-01-07T11:07:41.379Z" },
{ url = "https://files.pythonhosted.org/packages/bd/fe/8c79837df620dc0eca6a8b799505910cbba2d85d92ccc58d1c549f7027be/granian-2.7.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f526583b72cf9e6ca9a4849c781ed546f44005f0ad4b5c7eb1090e1ebec209bf", size = 4561440, upload-time = "2026-02-02T11:38:50.799Z" }, { url = "https://files.pythonhosted.org/packages/59/3c/ef25189d251d14c81b11514e8d0a9a3cd8f9a467df423fb14362d95c7d6a/granian-2.6.1-cp313-cp313-manylinux_2_24_i686.whl", hash = "sha256:1eca9cfcf05dc54ffb21b14797ed7718707f7d26e7c5274722212e491eb8a4a6", size = 3136201, upload-time = "2026-01-07T11:07:42.703Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e7/d7abfaa9829ff50cddc27919bd3ce5a335402ebbbaa650e96fe579136674/granian-2.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ac07d5314e03e667210349dfc76124d69726731007c24716e21a2554cc15ca", size = 5005076, upload-time = "2026-02-02T11:38:52.157Z" }, { url = "https://files.pythonhosted.org/packages/4f/31/a5621235cd26e7cb78d57ce9a3baeb837885dc7ebf5c2c907f2bf319002a/granian-2.6.1-cp313-cp313-manylinux_2_24_x86_64.whl", hash = "sha256:7722373629ab423835eb25015223f59788aa077036ea9ac3a4bddce43b0eb9c9", size = 3386378, upload-time = "2026-01-07T11:07:44.289Z" },
{ url = "https://files.pythonhosted.org/packages/1a/45/108afaa0636c93b6a8ff12810787e4a1ea27fffe59f12ca0de7c784b119a/granian-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f6812e342c41ca80e1b34fb6c9a7e51a4bbd14f59025bd1bb59d45a39e02b8d5", size = 4825142, upload-time = "2026-02-02T11:38:53.506Z" }, { url = "https://files.pythonhosted.org/packages/22/ce/134a494be1c4d8a602cc03298e3f961d66e4a2b97c974403ffce50c09965/granian-2.6.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7de143cf76934cfc63cc8cf296af69f750e4e3799ec0700d5da8254202aad12a", size = 3240009, upload-time = "2026-01-07T11:07:46.18Z" },
{ url = "https://files.pythonhosted.org/packages/4b/eb/cedf4675b1047490f819ce8bd1ee1ea74b6c772ae9d9dd1c117ae690a3eb/granian-2.7.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a4099ba59885123405699a5313757556ff106f90336dccdf4ceda76f32657d0", size = 4927830, upload-time = "2026-02-02T11:38:54.92Z" }, { url = "https://files.pythonhosted.org/packages/c2/f5/48f2fff5effee50dce30d726874936d582e83a690ccdc87cc2ca15b9accf/granian-2.6.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b0e7d6ee92962544a16039e1d5f36c5f43cd0a538541e7a3e5337147c701539", size = 3312533, upload-time = "2026-01-07T11:07:48.176Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b5/2d7a2e03ba29a6915ad41502e2870899b9eb54861e3d06ad8470c5e70b41/granian-2.7.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c487731fbae86808410e88c587eb4071213812c5f52570b7981bf07a1b84be25", size = 5282142, upload-time = "2026-02-02T11:38:56.445Z" }, { url = "https://files.pythonhosted.org/packages/a2/90/0590100351bf2b99ff95b0ac34c8c14f61c13f8417c16b93860fe8b1619a/granian-2.6.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:d9ca799472c72cb378192d49004abe98e387c1719378b01d7dc85ab293fa680e", size = 3482213, upload-time = "2026-01-07T11:07:49.471Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e7/c851b2e2351727186b4bc4a35df832e2e97e4f77b8a93dfdb6daa098cf9e/granian-2.7.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ca4877ebf8873488ba72a299206621bd0c6febb8f091f3da62117c1fe344501f", size = 5117907, upload-time = "2026-02-02T11:38:57.852Z" }, { url = "https://files.pythonhosted.org/packages/22/12/0f8f1367ebc4bbd3e17bed842ad21cf9691d828b8c89029dfcf9f1448fcd/granian-2.6.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:616c3213d2ffe638e49a3578185cbe9d0009635949e7ac083275942a2cbbee0c", size = 3513942, upload-time = "2026-01-07T11:07:51.011Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2f/c9bcd4aa36d3092fe88a623e60aa89bd4ff16836803a633b8b454946a845/granian-2.7.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:e1df8e4669b4fb69b373b2ab40a10a8c511eeb41838d65adb375d1c0e4e7454c", size = 4493110, upload-time = "2026-02-02T11:39:01.294Z" }, { url = "https://files.pythonhosted.org/packages/64/ec/49ada0ed6861db9ba127c26058cc1a8451af891922cb2673b9e88e847cf6/granian-2.6.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:fd3151933d867352b6e240b0e57d97b96cd6e0fa010c9e3503f4cb83e6815f6b", size = 3030683, upload-time = "2026-01-07T11:07:53.803Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b4/02d11870255920d35f8eab390e509d3688fe0018011bb606aa00057b778f/granian-2.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6331ed9d3eb06cfba737dfb8efa3f0a8b4d4312a5af91c0a67bfbaa078b62eb4", size = 4122388, upload-time = "2026-02-02T11:39:02.509Z" }, { url = "https://files.pythonhosted.org/packages/71/7e/4c8b9eb66555e95798b8cf5e18be910519daf6cdf3c8cac90333ffe8e031/granian-2.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1d11bb744878fa73197a1af99b324b6ccd9368f2c73b82a6c4cfcc696c14dcde", size = 2772412, upload-time = "2026-01-07T11:07:55.605Z" },
{ url = "https://files.pythonhosted.org/packages/98/50/dfad5a414a2e3e14c30cd0d54cef1dab4874a67c1e6f8b1124d9998ed8b2/granian-2.7.0-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:093e1c277eddba00eaa94ca82ff7a9ab57b0554cd7013e5b2f3468635dbe520d", size = 4379344, upload-time = "2026-02-02T11:39:04.489Z" }, { url = "https://files.pythonhosted.org/packages/15/20/013495365505da26d45721b670114855a8969f1d3784ecdc60a124330075/granian-2.6.1-cp313-cp313t-manylinux_2_24_armv7l.whl", hash = "sha256:fe1103a49cdb75bbac47005f0a70353aa575b6ac052e9dc932b39b644358c43a", size = 3317657, upload-time = "2026-01-07T11:07:57.472Z" },
{ url = "https://files.pythonhosted.org/packages/6e/53/ef086af03ef31aa3c1dbff2da5928a9b5dd1f48d8ebee18dd6628951ae9e/granian-2.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e8e317bdc9ca9905d0b20f665f8fe31080c7f13d90675439113932bb3272c24", size = 5069172, upload-time = "2026-02-02T11:39:05.757Z" }, { url = "https://files.pythonhosted.org/packages/63/1e/4070bb26876c12b1304da9d27d6309e3dc53bbdf5928d732ba9a87fe561a/granian-2.6.1-cp313-cp313t-manylinux_2_24_i686.whl", hash = "sha256:e965d85634e02fbf97960e4ba8ef5290d37c094ad089a89fb19b68958888297a", size = 2969120, upload-time = "2026-01-07T11:07:58.778Z" },
{ url = "https://files.pythonhosted.org/packages/c3/57/117864ea46c6cbcbeff733a4da736e814b06d6634beeb201b9db176bd6be/granian-2.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:391e8589265178fd7f444b6711b6dda157a6b66059a15bf1033ffceeaf26918c", size = 4848246, upload-time = "2026-02-02T11:39:07.048Z" }, { url = "https://files.pythonhosted.org/packages/08/3e/dd29f04737a554acd3309372c8d2c6901a99cac3f9fcdee73bbe5a9bf0aa/granian-2.6.1-cp313-cp313t-manylinux_2_24_x86_64.whl", hash = "sha256:570c509cf608d77f0a32d66a51c9e4e9aba064a0a388776c41398392cc5e58d3", size = 3263555, upload-time = "2026-01-07T11:08:00.133Z" },
{ url = "https://files.pythonhosted.org/packages/60/da/2d45b7b6638a77362228d6770a61fa2bc3feae6c52a80993c230f344b197/granian-2.7.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:49b6873f4a8ee7a1ea627ff98d67ecdd644cfc18aab475b2e15f651dbcbe4140", size = 4669023, upload-time = "2026-02-02T11:39:09.612Z" }, { url = "https://files.pythonhosted.org/packages/f2/b6/ad4c439a8a20bebbed5066d051150ed0ad32160aee89c0cbdcf49fb1b092/granian-2.6.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3a105aa2f9b6dba037f81bc36b49a61c1648b60a82020e5c34394ce0940cdaef", size = 3112547, upload-time = "2026-01-07T11:08:01.453Z" },
{ url = "https://files.pythonhosted.org/packages/22/69/49e54eb6ed67ccf471c19d4c65f64197dd5a416d501620519e28ea92c82e/granian-2.7.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:39778147c7527de0bcda12cd9c38863d4e6a80d3a8a96ddeb6fe2d1342f337db", size = 4896002, upload-time = "2026-02-02T11:39:10.996Z" }, { url = "https://files.pythonhosted.org/packages/b8/92/58b5c7ca48540232034f2fa8be839e2ec997ec723489ff704a02c5a19945/granian-2.6.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:a4d90804f8d7c61e01741d9ccd400257763670f0e52a3cb59829fe2465b2b4a1", size = 3304759, upload-time = "2026-01-07T11:08:02.967Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f1/a864a78029265d06a6fd61c760c8facf032be0d345deca5081718cbb006f/granian-2.7.0-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:8135d0a4574dc5a0acf3a815fc6cad5bbe9075ef86df2c091ec34fbd21639c1c", size = 5239945, upload-time = "2026-02-02T11:39:12.726Z" }, { url = "https://files.pythonhosted.org/packages/a6/e0/fdcf7d91937acf6bfbe6c459820ffc847840d0375624daf1fcd3e2acea50/granian-2.6.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:647a818814c6c2a4bcd5757509596d9d5a0e10fbe96d52acb4c992f56134ae27", size = 3479270, upload-time = "2026-01-07T11:08:04.337Z" },
{ url = "https://files.pythonhosted.org/packages/26/33/feef40e4570b771d815c1ddd1008ccc9c0e81ce5a015deded6788e919f18/granian-2.7.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:47df2d9e50f22fa820b34fd38ceeeedc0b97994fa164425fa30e746759db8a44", size = 5078968, upload-time = "2026-02-02T11:39:14.048Z" }, { url = "https://files.pythonhosted.org/packages/a8/f3/d40f9e8699c9bb6ebf923e6baee6c9f90b9ff997c075173f67ffdee9aefc/granian-2.6.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:20639cec0106f047147c6b873bce6aaa275fb71456810ce3c0124f35b149e408", size = 3509699, upload-time = "2026-01-07T11:08:06.215Z" },
{ url = "https://files.pythonhosted.org/packages/b9/6a/b8d58474bbcbca450f030fd41b65c94ae0afb5e8f58c39fbea2df4efee2b/granian-2.7.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:23c6531b75c94c7b533812aed4f40dc93008c406cfa5629ec93397cd0f6770cb", size = 4569780, upload-time = "2026-02-02T11:39:16.671Z" }, { url = "https://files.pythonhosted.org/packages/55/49/2a3f993a2ee5e9876811a9d5fc41f47582da9a6873b02f214271e68f539b/granian-2.6.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:021ed3abb6805be450136f73ddc15283fe83714135d4481f5c3363c12109ef43", size = 3067322, upload-time = "2026-01-07T11:08:09.136Z" },
{ url = "https://files.pythonhosted.org/packages/c2/dc/a8b11425ebdf6cb58e1084fdb7759d853ca7f0b00376e4bb66300322f5d3/granian-2.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e4939b86f2b7918202ce56cb01c2efe20a393c742d41640b444e82c8b444b614", size = 4195285, upload-time = "2026-02-02T11:39:18.596Z" }, { url = "https://files.pythonhosted.org/packages/04/86/ad139301cc61c97ef8d8ec9a96e796350f108fa434c088ea14faa5e43e81/granian-2.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:df10c8fb56e00fd51aaccb639a34d067ec43cfbf1241d57d93ac67f0dbebd33d", size = 2818936, upload-time = "2026-01-07T11:08:11.091Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b5/6cc0b94f997d93f4b1510b2d953f07a7f1d16a143d60b53e0e50b887fa12/granian-2.7.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38fa10adf3c4d50e31a08401e6701ee2488613d905bb316cad456e5ebad5aa81", size = 5121311, upload-time = "2026-02-02T11:39:20.092Z" }, { url = "https://files.pythonhosted.org/packages/f0/a7/5223956aed93671dd83094cb4883e6431701e07a7641382b8cde6f4328d3/granian-2.6.1-cp314-cp314-manylinux_2_24_armv7l.whl", hash = "sha256:8f0eaacf8c6f1be67b38022d9839b35040e5a23df6117a08be5fc661ca404200", size = 3319401, upload-time = "2026-01-07T11:08:12.817Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f9/df3d862874cf4b233f97253bb78991ae4f31179a5581beaa41a2100e3bce/granian-2.7.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b366a9fd713a20321e668768b122b7b0140bfaeb3cb0557b6cb11dce827a4fb", size = 4557737, upload-time = "2026-02-02T11:39:21.992Z" }, { url = "https://files.pythonhosted.org/packages/90/ab/cbd1e9d9d0419b8d55043c821ea5b431d9cd43b35928c4c9f2034f9abf34/granian-2.6.1-cp314-cp314-manylinux_2_24_i686.whl", hash = "sha256:2f987bdb76a78a78dee56e9b4e44862471dcdbc3549152e35c64b19ba09c4dd0", size = 3132180, upload-time = "2026-01-07T11:08:14.215Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7f/e3063368345f39188afe5baa1ab62fdd951097656cd83bec3964f91f6e66/granian-2.7.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a916413e0dcd5c6eaf7f7413a6d899f7ba53a988d08e3b3c7ab2e0b5fa687559", size = 5004108, upload-time = "2026-02-02T11:39:23.306Z" }, { url = "https://files.pythonhosted.org/packages/c4/cb/4e4aa44e696c10a701c1366b9882561d6ba5caa2e3c00e3716a57da40c18/granian-2.6.1-cp314-cp314-manylinux_2_24_x86_64.whl", hash = "sha256:ffb814d4f9df8f04759c4c0fc1c903b607d363496e9d5fcb29596ab7f82bfae0", size = 3383751, upload-time = "2026-01-07T11:08:15.775Z" },
{ url = "https://files.pythonhosted.org/packages/bc/eb/892bcc0cfc44ed791795bab251e0b6ed767397182bac134d9f0fcecc552e/granian-2.7.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e315adf24162294d35ca4bed66c8f66ac15a0696f2cb462e729122d148f6d958", size = 4823143, upload-time = "2026-02-02T11:39:24.696Z" }, { url = "https://files.pythonhosted.org/packages/72/10/f37497b41c8f2a49fbc7f17dafdb979c84084ea127e27bfe92fe28dcd229/granian-2.6.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:450d45418116766cce8d2ef138eeea59e74d15da3759ae87af9732a8896ca3ef", size = 3239746, upload-time = "2026-01-07T11:08:17.75Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e0/ff8528bf620b6da7833171f6d30bfe4b4b1d6e7d155b634bd17590e0c4b4/granian-2.7.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:486f8785e716f76f96534aaba25acd5dee1a8398725ffd2a55f0833689c75933", size = 4926328, upload-time = "2026-02-02T11:39:26.111Z" }, { url = "https://files.pythonhosted.org/packages/33/c5/842eb481cba2dc49374ffabb5a0ffeb8e61017da10a5084764369b1ac452/granian-2.6.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6e85ca2e6c83b5d70801068fbdca8acf733e12e35cee782ee94554f417971aff", size = 3311896, upload-time = "2026-01-07T11:08:19.3Z" },
{ url = "https://files.pythonhosted.org/packages/02/f7/fb0a761d39245295660703a42e9448f3c04ce1f26b2f62e044d179167880/granian-2.7.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:0e5e2c1c6ff1501e3675e5237096b90b767f506bb0ef88594310b7b9eaa95532", size = 5281190, upload-time = "2026-02-02T11:39:27.68Z" }, { url = "https://files.pythonhosted.org/packages/9b/6e/db253ae3ab220a394ffcd0cc7c3d7752ba20757bf37270af8f7b6813cd59/granian-2.6.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:aa4d187fd52e2873ef714c1d8aee097d88c141ac850f5bf04dbd37b513a3d67b", size = 3480487, upload-time = "2026-01-07T11:08:21.037Z" },
{ url = "https://files.pythonhosted.org/packages/d6/d8/860e7e96ea109c6db431c8284040d265758bded35f9ce2de05f3969d7c0c/granian-2.7.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:d4418b417f9c2162b4fa9ec41ec34ed3e8ed891463bb058873034222be53542f", size = 5117989, upload-time = "2026-02-02T11:39:29.008Z" }, { url = "https://files.pythonhosted.org/packages/4d/1d/0a78570d2c1fcccd5df93ff18e2cf00a708a75b9e935c038d973554f5f87/granian-2.6.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:53e2380f2b522082d5587c5dc05ad80ae0463dc751655d7cfb19d7ed3dbc48d6", size = 3509926, upload-time = "2026-01-07T11:08:23.067Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9a/500ab01ae273870e8fc056956cc49716707b4a0e76fb2b5993258e1494f7/granian-2.7.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:b4367c088c00bdc38a8a495282070010914931edb4c488499f290c91018d9e80", size = 4492656, upload-time = "2026-02-02T11:39:31.614Z" }, { url = "https://files.pythonhosted.org/packages/7c/ed/f1130abe166f5a9bb4c032bae94b4cbc1b04179703a98906493e3b329974/granian-2.6.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:997a2a45b14e583504b920abfbcaf605d3ee9a4e7e0d3599b41f2091b388bd81", size = 3029240, upload-time = "2026-01-07T11:08:26.17Z" },
{ url = "https://files.pythonhosted.org/packages/d0/26/86dc5a6fff60ee0cc38c2fcd1a0d4cebd52e6764a9f752a20458001ca57e/granian-2.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c8f3df224284ed1ff673f61de652337d7721100bf4cfc336b2047005b0edb2e0", size = 4122201, upload-time = "2026-02-02T11:39:33.162Z" }, { url = "https://files.pythonhosted.org/packages/cb/40/f5ec53011f3d91c14d6be604b02df930069cdb2dddbeb6aabda0ce265fc4/granian-2.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ee9ac64109dd54ff6773a82e9dc302843079edd7d9fcca4a72d9286918ae2c03", size = 2771824, upload-time = "2026-01-07T11:08:28.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/60/887dc5a099135ff449adcdea9a2aa38f39673baf99de9acb78077b701432/granian-2.7.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6682c08b0d82ad75f8e9d1571254630133e1563c49f0600c2e2dc26cec743ae7", size = 4377489, upload-time = "2026-02-02T11:39:34.532Z" }, { url = "https://files.pythonhosted.org/packages/82/98/a75d6d3b47d0a44e7af0ee7733ea295c01fb0070a636cb93290a6075da35/granian-2.6.1-cp314-cp314t-manylinux_2_24_armv7l.whl", hash = "sha256:4a76bbfcfea1b5114bda720fd9dbc11e584e513c351c2ec399cd3e2dcb1f8fd2", size = 3313753, upload-time = "2026-01-07T11:08:30.556Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6b/68c12f8c4c1f1c109bf55d66beeb37a817fd908af5d5d9b48afcbdc3e623/granian-2.7.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6ccc3bdc2248775b6bd292d7d37a1bff79eb1aaf931f3a217ea9fb9a6fe7ca4", size = 5067294, upload-time = "2026-02-02T11:39:35.84Z" }, { url = "https://files.pythonhosted.org/packages/3d/33/c37f0c5dd649ff97bed0b70d36b8f02fd488d926c30b272f6d09618ccde7/granian-2.6.1-cp314-cp314t-manylinux_2_24_i686.whl", hash = "sha256:5f3c6bab1e443b2277923b0f836141584b0f56b6db74904d4436d6031118a3fa", size = 2966697, upload-time = "2026-01-07T11:08:32.52Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/be4f9c129f5f80f52654f257abe91f647defec020fa134b3600013b7219d/granian-2.7.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5431272a4d6f49a200aeb7b01010a3785b93b9bd8cd813d98ed29c8e9ba1c476", size = 4848356, upload-time = "2026-02-02T11:39:37.443Z" }, { url = "https://files.pythonhosted.org/packages/5a/71/92dd02f5b53d28478906fc374a91337e65efeae04be9f392bdcb4a313906/granian-2.6.1-cp314-cp314t-manylinux_2_24_x86_64.whl", hash = "sha256:bbc0cac57a8719ae0d96ee250c539872becdd941795ab4590dca49cba240c809", size = 3261860, upload-time = "2026-01-07T11:08:33.946Z" },
{ url = "https://files.pythonhosted.org/packages/d7/aa/f6efcfb435f370a6f3626bd5837465bfb71950f6b3cb3c74e54b176c72e2/granian-2.7.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:790b150255576775672f26dbcbd6eb05f70260dd661b91ce462f6f3846db9501", size = 4669022, upload-time = "2026-02-02T11:39:38.782Z" }, { url = "https://files.pythonhosted.org/packages/e9/ca/abab95b8b3541a5e540a3b89a0b2805cdd1b064c9083cd4ada71c7d1ac76/granian-2.6.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dac537c663ec9b2f1f8bd3c0c6fd0c3f749d91b4f35f918df9ea1357528360dd", size = 3111002, upload-time = "2026-01-07T11:08:35.355Z" },
{ url = "https://files.pythonhosted.org/packages/1d/36/e86050c476046ef1f0aae0eb86d098fa787abfc8887a131c82baccc7565e/granian-2.7.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ce9be999273c181e4b65efbbd82a5bc6f223f1db3463660514d1dc229c8ba760", size = 4895567, upload-time = "2026-02-02T11:39:40.144Z" }, { url = "https://files.pythonhosted.org/packages/8e/3f/b872cca9f39b98aaeccf95f4c5b10fca21562c623bb3f7115de4506e9e1b/granian-2.6.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:9d6a3ac7234708fb2cce1fec6dd0941458c45e26b98b377b4a08d715fe9a9bd2", size = 3303389, upload-time = "2026-01-07T11:08:36.767Z" },
{ url = "https://files.pythonhosted.org/packages/2b/5e/25283ff7fc12fcf42ae8a5687243119739cf4b0bf5ccb1c32d11d37987b1/granian-2.7.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:319b34f18ed3162354513acb5a9e8cee720ac166cd88fe05f0f057703eb47e4f", size = 5238652, upload-time = "2026-02-02T11:39:41.648Z" }, { url = "https://files.pythonhosted.org/packages/5d/39/2224e99dbd2c61aed585550f5a624b653a5c90b6ea7821c7a1e1c187d4cb/granian-2.6.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:75bb8af9306aaceade90ec91cd9f5d4bdb5537b221b31e5a358b1eadb57279ad", size = 3475366, upload-time = "2026-01-07T11:08:38.29Z" },
{ url = "https://files.pythonhosted.org/packages/5f/60/06148781120e086c7437aa9513198025ea1eb847cb2e244d5e2b9801782e/granian-2.7.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:b01bed8ad748840e7ab49373f642076f3bc459e39937a4ce11c5be03e67cdfd9", size = 5079018, upload-time = "2026-02-02T11:39:43.309Z" }, { url = "https://files.pythonhosted.org/packages/72/01/5077972a90cee7ef28c03a02b35c15d111416b25ccf8f8fda8df9972b6ce/granian-2.6.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:b7c1a0f24bd079bab8f6550fbb277e09c187dd5fe15e24ea3f2ace78453e5b7b", size = 3507600, upload-time = "2026-01-07T11:08:39.691Z" },
{ url = "https://files.pythonhosted.org/packages/0f/0b/39ebf1b791bbd4049239ecfee8f072321211879e5617a023921961be1d55/granian-2.7.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:24a1f6a894bea95ef0e603bebacbccd19c319c0da493bb4fde8b94b8629f3dc8", size = 4581648, upload-time = "2026-02-02T11:39:45.991Z" }, { url = "https://files.pythonhosted.org/packages/e1/eb/8965002085f93cc1a9887414f43aed0d23025a7d4a4965c27d23d2d9c3c6/granian-2.6.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c14a8708066e396a56673cc23acff8174fff613c433f1da0746c903341e4c22", size = 3077208, upload-time = "2026-01-07T11:08:43.752Z" },
{ url = "https://files.pythonhosted.org/packages/2f/cd/4642192520478bba4cd547124d92607c958a0786864ebe378f3008b40048/granian-2.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c2799497ac896cffea85512983c5d9eb4ae51ebacd7a9a5fd3d2ac81f1755fac", size = 4214257, upload-time = "2026-02-02T11:39:47.507Z" }, { url = "https://files.pythonhosted.org/packages/ff/8b/5e08ad10d2cfde71cc438bc3887f168f7845e195f67656d726c36bfbfa0f/granian-2.6.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4e3041bc47b6add143e3944c5bb0d14cd94b5b9722812319d73c24d24816b039", size = 2813151, upload-time = "2026-01-07T11:08:45.094Z" },
{ url = "https://files.pythonhosted.org/packages/e2/3f/615f93753c3b682219fe546196fc9eb3a045d846e57883312c97de4d785a/granian-2.7.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b66a15d004136e641706e0e5522b3509151e2027a0677cf4fa97d049d9ddfa41", size = 4979656, upload-time = "2026-02-02T11:39:48.838Z" }, { url = "https://files.pythonhosted.org/packages/82/1b/dcb6c44a059b0a6571da1d4abe329a30c7e40c49e7a108e963a7b8c61a4c/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:9635f707119a8bdc22ebfd70b50944a72a7e791905b544ac64938f4e87a8060f", size = 3357183, upload-time = "2026-01-07T11:08:46.952Z" },
{ url = "https://files.pythonhosted.org/packages/6e/68/1f2c36a964f93bfe8d6189431b8425acc591b735e47d8898b2e70c478398/granian-2.7.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:de5a6fa93d2138ba2372d20d97b87c1af75fa16a59a93841745326825c3ddf83", size = 4844448, upload-time = "2026-02-02T11:39:50.5Z" }, { url = "https://files.pythonhosted.org/packages/a3/18/b8e976b0bec47edb5d469a3c4e44d8cad3383ffb6b8202eba35249a23845/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:566941333bed75345583c4ff0a72783812c68c5f87df3f9974e847bfcfb78d3e", size = 3233117, upload-time = "2026-01-07T11:08:48.354Z" },
{ url = "https://files.pythonhosted.org/packages/df/23/d8c83fe6a6656026c734c2ea771cbcdec6f0010e749f8ab0db1bfc8a3dfe/granian-2.7.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:aacda2ad46724490c4cd811b8dcadff2260603a3e95ca0d8c33552d791a3c6ac", size = 4930755, upload-time = "2026-02-02T11:39:51.866Z" }, { url = "https://files.pythonhosted.org/packages/e9/e7/939eb934e4de6faa3b60448bf233610aec39e6186b0da212179cedce3baf/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4a85fa1f42fd54feda9c70a7ef349259da6c5d81f9d5918633c677b7be8238ba", size = 3306125, upload-time = "2026-01-07T11:08:49.848Z" },
{ url = "https://files.pythonhosted.org/packages/20/e5/2a86ee18544185e72fc50b50985b6bfb4504f7835875d2636f573e100071/granian-2.7.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:7efb5ebdb308ed1685a80cded6ea51447753e8afe92c21fc3abf9a06a9eb5d2e", size = 5295728, upload-time = "2026-02-02T11:39:53.364Z" }, { url = "https://files.pythonhosted.org/packages/0f/30/b99516161444c2528b9ab9e2633060c226f7f6ddf2d24464fb0d3b3f86ce/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:7787cfc9b79a298289ff728d26937eac95482fcb468ef912d9178e707889c276", size = 3485546, upload-time = "2026-01-07T11:08:51.479Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bd/0d47d17769601c56d876b289456f27799611571227b99ad300e221600bbd/granian-2.7.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ae96b75420d01d9a7dbe1bd84f1898b2b0ade6883db59bfe2b233d7c28c6b0df", size = 5095149, upload-time = "2026-02-02T11:39:54.767Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/30b0db6729767eac2249856b563a9f71a04b7eb8ce44321b7472834dcb19/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d3fd613953ea080f911d66bbebd46c8c4b3e66fbb293f78b13c86fb3ec0202ae", size = 3497427, upload-time = "2026-01-07T11:08:53.065Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1897,6 +1915,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" },
] ]
[[package]]
name = "identify"
version = "2.6.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@@ -2292,15 +2319,15 @@ wheels = [
[[package]] [[package]]
name = "llama-index-llms-openai" name = "llama-index-llms-openai"
version = "0.6.17" version = "0.6.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/94/55e9855f565b30604fe9e154a7c3df1f4437ce62ed8926a8d6b5112cf73b/llama_index_llms_openai-0.6.17.tar.gz", hash = "sha256:63294c1d8d221d2009023b2c294c919f6141f4cdce9cd48bffe700d8be2a37e0", size = 25941, upload-time = "2026-02-03T11:03:26.355Z" } sdist = { url = "https://files.pythonhosted.org/packages/6a/66/71d2e7fb2dcf077d02065346969ccd84a38188f399ac5835408ccfce7c2e/llama_index_llms_openai-0.6.15.tar.gz", hash = "sha256:5bd059ea44412e927743a98bb1e5b84b2e194bb396ef959527fc3a8ac80b025d", size = 25856, upload-time = "2026-01-26T12:07:15.727Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/85/1dee39a45b31528611cc6139777fc5631b9946fe085b37cd981062aa6d50/llama_index_llms_openai-0.6.17-py3-none-any.whl", hash = "sha256:d275f7e218611afc6bb157fe722f6e8590e8e20ac3ea1392cf560ac674c175af", size = 26940, upload-time = "2026-02-03T11:03:25.059Z" }, { url = "https://files.pythonhosted.org/packages/9c/18/47190a84b7085536669161c2bd932f1e187d0ea4526c5e3e4c690dcb7cd4/llama_index_llms_openai-0.6.15-py3-none-any.whl", hash = "sha256:b4ef1756a0815e7d930678ba8c6e69f56aea0bc8ca5372759fbb90f678bbff3d", size = 26874, upload-time = "2026-01-26T12:07:14.614Z" },
] ]
[[package]] [[package]]
@@ -2557,11 +2584,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
] ]
[[package]]
name = "microsoft-python-type-stubs"
version = "0"
source = { git = "https://github.com/microsoft/python-type-stubs.git#692c37c3969d22612b295ddf7e7af5907204a386" }
[[package]] [[package]]
name = "mkdocs" name = "mkdocs"
version = "1.6.1" version = "1.6.1"
@@ -2853,15 +2875,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
] ]
[[package]]
name = "mypy-baseline"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/2a/03288dab6d5f24d187ba0c223f6b3035d9a29de3dd31a3e105a0d4f1b5da/mypy_baseline-0.7.3.tar.gz", hash = "sha256:325f0695310eb8f5c0f10fa7af36ee1b3785a9d26b886a61c07b4a8eddb28d29", size = 319108, upload-time = "2025-05-30T08:43:00.629Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/93/7780302b206a8e8e767ce763ef06159725d1323acbe55e46a1cd1ffd109d/mypy_baseline-0.7.3-py3-none-any.whl", hash = "sha256:bd7fa899e687d75af2e3f392a9d6d1790e65dae3d31fe12525cc14f26d866b74", size = 17868, upload-time = "2025-05-30T08:42:58.262Z" },
]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.1.0" version = "1.1.0"
@@ -2931,6 +2944,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" }, { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" },
] ]
[[package]]
name = "nodeenv"
version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.2.6" version = "2.2.6"
@@ -3098,7 +3120,7 @@ wheels = [
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.16.0" version = "2.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3110,9 +3132,9 @@ dependencies = [
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" } sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" }, { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" },
] ]
[[package]] [[package]]
@@ -3196,8 +3218,8 @@ dependencies = [
{ name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "setproctitle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "setproctitle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3229,7 +3251,8 @@ dev = [
{ name = "imagehash", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "imagehash", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mkdocs-glightbox", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mkdocs-glightbox", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mkdocs-material", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mkdocs-material", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "prek", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pre-commit-uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pytest-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3246,7 +3269,8 @@ docs = [
{ name = "mkdocs-material", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mkdocs-material", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
lint = [ lint = [
{ name = "prek", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pre-commit-uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
testing = [ testing = [
@@ -3269,11 +3293,8 @@ typing = [
{ name = "django-stubs", extra = ["compatible-mypy"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-stubs", extra = ["compatible-mypy"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "djangorestframework-stubs", extra = ["compatible-mypy"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "djangorestframework-stubs", extra = ["compatible-mypy"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "lxml-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "lxml-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "microsoft-python-type-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mypy-baseline", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-channels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-dateparser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-dateparser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3296,9 +3317,9 @@ requires-dist = [
{ name = "concurrent-log-handler", specifier = "~=0.9.25" }, { name = "concurrent-log-handler", specifier = "~=0.9.25" },
{ name = "dateparser", specifier = "~=1.2" }, { name = "dateparser", specifier = "~=1.2" },
{ name = "django", specifier = "~=5.2.10" }, { name = "django", specifier = "~=5.2.10" },
{ name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.14.0" }, { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.13.1" },
{ name = "django-auditlog", specifier = "~=3.4.1" }, { name = "django-auditlog", specifier = "~=3.4.1" },
{ name = "django-cachalot", specifier = "~=2.9.0" }, { name = "django-cachalot", specifier = "~=2.8.0" },
{ name = "django-celery-results", specifier = "~=2.6.0" }, { name = "django-celery-results", specifier = "~=2.6.0" },
{ name = "django-compression-middleware", specifier = "~=0.5.0" }, { name = "django-compression-middleware", specifier = "~=0.5.0" },
{ name = "django-cors-headers", specifier = "~=4.9.0" }, { name = "django-cors-headers", specifier = "~=4.9.0" },
@@ -3311,13 +3332,13 @@ requires-dist = [
{ name = "djangorestframework", specifier = "~=3.16" }, { name = "djangorestframework", specifier = "~=3.16" },
{ name = "djangorestframework-guardian", specifier = "~=0.4.0" }, { name = "djangorestframework-guardian", specifier = "~=0.4.0" },
{ name = "drf-spectacular", specifier = "~=0.28" }, { name = "drf-spectacular", specifier = "~=0.28" },
{ name = "drf-spectacular-sidecar", specifier = "~=2026.1.1" }, { name = "drf-spectacular-sidecar", specifier = "~=2025.10.1" },
{ name = "drf-writable-nested", specifier = "~=0.7.1" }, { name = "drf-writable-nested", specifier = "~=0.7.1" },
{ name = "faiss-cpu", specifier = ">=1.10" }, { name = "faiss-cpu", specifier = ">=1.10" },
{ name = "filelock", specifier = "~=3.20.0" }, { name = "filelock", specifier = "~=3.20.0" },
{ name = "flower", specifier = "~=2.0.1" }, { name = "flower", specifier = "~=2.0.1" },
{ name = "gotenberg-client", specifier = "~=0.13.1" }, { name = "gotenberg-client", specifier = "~=0.13.1" },
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.7.0" }, { name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.6.0" },
{ name = "httpx-oauth", specifier = "~=0.16" }, { name = "httpx-oauth", specifier = "~=0.16" },
{ name = "imap-tools", specifier = "~=1.11.0" }, { name = "imap-tools", specifier = "~=1.11.0" },
{ name = "jinja2", specifier = "~=3.1.5" }, { name = "jinja2", specifier = "~=3.1.5" },
@@ -3352,7 +3373,7 @@ requires-dist = [
{ name = "sentence-transformers", specifier = ">=4.1" }, { name = "sentence-transformers", specifier = ">=4.1" },
{ name = "setproctitle", specifier = "~=1.3.4" }, { name = "setproctitle", specifier = "~=1.3.4" },
{ name = "tika-client", specifier = "~=0.10.0" }, { name = "tika-client", specifier = "~=0.10.0" },
{ name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "torch", specifier = "~=2.9.1", index = "https://download.pytorch.org/whl/cpu" },
{ name = "tqdm", specifier = "~=4.67.1" }, { name = "tqdm", specifier = "~=4.67.1" },
{ name = "watchfiles", specifier = ">=1.1.1" }, { name = "watchfiles", specifier = ">=1.1.1" },
{ name = "whitenoise", specifier = "~=6.11" }, { name = "whitenoise", specifier = "~=6.11" },
@@ -3370,7 +3391,8 @@ dev = [
{ name = "imagehash" }, { name = "imagehash" },
{ name = "mkdocs-glightbox", specifier = "~=0.5.1" }, { name = "mkdocs-glightbox", specifier = "~=0.5.1" },
{ name = "mkdocs-material", specifier = "~=9.7.0" }, { name = "mkdocs-material", specifier = "~=9.7.0" },
{ name = "prek", specifier = "~=0.3.0" }, { name = "pre-commit", specifier = "~=4.5.1" },
{ name = "pre-commit-uv", specifier = "~=4.2.0" },
{ name = "pytest", specifier = "~=9.0.0" }, { name = "pytest", specifier = "~=9.0.0" },
{ name = "pytest-cov", specifier = "~=7.0.0" }, { name = "pytest-cov", specifier = "~=7.0.0" },
{ name = "pytest-django", specifier = "~=4.11.1" }, { name = "pytest-django", specifier = "~=4.11.1" },
@@ -3380,15 +3402,16 @@ dev = [
{ name = "pytest-rerunfailures", specifier = "~=16.1" }, { name = "pytest-rerunfailures", specifier = "~=16.1" },
{ name = "pytest-sugar" }, { name = "pytest-sugar" },
{ name = "pytest-xdist", specifier = "~=3.8.0" }, { name = "pytest-xdist", specifier = "~=3.8.0" },
{ name = "ruff", specifier = "~=0.15.0" }, { name = "ruff", specifier = "~=0.14.0" },
] ]
docs = [ docs = [
{ name = "mkdocs-glightbox", specifier = "~=0.5.1" }, { name = "mkdocs-glightbox", specifier = "~=0.5.1" },
{ name = "mkdocs-material", specifier = "~=9.7.0" }, { name = "mkdocs-material", specifier = "~=9.7.0" },
] ]
lint = [ lint = [
{ name = "prek", specifier = "~=0.3.0" }, { name = "pre-commit", specifier = "~=4.5.1" },
{ name = "ruff", specifier = "~=0.15.0" }, { name = "pre-commit-uv", specifier = "~=4.2.0" },
{ name = "ruff", specifier = "~=0.14.0" },
] ]
testing = [ testing = [
{ name = "daphne" }, { name = "daphne" },
@@ -3410,11 +3433,8 @@ typing = [
{ name = "django-stubs", extras = ["compatible-mypy"] }, { name = "django-stubs", extras = ["compatible-mypy"] },
{ name = "djangorestframework-stubs", extras = ["compatible-mypy"] }, { name = "djangorestframework-stubs", extras = ["compatible-mypy"] },
{ name = "lxml-stubs" }, { name = "lxml-stubs" },
{ name = "microsoft-python-type-stubs", git = "https://github.com/microsoft/python-type-stubs.git" },
{ name = "mypy" }, { name = "mypy" },
{ name = "mypy-baseline" },
{ name = "types-bleach" }, { name = "types-bleach" },
{ name = "types-channels" },
{ name = "types-colorama" }, { name = "types-colorama" },
{ name = "types-dateparser" }, { name = "types-dateparser" },
{ name = "types-markdown" }, { name = "types-markdown" },
@@ -3662,24 +3682,32 @@ wheels = [
] ]
[[package]] [[package]]
name = "prek" name = "pre-commit"
version = "0.3.1" version = "4.5.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/62/4b91c8a343e21fcefabc569a91d08cf8756c554332521af78294acef7c27/prek-0.3.1.tar.gz", hash = "sha256:45abc4ffd3cb2d39c478f47e92e88f050e5a4b7a20915d78e54b1a3d3619ebe4", size = 323141, upload-time = "2026-01-31T13:25:58.128Z" } dependencies = [
{ name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "identify", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/c1/e0e545048e4190245fb4ae375d67684108518f3c69b67b81d62e1cd855c6/prek-0.3.1-py3-none-linux_armv6l.whl", hash = "sha256:1f77d0845cc63cad9c447f7f7d554c1ad188d07dbe02741823d20d58c7312eaf", size = 4285460, upload-time = "2026-01-31T13:25:42.066Z" }, { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
{ url = "https://files.pythonhosted.org/packages/10/fe/7636d10e2dafdf2a4a927c989f32ce3f08e99d62ebad7563f0272e74b7f4/prek-0.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e21142300d139e8c7f3970dd8aa66391cb43cd17c0c4ee65ff1af48856bb6a4b", size = 4287085, upload-time = "2026-01-31T13:25:40.193Z" }, ]
{ url = "https://files.pythonhosted.org/packages/a3/7f/62ed57340071e04c02057d64276ec3986baca3ad4759523e2f433dc9be55/prek-0.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c09391de7d1994f9402c46cb31671800ea309b1668d839614965706206da3655", size = 3936188, upload-time = "2026-01-31T13:25:47.314Z" },
{ url = "https://files.pythonhosted.org/packages/6b/17/cb24f462c255f76d130ca110f4fcec09b041c3c770e43960cc3fc9dcc9ce/prek-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a0b0a012ef6ef28dee019cf81a95c5759b2c03287c32c1f2adcb5f5fb097138e", size = 4275214, upload-time = "2026-01-31T13:25:38.053Z" }, [[package]]
{ url = "https://files.pythonhosted.org/packages/f2/85/db155b59d73cf972c8467e4d95def635f9976d5fcbcc790a4bbe9d2e049a/prek-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4a0e40785d39b8feae0d7ecf5534789811a2614114ab47f4e344a2ebd75ac10", size = 4197982, upload-time = "2026-01-31T13:25:50.034Z" }, name = "pre-commit-uv"
{ url = "https://files.pythonhosted.org/packages/06/cf/d35c32436692928a9ca53eed3334e30148a60f0faa33c42e8d11b6028fa6/prek-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5c2f5e377e3cc5a5ea191deb8255a5823fbaa01b424417fe18ff12c7c800f3", size = 4458572, upload-time = "2026-01-31T13:25:51.46Z" }, version = "4.2.0"
{ url = "https://files.pythonhosted.org/packages/b4/c0/eb36fecb21fe30baa72fb87ccf3a791c32932786c287f95f8972402e9245/prek-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fe70e97f4dfca57ce142caecd77b90a435abd1c855f9e96041078415d80e89a", size = 4999230, upload-time = "2026-01-31T13:25:44.055Z" }, source = { registry = "https://pypi.org/simple" }
{ url = "https://files.pythonhosted.org/packages/e4/f3/ad1a25ea16320e6acd1fedd6bd96a0d22526f5132d9b5adc045996ccca4c/prek-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b28e921d893771bdd7533cd94d46a10e0cf2855c5e6bf6809b598b5e45baa73", size = 4510376, upload-time = "2026-01-31T13:25:48.563Z" }, dependencies = [
{ url = "https://files.pythonhosted.org/packages/39/b7/91afdd24be808ccf3b9119f4cf2bd6d02e30683143a62a08f432a3435861/prek-0.3.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:555610a53c4541976f4fe8602177ecd7a86a931dcb90a89e5826cfc4a6c8f2cb", size = 4273229, upload-time = "2026-01-31T13:25:56.362Z" }, { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ url = "https://files.pythonhosted.org/packages/5a/bb/636c77c5c9fc5eadcc2af975a95b48eeeff2dc833021e222b0e9479b9b47/prek-0.3.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:663a15a31b705db5d01a1c9eb28c6ea417628e6cb68de2dc7b3016ca2f959a99", size = 4301166, upload-time = "2026-01-31T13:25:36.281Z" }, { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ url = "https://files.pythonhosted.org/packages/4e/cf/c928a36173e71b21b252943404a5b3d1ddc1f08c9e0f1d7282a2c62c7325/prek-0.3.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1d03fb5fa37177dc37ccbe474252473adcbde63287a63e9fa3d5745470f95bd8", size = 4188473, upload-time = "2026-01-31T13:25:53.341Z" }, ]
{ url = "https://files.pythonhosted.org/packages/98/4c/af8f6a40cb094e88225514b89e8ae05ac69fc479d6b500e4b984f9ef8ae3/prek-0.3.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3e20a5b704c06944dca9555a39f1de3622edc8ed2becaf8b3564925d1b7c1c0d", size = 4342239, upload-time = "2026-01-31T13:25:55.179Z" }, sdist = { url = "https://files.pythonhosted.org/packages/f6/42/84372bc99a841bfdd8b182a50186471a7f5e873d8e8bcec0d0cb6dabcbb0/pre_commit_uv-4.2.0.tar.gz", hash = "sha256:c32bb1d90235507726eee2aeef2be5fdab431a6f1906e3f1addb0a4e99b369d1", size = 6912, upload-time = "2025-10-09T19:30:48.354Z" }
{ url = "https://files.pythonhosted.org/packages/b7/ba/6b0f725c0cf96182ab9622b4d122a01f04de9b2b6e4a6516874390218510/prek-0.3.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6889acf0c9b0dd7b9cd3510ec36af10a739826d2b9de1e2d021ae6a9f130959a", size = 4618674, upload-time = "2026-01-31T13:25:59.175Z" }, wheels = [
{ url = "https://files.pythonhosted.org/packages/87/9f/ec8491f6b3022489a4d36ce372214c10a34f90b425aa61ff2e0a8dc5b9d5/pre_commit_uv-4.2.0-py3-none-any.whl", hash = "sha256:cc1b56641e6c62d90a4d8b4f0af6f2610f1c397ce81af024e768c0f33715cb81", size = 5650, upload-time = "2025-10-09T19:30:47.257Z" },
] ]
[[package]] [[package]]
@@ -4751,24 +4779,25 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.0" version = "0.14.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
] ]
[[package]] [[package]]
@@ -5012,8 +5041,8 @@ dependencies = [
{ name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
{ name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" },
{ name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -5377,7 +5406,7 @@ wheels = [
[[package]] [[package]]
name = "torch" name = "torch"
version = "2.10.0" version = "2.9.1"
source = { registry = "https://download.pytorch.org/whl/cpu" } source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version >= '3.12' and sys_platform == 'darwin'",
@@ -5395,18 +5424,18 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin'" },
] ]
wheels = [ wheels = [
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2d16abfce6c92584ceeb00c3b2665d5798424dd9ed235ea69b72e045cd53ae97" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:bf1e68cfb935ae2046374ff02a7aa73dda70351b46342846f557055b3a540bf0" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:a52952a8c90a422c14627ea99b9826b7557203b46b4d0772d3ca5c7699692425" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:287242dd1f830846098b5eca847f817aa5c6015ea57ab4c1287809efea7b77eb" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:339e05502b6c839db40e88720cb700f5a3b50cda332284873e851772d41b2c1e" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8924d10d36eac8fe0652a060a03fc2ae52980841850b9a1a2ddb0f27a4f181cd" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:840351da59cedb7bcbc51981880050813c19ef6b898a7fecf73a3afc71aff3fe" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:bcee64ae7aa65876ceeae6dcaebe75109485b213528c74939602208a20706e3f" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c88b1129fd4e14f0f882963c6728315caae35d2f47374d17edeed1edc7697497" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:defadbeb055cfcf5def58f70937145aecbd7a4bc295238ded1d0e85ae2cf0e1d" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f4bea7dc451267c028593751612ad559299589304e68df54ae7672427893ff2c" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:886f84b181f766f53265ba0a1d503011e60f53fff9d569563ef94f24160e1072" },
] ]
[[package]] [[package]]
name = "torch" name = "torch"
version = "2.10.0+cpu" version = "2.9.1+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" } source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
@@ -5426,34 +5455,20 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'linux'" },
] ]
wheels = [ wheels = [
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp310-cp310-linux_aarch64.whl", hash = "sha256:31ae44836c8b9bbd1a3943d29c7c7457709ddf7c6173aa34aefe9d2203e4c405" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:10866c8a48c4aa5ae3f48538dc8a055b99c57d9c6af2bf5dd715374d9d6ddca3" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp310-cp310-linux_s390x.whl", hash = "sha256:beadc2a6a1785b09a46daad378de91ef274b8d3eea7af0bc2d017d97f115afdf" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7210713b66943fdbfcc237b2e782871b649123ac5d29f548ce8c85be4223ab38" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d63ee6a80982fd73fe44bb70d97d2976e010312ff6db81d7bfb9167b06dd45b9" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0e611cfb16724e62252b67d31073bc5c490cb83e92ecdc1192762535e0e44487" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a280ffaea7b9c828e0c1b9b3bd502d9b6a649dc9416997b69b84544bd469f215" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3de2adb9b4443dc9210ef1f1b16da3647ace53553166d6360bbbd7edd6f16e4d" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_aarch64.whl", hash = "sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf9b442a51a2948e41216a76d7ab00f0694cfcaaa51b6f9bcab57b7f89843e6" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:0e286fcf6ce0cc7b204396c9b4ea0d375f1f0c3e752f68ce3d3aeb265511db8c" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7417d8c565f219d3455654cb431c6d892a3eb40246055e14d645422de13b9ea1" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1cfcb9b1558c6e52dffd0d4effce83b13c5ae5d97338164c372048c21f9cfccb" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3e532e553b37ee859205a9b2d1c7977fd6922f53bbb1b9bfdd5bdc00d1a60ed4" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7cb1ec66cefb90fd7b676eac72cfda3b8d4e4d0cacd7a531963bc2e0a9710ab" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:39b3dff6d8fba240ae0d1bede4ca11c2531ae3b47329206512d99e17907ff74b" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_aarch64.whl", hash = "sha256:8de5a36371b775e2d4881ed12cc7f2de400b1ad3d728aa74a281f649f87c9b8c" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:01b1884f724977a20c7da2f640f1c7b37f4a2c117a7f4a6c1c0424d14cb86322" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:9accc30b56cb6756d4a9d04fcb8ebc0bb68c7d55c1ed31a8657397d316d31596" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:031a597147fa81b1e6d79ccf1ad3ccc7fafa27941d6cf26ff5caaa384fb20e92" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:179451716487f8cb09b56459667fa1f5c4c0946c1e75fbeae77cfc40a5768d87" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:65010ab4aacce6c9a1ddfc935f986c003ca8638ded04348fd326c3e74346237c" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ee40b8a4b4b2cf0670c6fd4f35a7ef23871af956fecb238fbf5da15a72650b1d" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:88adf5157db5da1d54b1c9fe4a6c1d20ceef00e75d854e206a87dbf69e3037dc" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_aarch64.whl", hash = "sha256:fd215f3d0f681905c5b56b0630a3d666900a37fcc3ca5b937f95275c66f9fd9c" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3ac2b8df2c55430e836dcda31940d47f1f5f94b8731057b6f20300ebea394dd9" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:170a0623108055be5199370335cf9b41ba6875b3cb6f086db4aee583331a4899" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5b688445f928f13563b7418b17c57e97bf955ab559cf73cd8f2b961f8572dbb3" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e51994492cdb76edce29da88de3672a3022f9ef0ffd90345436948d4992be2c7" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8d316e5bf121f1eab1147e49ad0511a9d92e4c45cc357d1ab0bee440da71a095" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_aarch64.whl", hash = "sha256:5af75e5f49de21b0bdf7672bc27139bd285f9e8dbcabe2d617a2eb656514ac36" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:ba51ef01a510baf8fff576174f702c47e1aa54389a9f1fba323bb1a5003ff0bf" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0fedcb1a77e8f2aaf7bfd21591bf6d1e0b207473268c9be16b17cb7783253969" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:106dd1930cb30a4a337366ba3f9b25318ebf940f51fd46f789281dd9e736bdc4" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314-linux_aarch64.whl", hash = "sha256:ea2bcc9d1fca66974a71d4bf9a502539283f35d61fcab5a799b4e120846f1e02" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:f8294fd2fc6dd8f4435a891a0122307a043b14b21f0dac1bca63c85bfb59e586" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a28fdbcfa2fbacffec81300f24dd1bed2b0ccfdbed107a823cff12bc1db070f6" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:aada8afc068add586464b2a55adb7cc9091eec55caf5320447204741cb6a0604" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314t-linux_aarch64.whl", hash = "sha256:9412bd37b70f5ebd1205242c4ba4cabae35a605947f2b30806d5c9b467936db9" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:e71c476517c33e7db69825a9ff46c7f47a723ec4dac5b2481cff4246d1c632be" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:23882f8d882460aca809882fc42f5e343bf07585274f929ced00177d1be1eb67" },
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4fcd8b4cc2ae20f2b7749fb275349c55432393868778c2d50a08e81d5ee5591e" },
] ]
[[package]] [[package]]
@@ -5474,11 +5489,11 @@ wheels = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.67.3" version = "4.67.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
] ]
[[package]] [[package]]
@@ -5595,19 +5610,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/ec/092f2b74b49ec4855cdb53050deb9699f7105b8fda6fe034c0781b8687f3/types_cffi-1.17.0.20250915-py3-none-any.whl", hash = "sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c", size = 20112, upload-time = "2025-09-15T03:01:24.187Z" }, { url = "https://files.pythonhosted.org/packages/aa/ec/092f2b74b49ec4855cdb53050deb9699f7105b8fda6fe034c0781b8687f3/types_cffi-1.17.0.20250915-py3-none-any.whl", hash = "sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c", size = 20112, upload-time = "2025-09-15T03:01:24.187Z" },
] ]
[[package]]
name = "types-channels"
version = "4.3.0.20250822"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/3d/e0a164b7eaab18ab2302b7a3d90843824b729d9fe1d7bdbb3769dfc9d77b/types_channels-4.3.0.20250822.tar.gz", hash = "sha256:29a4928fdaed6d444b93b69d44fcdb5a8fe32fa72d6a41016c5d39fa7bd7f474", size = 15357, upload-time = "2025-08-22T03:04:26.444Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/52/4e3094e43d460feacb9051ec4c3498f8272f69d92b772647211478b25079/types_channels-4.3.0.20250822-py3-none-any.whl", hash = "sha256:d3fc0a1467c8cc901686826408c8a673822e07aa79cbe1a6d21946e7e55d9ddf", size = 21125, upload-time = "2025-08-22T03:04:25.539Z" },
]
[[package]] [[package]]
name = "types-colorama" name = "types-colorama"
version = "0.4.15.20250801" version = "0.4.15.20250801"
@@ -5744,14 +5746,14 @@ wheels = [
[[package]] [[package]]
name = "types-tqdm" name = "types-tqdm"
version = "4.67.2.20260202" version = "4.67.0.20250809"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/53/6a/a9e27944f2715940cf7b43b35a2b464d968d086723414983e47af3261e2d/types_tqdm-4.67.2.20260202.tar.gz", hash = "sha256:c3c494ae8dfd2946fa1fe089ce6931a85bb88d9078da0d65c6b99b6643f7561d", size = 17418, upload-time = "2026-02-02T04:11:26.288Z" } sdist = { url = "https://files.pythonhosted.org/packages/fb/d0/cf498fc630d9fdaf2428b93e60b0e67b08008fec22b78716b8323cf644dc/types_tqdm-4.67.0.20250809.tar.gz", hash = "sha256:02bf7ab91256080b9c4c63f9f11b519c27baaf52718e5fdab9e9606da168d500", size = 17200, upload-time = "2025-08-09T03:17:43.489Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/21/f4/d24e5216630081356fd9427cb9fb4c94e3c6cfde4d4d46db2c6717e286ae/types_tqdm-4.67.2.20260202-py3-none-any.whl", hash = "sha256:01b31780b2a2cdad8465a3e53a06fdf53a47abab0d46d69c1b2039197ffd55b0", size = 23893, upload-time = "2026-02-02T04:11:25.007Z" }, { url = "https://files.pythonhosted.org/packages/3f/13/3ff0781445d7c12730befce0fddbbc7a76e56eb0e7029446f2853238360a/types_tqdm-4.67.0.20250809-py3-none-any.whl", hash = "sha256:1a73053b31fcabf3c1f3e2a9d5ecdba0f301bde47a418cd0e0bdf774827c5c57", size = 24020, upload-time = "2025-08-09T03:17:42.453Z" },
] ]
[[package]] [[package]]
@@ -5903,6 +5905,29 @@ wheels = [
{ 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" }, { 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" },
] ]
[[package]]
name = "uv"
version = "0.9.27"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/70/611bcee4385b7aa00cf7acf29bc51854c365ee09ddb2cdb14b07a36b4db8/uv-0.9.27.tar.gz", hash = "sha256:9147862902b5d40894f78339803225e39b0535c4c04537188de160eb7635e46b", size = 3830404, upload-time = "2026-01-26T23:27:43.442Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/f1/9e9afb9fadb73f7914a0fc63b6f9d182b6fe6b1e55deb7cbdc29ff31299f/uv-0.9.27-py3-none-linux_armv6l.whl", hash = "sha256:ce3f16e66a96dcdc63f6ada9f7747686930986d2df104a9dd2d09664b2d870c8", size = 22011502, upload-time = "2026-01-26T23:27:31.714Z" },
{ url = "https://files.pythonhosted.org/packages/ea/b2/c36a87f5c745d310b7d8a53df053d6a87864aa38e3a964b0845eb6de37cc/uv-0.9.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a662a7df5cc781ae7baa65171b5d488d946ea93e61b7bbeda5a24d21a0cd9003", size = 21081065, upload-time = "2026-01-26T23:28:11.895Z" },
{ url = "https://files.pythonhosted.org/packages/44/1d/be2d80573c531389933059f6e5ef265ef7324c268f3ade80e500aa627f6b/uv-0.9.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f00158023e77600da602c5f1fa97cd8c2eef987d6aba34c16cf04a3e5a932f4", size = 19844905, upload-time = "2026-01-26T23:27:34.486Z" },
{ url = "https://files.pythonhosted.org/packages/3e/f7/59679af9f0446d8ffc1239e3356390c95925e0004549b64df3f189b1422b/uv-0.9.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5f2393051ed2023cc7d6ff99e41184b7c7bb7da001bc727cd4bee6da96f4a966", size = 21592623, upload-time = "2026-01-26T23:27:51.132Z" },
{ url = "https://files.pythonhosted.org/packages/e2/31/0faaad82951fc6b14dfad8e187e43747a528aa50ee283385f903e86d67d1/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3f8cf7a50a95ae5cb0366d24edf79d15b3ba380b462af49e3404f9f7b91101c7", size = 21636917, upload-time = "2026-01-26T23:27:46.252Z" },
{ url = "https://files.pythonhosted.org/packages/c2/8a/e181c32b7f5309fd987667d368fb944822c713e92a7eba3c73d2eddec6cd/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c331e0445465ea6029e2dd0f5499024e552136c10069fac0ca21708f2aeb1ce6", size = 21633082, upload-time = "2026-01-26T23:28:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1d44157bc8e5d1c382db087d9a30ab85fc7b5c2d610fb2e3d861c5a69d9b/uv-0.9.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56e0c92da67060d4c86a3b92b2c69e8fb1d61933636764aba16531ddb13f6e3", size = 22843044, upload-time = "2026-01-26T23:27:29.091Z" },
{ url = "https://files.pythonhosted.org/packages/eb/76/7c1b13e4dc8237dd3721f4ec933bb2e5be400fd2812cf98dc2be645a0f7d/uv-0.9.27-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0c9b2e874f5207a50b852726f3a0740eadf30baf2c7973380d697f4e3166d347", size = 24141329, upload-time = "2026-01-26T23:27:39.705Z" },
{ url = "https://files.pythonhosted.org/packages/6f/04/551749fd863cb43290c9a3f4348ccdd88ec0445c26a00ba840d776572867/uv-0.9.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a2e4df4a32f22fbb0919c3e513226684572fba227b37467ba6404f3fafd", size = 23637517, upload-time = "2026-01-26T23:27:37.111Z" },
{ url = "https://files.pythonhosted.org/packages/d9/c6/78b619a51a6646af4633714b161f980ab764cc892e0f79228162fba51fe8/uv-0.9.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33902c95b561ac67d15c339fe1eaf39e068372c7464c79c3bd0e2bf9ee914dcb", size = 22864516, upload-time = "2026-01-26T23:27:56.35Z" },
{ url = "https://files.pythonhosted.org/packages/15/19/b35928e55307beb69b60b88446df3cb8d7ff3ba0993fc2214a43266c17d1/uv-0.9.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79939f7e92d707fb84933509df747d1b88b00d94ebe41f3a1e30916cc33c7307", size = 22746151, upload-time = "2026-01-26T23:27:26.166Z" },
{ url = "https://files.pythonhosted.org/packages/9f/70/fbab20d40afe7ac9ec20011acec75f8bb3b9b83dfbe2cdb1405cad7a8cf2/uv-0.9.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7e2d4183a0dca7596ea6385e9d5a0a87ada4f71a70aa110e2b22234370b8d8ef", size = 21661188, upload-time = "2026-01-26T23:27:53.79Z" },
{ url = "https://files.pythonhosted.org/packages/44/02/4d4cf298bd22e53d6c289404b093cf876e64ee1fb946cc32a6f965030629/uv-0.9.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1555ab7bc8501144e8771e54a628eb02cb95f3612d54659bb7132576260feee5", size = 22397798, upload-time = "2026-01-26T23:28:07.102Z" },
{ url = "https://files.pythonhosted.org/packages/97/29/3acef6a0eea58afbf7f7a08e4258430e3c7394a6b1e28249450f4c0ddc60/uv-0.9.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:542731a6f53072e725959a9c839b195048715d840213d9834d36f74fa4249855", size = 22111665, upload-time = "2026-01-26T23:27:58.762Z" },
{ url = "https://files.pythonhosted.org/packages/13/15/1e7b34f02e8f53c9498311f991421e794ad57fa60a2d3e41b43485e914e4/uv-0.9.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4f534ad701ca3fffac4a8e1df2a36930e6a0cbf4dad52aeabc2c3c9e2cbbe65e", size = 22951420, upload-time = "2026-01-26T23:27:48.818Z" },
]
[[package]] [[package]]
name = "uvloop" name = "uvloop"
version = "0.22.1" version = "0.22.1"
@@ -5956,6 +5981,21 @@ wheels = [
{ 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" }, { 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" },
] ]
[[package]]
name = "virtualenv"
version = "20.36.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
]
[[package]] [[package]]
name = "watchdog" name = "watchdog"
version = "6.0.0" version = "6.0.0"