From 65aed2405cfd48cbb0193aa4a2ab19e9da982497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20M=C3=A9rino?= Date: Tue, 30 Dec 2025 22:06:21 +0100 Subject: [PATCH 01/57] Documentation: update notes for DB pool size (#11600) --- docs/configuration.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index cd5b8cf0a..adf34e2c6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -170,11 +170,18 @@ Available options are `postgresql` and `mariadb`. !!! note - A small pool is typically sufficient — for example, a size of 4. - Make sure your PostgreSQL server's max_connections setting is large enough to handle: - ```(Paperless workers + Celery workers) × pool size + safety margin``` - For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4: - (4 + 2) × 4 + 10 = 34 connections required. + A pool of 8-10 connections per worker is typically sufficient. + If you encounter error messages such as `couldn't get a connection` + or database connection timeouts, you probably need to increase the pool size. + + !!! warning + Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools: + `(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with + 4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`, + so `max_connections = 60` (or even more) is appropriate. + + This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications, + you should increase `max_connections` accordingly. #### [`PAPERLESS_DB_READ_CACHE_ENABLED=`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED} From 7b666e7569d53bd7736ddf7758ce2924357ed786 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:04:03 -0800 Subject: [PATCH 02/57] Performance: improve treenode inefficiencies (#11606) --- src/documents/serialisers.py | 44 ++++++++++++++++++++---------------- src/documents/views.py | 35 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 6e2307c2e..b780db815 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -580,30 +580,34 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): ), ) def get_children(self, obj): - filter_q = self.context.get("document_count_filter") - request = self.context.get("request") - if filter_q is None: - user = getattr(request, "user", None) if request else None - filter_q = get_document_count_filter_for_user(user) - self.context["document_count_filter"] = filter_q + children_map = self.context.get("children_map") + if children_map is not None: + children = children_map.get(obj.pk, []) + else: + filter_q = self.context.get("document_count_filter") + request = self.context.get("request") + if filter_q is None: + user = getattr(request, "user", None) if request else None + filter_q = get_document_count_filter_for_user(user) + self.context["document_count_filter"] = filter_q - children_queryset = ( - obj.get_children_queryset() - .select_related("owner") - .annotate(document_count=Count("documents", filter=filter_q)) - ) + children = ( + obj.get_children_queryset() + .select_related("owner") + .annotate(document_count=Count("documents", filter=filter_q)) + ) - view = self.context.get("view") - ordering = ( - OrderingFilter().get_ordering(request, children_queryset, view) - if request and view - else None - ) - ordering = ordering or (Lower("name"),) - children_queryset = children_queryset.order_by(*ordering) + view = self.context.get("view") + ordering = ( + OrderingFilter().get_ordering(request, children, view) + if request and view + else None + ) + ordering = ordering or (Lower("name"),) + children = children.order_by(*ordering) serializer = TagSerializer( - children_queryset, + children, many=True, user=self.user, full_perms=self.full_perms, diff --git a/src/documents/views.py b/src/documents/views.py index 680600c4b..99b7eb00b 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -448,8 +448,43 @@ class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): def get_serializer_context(self): context = super().get_serializer_context() context["document_count_filter"] = self.get_document_count_filter() + if hasattr(self, "_children_map"): + context["children_map"] = self._children_map return context + def list(self, request, *args, **kwargs): + """ + Build a children map once to avoid per-parent queries in the serializer. + """ + queryset = self.filter_queryset(self.get_queryset()) + ordering = OrderingFilter().get_ordering(request, queryset, self) or ( + Lower("name"), + ) + queryset = queryset.order_by(*ordering) + + all_tags = list(queryset) + descendant_pks = {pk for tag in all_tags for pk in tag.get_descendants_pks()} + + if descendant_pks: + filter_q = self.get_document_count_filter() + children_source = ( + Tag.objects.filter(pk__in=descendant_pks | {t.pk for t in all_tags}) + .select_related("owner") + .annotate(document_count=Count("documents", filter=filter_q)) + .order_by(*ordering) + ) + else: + children_source = all_tags + + children_map = {} + for tag in children_source: + children_map.setdefault(tag.tn_parent_id, []).append(tag) + self._children_map = children_map + + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + def perform_update(self, serializer): old_parent = self.get_object().get_parent() tag = serializer.save() From 4347ba1f9cdefb9901b643f36f496b8fdd2d9d40 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:05:36 +0000 Subject: [PATCH 03/57] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 850c20ed5..29c8ccaeb 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-08 21:50+0000\n" +"POT-Creation-Date: 2026-01-12 21:04+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -1219,35 +1219,35 @@ msgstr "" msgid "workflow runs" msgstr "" -#: documents/serialisers.py:642 +#: documents/serialisers.py:646 msgid "Invalid color." msgstr "" -#: documents/serialisers.py:1846 +#: documents/serialisers.py:1850 #, python-format msgid "File type %(type)s not supported" msgstr "" -#: documents/serialisers.py:1890 +#: documents/serialisers.py:1894 #, python-format msgid "Custom field id must be an integer: %(id)s" msgstr "" -#: documents/serialisers.py:1897 +#: documents/serialisers.py:1901 #, python-format msgid "Custom field with id %(id)s does not exist" msgstr "" -#: documents/serialisers.py:1914 documents/serialisers.py:1924 +#: documents/serialisers.py:1918 documents/serialisers.py:1928 msgid "" "Custom fields must be a list of integers or an object mapping ids to values." msgstr "" -#: documents/serialisers.py:1919 +#: documents/serialisers.py:1923 msgid "Some custom fields don't exist or were specified twice." msgstr "" -#: documents/serialisers.py:2034 +#: documents/serialisers.py:2038 msgid "Invalid variable detected." msgstr "" From e940764fe01038bec4d0fb14009dc375b44d9323 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:24:42 -0800 Subject: [PATCH 04/57] Feature: Paperless AI (#10319) --- .github/workflows/ci-docker.yml | 6 + docker/install_management_commands.sh | 1 + docker/rootfs/usr/local/bin/document_llmindex | 14 + docs/configuration.md | 64 + docs/index.md | 1 + docs/usage.md | 22 + pyproject.toml | 10 + .../admin/config/config.component.html | 4 + .../admin/config/config.component.ts | 2 + .../admin/settings/settings.component.spec.ts | 3 + .../app-frame/app-frame.component.html | 3 + .../app-frame/app-frame.component.ts | 6 + .../toasts-dropdown.component.html | 2 +- .../components/chat/chat/chat.component.html | 35 + .../components/chat/chat/chat.component.scss | 37 + .../chat/chat/chat.component.spec.ts | 132 ++ .../components/chat/chat/chat.component.ts | 140 ++ .../custom-fields-dropdown.component.html | 6 +- .../input/password/password.component.html | 35 +- .../common/input/text/text.component.html | 6 + .../common/input/text/text.component.spec.ts | 20 +- .../common/input/text/text.component.ts | 20 +- .../suggestions-dropdown.component.html | 49 + .../suggestions-dropdown.component.scss | 3 + .../suggestions-dropdown.component.spec.ts | 51 + .../suggestions-dropdown.component.ts | 64 + .../system-status-dialog.component.html | 37 + .../system-status-dialog.component.spec.ts | 3 + .../system-status-dialog.component.ts | 7 + .../document-detail.component.html | 46 +- .../document-detail.component.spec.ts | 65 +- .../document-detail.component.ts | 107 +- src-ui/src/app/data/document-suggestions.ts | 6 + src-ui/src/app/data/paperless-config.ts | 72 + src-ui/src/app/data/paperless-task.ts | 1 + src-ui/src/app/data/system-status.ts | 4 + src-ui/src/app/data/ui-settings.ts | 6 + .../src/app/interceptors/csrf.interceptor.ts | 6 +- src-ui/src/app/services/chat.service.spec.ts | 58 + src-ui/src/app/services/chat.service.ts | 46 + src-ui/src/main.ts | 7 +- src/documents/apps.py | 2 + src/documents/caching.py | 49 + .../management/commands/document_llmindex.py | 22 + .../1075_alter_paperlesstask_task_name.py | 30 + src/documents/models.py | 1 + src/documents/signals/handlers.py | 35 + src/documents/tasks.py | 67 + src/documents/tests/test_api_app_config.py | 81 + src/documents/tests/test_api_status.py | 66 + src/documents/tests/test_api_uisettings.py | 1 + src/documents/tests/test_tasks.py | 103 ++ src/documents/tests/test_views.py | 181 +++ src/documents/views.py | 222 ++- src/paperless/config.py | 34 + ...cationconfiguration_ai_enabled_and_more.py | 84 + src/paperless/models.py | 68 + src/paperless/serialisers.py | 9 + src/paperless/settings.py | 46 + src/paperless/tests/test_settings.py | 23 + src/paperless/urls.py | 6 + src/paperless/views.py | 26 + src/paperless_ai/__init__.py | 0 src/paperless_ai/ai_classifier.py | 102 ++ src/paperless_ai/base_model.py | 10 + src/paperless_ai/chat.py | 105 ++ src/paperless_ai/client.py | 69 + src/paperless_ai/embedding.py | 92 ++ src/paperless_ai/indexing.py | 283 ++++ src/paperless_ai/matching.py | 102 ++ src/paperless_ai/tests/__init__.py | 0 src/paperless_ai/tests/test_ai_classifier.py | 186 +++ src/paperless_ai/tests/test_ai_indexing.py | 334 ++++ src/paperless_ai/tests/test_chat.py | 142 ++ src/paperless_ai/tests/test_client.py | 111 ++ src/paperless_ai/tests/test_embedding.py | 169 ++ src/paperless_ai/tests/test_matching.py | 86 + uv.lock | 1381 ++++++++++++++++- 78 files changed, 5429 insertions(+), 106 deletions(-) create mode 100755 docker/rootfs/usr/local/bin/document_llmindex create mode 100644 src-ui/src/app/components/chat/chat/chat.component.html create mode 100644 src-ui/src/app/components/chat/chat/chat.component.scss create mode 100644 src-ui/src/app/components/chat/chat/chat.component.spec.ts create mode 100644 src-ui/src/app/components/chat/chat/chat.component.ts create mode 100644 src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html create mode 100644 src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss create mode 100644 src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts create mode 100644 src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts create mode 100644 src-ui/src/app/services/chat.service.spec.ts create mode 100644 src-ui/src/app/services/chat.service.ts create mode 100644 src/documents/management/commands/document_llmindex.py create mode 100644 src/documents/migrations/1075_alter_paperlesstask_task_name.py create mode 100644 src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py create mode 100644 src/paperless_ai/__init__.py create mode 100644 src/paperless_ai/ai_classifier.py create mode 100644 src/paperless_ai/base_model.py create mode 100644 src/paperless_ai/chat.py create mode 100644 src/paperless_ai/client.py create mode 100644 src/paperless_ai/embedding.py create mode 100644 src/paperless_ai/indexing.py create mode 100644 src/paperless_ai/matching.py create mode 100644 src/paperless_ai/tests/__init__.py create mode 100644 src/paperless_ai/tests/test_ai_classifier.py create mode 100644 src/paperless_ai/tests/test_ai_indexing.py create mode 100644 src/paperless_ai/tests/test_chat.py create mode 100644 src/paperless_ai/tests/test_client.py create mode 100644 src/paperless_ai/tests/test_embedding.py create mode 100644 src/paperless_ai/tests/test_matching.py diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 5793ecfa1..7ecdb055c 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -98,6 +98,12 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Maximize space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Docker metadata id: docker-meta uses: docker/metadata-action@v5.10.0 diff --git a/docker/install_management_commands.sh b/docker/install_management_commands.sh index 17dae68a2..be972d605 100755 --- a/docker/install_management_commands.sh +++ b/docker/install_management_commands.sh @@ -11,6 +11,7 @@ for command in decrypt_documents \ mail_fetcher \ document_create_classifier \ document_index \ + document_llmindex \ document_renamer \ document_retagger \ document_thumbnails \ diff --git a/docker/rootfs/usr/local/bin/document_llmindex b/docker/rootfs/usr/local/bin/document_llmindex new file mode 100755 index 000000000..8e51245e1 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_llmindex @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_llmindex "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_llmindex "$@" +else + echo "Unknown user." +fi diff --git a/docs/configuration.md b/docs/configuration.md index e1f6f6d4c..2517d9cc1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1824,3 +1824,67 @@ password. All of these options come from their similarly-named [Django settings] : The endpoint to use for the remote OCR engine. This is required for Azure AI. Defaults to None. + +## AI {#ai} + +#### [`PAPERLESS_AI_ENABLED=`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED} + +: Enables the AI features in Paperless. This includes the AI-based +suggestions. This setting is required to be set to true in order to use the AI features. + + Defaults to false. + +#### [`PAPERLESS_AI_LLM_EMBEDDING_BACKEND=`](#PAPERLESS_AI_LLM_EMBEDDING_BACKEND) {#PAPERLESS_AI_LLM_EMBEDDING_BACKEND} + +: The embedding backend to use for RAG. This can be either "openai" or "huggingface". + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_EMBEDDING_MODEL=`](#PAPERLESS_AI_LLM_EMBEDDING_MODEL) {#PAPERLESS_AI_LLM_EMBEDDING_MODEL} + +: The model to use for the embedding backend for RAG. This can be set to any of the embedding models supported by the current embedding backend. If not supplied, defaults to "text-embedding-3-small" for OpenAI and "sentence-transformers/all-MiniLM-L6-v2" for Huggingface. + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_BACKEND=`](#PAPERLESS_AI_LLM_BACKEND) {#PAPERLESS_AI_LLM_BACKEND} + +: The AI backend to use. This can be either "openai" or "ollama". If set to "ollama", the AI +features will be run locally on your machine. If set to "openai", the AI features will be run +using the OpenAI API. This setting is required to be set to use the AI features. + + Defaults to None. + + !!! note + + The OpenAI API is a paid service. You will need to set up an OpenAI account and + will be charged for usage incurred by Paperless-ngx features and your document data + will (of course) be sent to the OpenAI API. Paperless-ngx does not endorse the use of the + OpenAI API in any way. + + Refer to the OpenAI terms of service, and use at your own risk. + +#### [`PAPERLESS_AI_LLM_MODEL=`](#PAPERLESS_AI_LLM_MODEL) {#PAPERLESS_AI_LLM_MODEL} + +: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the +current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3" for Ollama. + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_API_KEY=`](#PAPERLESS_AI_LLM_API_KEY) {#PAPERLESS_AI_LLM_API_KEY} + +: The API key to use for the AI backend. This is required for the OpenAI backend (optional for others). + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_ENDPOINT=`](#PAPERLESS_AI_LLM_ENDPOINT) {#PAPERLESS_AI_LLM_ENDPOINT} + +: The endpoint / url to use for the AI backend. This is required for the Ollama backend (optional for others). + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_INDEX_TASK_CRON=`](#PAPERLESS_AI_LLM_INDEX_TASK_CRON) {#PAPERLESS_AI_LLM_INDEX_TASK_CRON} + +: Configures the schedule to update the AI embeddings of text content and metadata for all documents. Only performed if +AI is enabled and the LLM embedding backend is set. + + Defaults to `10 2 * * *`, once per day. diff --git a/docs/index.md b/docs/index.md index c84cd0ce4..1d72f8f6c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ physical documents into a searchable online archive so you can keep, well, _less - _New!_ Supports remote OCR with Azure AI (opt-in). - Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals. - Uses machine-learning to automatically add tags, correspondents and document types to your documents. +- **New**: Paperless-ngx can now leverage AI (Large Language Models or LLMs) for document suggestions. This is an optional feature that can be enabled (and is disabled by default). - Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more. - Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents. - **Beautiful, modern web application** that features: diff --git a/docs/usage.md b/docs/usage.md index a307db3cd..f5c99aeaf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -278,6 +278,28 @@ Once setup, navigating to the email settings page in Paperless-ngx will allow yo You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads) for details. +## Document Suggestions + +Paperless-ngx can suggest tags, correspondents, document types and storage paths for documents based on the content of the document. This is done using a (non-LLM) machine learning model that is trained on the documents in your database. The suggestions are shown in the document detail page and can be accepted or rejected by the user. + +## AI Features + +Paperless-ngx includes several features that use AI to enhance the document management experience. These features are optional and can be enabled or disabled in the settings. If you are using the AI features, you may want to also enable the "LLM index" feature, which supports Retrieval-Augmented Generation (RAG) designed to improve the quality of AI responses. The LLM index feature is not enabled by default and requires additional configuration. + +!!! warning + + Remember that Paperless-ngx will send document content to the AI provider you have configured, so consider the privacy implications of using these features, especially if using a remote model (e.g. OpenAI), instead of the default local model. + +The AI features work by creating an embedding of the text content and metadata of documents, which is then used for various tasks such as similarity search and question answering. This uses the FAISS vector store. + +### AI-Enhanced Suggestions + +If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags, correspondents and document types for documents. This feature will always be "opt-in" and does not disable the existing classifier-based suggestion system. Currently, both remote (via the OpenAI API) and local (via Ollama) models are supported, see [configuration](configuration.md#ai) for details. + +### Document Chat + +Paperless-ngx can use an AI LLM model to answer questions about a document or across multiple documents. Again, this feature works best when RAG is enabled. The chat feature is available in the upper app toolbar and will switch between chatting across multiple documents or a single document based on the current view. + ## Sharing documents from Paperless-ngx Paperless-ngx supports sharing documents with other users by assigning them [permissions](#object-permissions) diff --git a/pyproject.toml b/pyproject.toml index fb47e55f1..2ba8325b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "drf-spectacular~=0.28", "drf-spectacular-sidecar~=2025.10.1", "drf-writable-nested~=0.7.1", + "faiss-cpu>=1.10", "filelock~=3.20.0", "flower~=2.0.1", "gotenberg-client~=0.12.0", @@ -52,8 +53,15 @@ dependencies = [ "inotifyrecursive~=0.3", "jinja2~=3.1.5", "langdetect~=1.0.9", + "llama-index-core>=0.12.33.post1", + "llama-index-embeddings-huggingface>=0.5.3", + "llama-index-embeddings-openai>=0.3.1", + "llama-index-llms-ollama>=0.5.4", + "llama-index-llms-openai>=0.3.38", + "llama-index-vector-stores-faiss>=0.3", "nltk~=3.9.1", "ocrmypdf~=16.12.0", + "openai>=1.76", "pathvalidate~=3.3.1", "pdf2image~=1.17.0", "python-dateutil~=2.9.0", @@ -66,6 +74,7 @@ dependencies = [ "redis[hiredis]~=5.2.1", "regex>=2025.9.18", "scikit-learn~=1.7.0", + "sentence-transformers>=4.1", "setproctitle~=1.3.4", "tika-client~=0.10.0", "tqdm~=4.67.1", @@ -255,6 +264,7 @@ testpaths = [ "src/paperless_tika/tests", "src/paperless_text/tests/", "src/paperless_remote/tests/", + "src/paperless_ai/tests", ] addopts = [ "--pythonwarnings=all", diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html index e1d7340a6..1d38a5d32 100644 --- a/src-ui/src/app/components/admin/config/config.component.html +++ b/src-ui/src/app/components/admin/config/config.component.html @@ -35,8 +35,12 @@ @case (ConfigOptionType.String) { } @case (ConfigOptionType.JSON) { } @case (ConfigOptionType.File) { } + @case (ConfigOptionType.Password) { } } + @if (option.note) { +
{{option.note}}
+ } diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts index eee617310..b0dcba57b 100644 --- a/src-ui/src/app/components/admin/config/config.component.ts +++ b/src-ui/src/app/components/admin/config/config.component.ts @@ -29,6 +29,7 @@ import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { FileComponent } from '../../common/input/file/file.component' import { NumberComponent } from '../../common/input/number/number.component' +import { PasswordComponent } from '../../common/input/password/password.component' import { SelectComponent } from '../../common/input/select/select.component' import { SwitchComponent } from '../../common/input/switch/switch.component' import { TextComponent } from '../../common/input/text/text.component' @@ -46,6 +47,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading TextComponent, NumberComponent, FileComponent, + PasswordComponent, AsyncPipe, NgbNavModule, FormsModule, diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index cc5c96640..650d6d8ea 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -91,6 +91,9 @@ const status: SystemStatus = { sanity_check_status: SystemStatusItemStatus.ERROR, sanity_check_last_run: new Date().toISOString(), sanity_check_error: 'Error running sanity check.', + llmindex_status: SystemStatusItemStatus.DISABLED, + llmindex_last_modified: new Date().toISOString(), + llmindex_error: null, }, } diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 673eaf03b..62a2e16cc 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -30,6 +30,9 @@
    + @if (aiEnabled) { + + } diff --git a/src-ui/src/app/components/chat/chat/chat.component.scss b/src-ui/src/app/components/chat/chat/chat.component.scss new file mode 100644 index 000000000..4b00cce1b --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.scss @@ -0,0 +1,37 @@ +.dropdown-menu { + width: var(--pngx-toast-max-width); +} + +.chat-messages { + max-height: 350px; + overflow-y: auto; +} + +.dropdown-toggle::after { + display: none; +} + +.dropdown-item { + white-space: initial; +} + +@media screen and (max-width: 400px) { + :host ::ng-deep .dropdown-menu-end { + right: -3rem; + } +} + +.blinking-cursor { + font-weight: bold; + font-size: 1.2em; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + from, to { + opacity: 0; + } + 50% { + opacity: 1; + } +} diff --git a/src-ui/src/app/components/chat/chat/chat.component.spec.ts b/src-ui/src/app/components/chat/chat/chat.component.spec.ts new file mode 100644 index 000000000..0ccb04a99 --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.spec.ts @@ -0,0 +1,132 @@ +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { ElementRef } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NavigationEnd, Router } from '@angular/router' +import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject } from 'rxjs' +import { ChatService } from 'src/app/services/chat.service' +import { ChatComponent } from './chat.component' + +describe('ChatComponent', () => { + let component: ChatComponent + let fixture: ComponentFixture + let chatService: ChatService + let router: Router + let routerEvents$: Subject + let mockStream$: Subject + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [NgxBootstrapIconsModule.pick(allIcons), ChatComponent], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(ChatComponent) + router = TestBed.inject(Router) + routerEvents$ = new Subject() + jest + .spyOn(router, 'events', 'get') + .mockReturnValue(routerEvents$.asObservable()) + chatService = TestBed.inject(ChatService) + mockStream$ = new Subject() + jest + .spyOn(chatService, 'streamChat') + .mockReturnValue(mockStream$.asObservable()) + component = fixture.componentInstance + + jest.useFakeTimers() + + fixture.detectChanges() + + component.scrollAnchor.nativeElement.scrollIntoView = jest.fn() + }) + + it('should update documentId on initialization', () => { + jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123') + component.ngOnInit() + expect(component.documentId).toBe(123) + }) + + it('should update documentId on navigation', () => { + component.ngOnInit() + routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456')) + expect(component.documentId).toBe(456) + }) + + it('should return correct placeholder based on documentId', () => { + component.documentId = 123 + expect(component.placeholder).toBe('Ask a question about this document...') + component.documentId = undefined + expect(component.placeholder).toBe('Ask a question about a document...') + }) + + it('should send a message and handle streaming response', () => { + component.input = 'Hello' + component.sendMessage() + + expect(component.messages.length).toBe(2) + expect(component.messages[0].content).toBe('Hello') + expect(component.loading).toBe(true) + + mockStream$.next('Hi') + expect(component.messages[1].content).toBe('H') + mockStream$.next('Hi there') + // advance time to process the typewriter effect + jest.advanceTimersByTime(1000) + expect(component.messages[1].content).toBe('Hi there') + + mockStream$.complete() + expect(component.loading).toBe(false) + expect(component.messages[1].isStreaming).toBe(false) + }) + + it('should handle errors during streaming', () => { + component.input = 'Hello' + component.sendMessage() + + mockStream$.error('Error') + expect(component.messages[1].content).toContain( + '⚠️ Error receiving response.' + ) + expect(component.loading).toBe(false) + }) + + it('should enqueue typewriter chunks correctly', () => { + const message = { content: '', role: 'assistant', isStreaming: true } + component.enqueueTypewriter(null, message as any) // coverage for null + component.enqueueTypewriter('Hello', message as any) + expect(component['typewriterBuffer'].length).toBe(4) + }) + + it('should scroll to bottom after sending a message', () => { + const scrollSpy = jest.spyOn( + ChatComponent.prototype as any, + 'scrollToBottom' + ) + component.input = 'Test' + component.sendMessage() + expect(scrollSpy).toHaveBeenCalled() + }) + + it('should focus chat input when dropdown is opened', () => { + const focus = jest.fn() + component.chatInput = { + nativeElement: { focus: focus }, + } as unknown as ElementRef + + component.onOpenChange(true) + jest.advanceTimersByTime(15) + expect(focus).toHaveBeenCalled() + }) + + it('should send message on Enter key press', () => { + jest.spyOn(component, 'sendMessage') + const event = new KeyboardEvent('keydown', { key: 'Enter' }) + component.searchInputKeyDown(event) + expect(component.sendMessage).toHaveBeenCalled() + }) +}) diff --git a/src-ui/src/app/components/chat/chat/chat.component.ts b/src-ui/src/app/components/chat/chat/chat.component.ts new file mode 100644 index 000000000..50d27e0b1 --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.ts @@ -0,0 +1,140 @@ +import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { NavigationEnd, Router } from '@angular/router' +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { filter, map } from 'rxjs' +import { ChatMessage, ChatService } from 'src/app/services/chat.service' + +@Component({ + selector: 'pngx-chat', + imports: [ + FormsModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + NgbDropdownModule, + ], + templateUrl: './chat.component.html', + styleUrl: './chat.component.scss', +}) +export class ChatComponent implements OnInit { + public messages: ChatMessage[] = [] + public loading = false + public input: string = '' + public documentId!: number + + private chatService: ChatService = inject(ChatService) + private router: Router = inject(Router) + + @ViewChild('scrollAnchor') scrollAnchor!: ElementRef + @ViewChild('chatInput') chatInput!: ElementRef + + private typewriterBuffer: string[] = [] + private typewriterActive = false + + public get placeholder(): string { + return this.documentId + ? $localize`Ask a question about this document...` + : $localize`Ask a question about a document...` + } + + ngOnInit(): void { + this.updateDocumentId(this.router.url) + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + map((event) => (event as NavigationEnd).url) + ) + .subscribe((url) => { + this.updateDocumentId(url) + }) + } + + private updateDocumentId(url: string): void { + const docIdRe = url.match(/^\/documents\/(\d+)/) + this.documentId = docIdRe ? +docIdRe[1] : undefined + } + + sendMessage(): void { + if (!this.input.trim()) return + + const userMessage: ChatMessage = { role: 'user', content: this.input } + this.messages.push(userMessage) + this.scrollToBottom() + + const assistantMessage: ChatMessage = { + role: 'assistant', + content: '', + isStreaming: true, + } + this.messages.push(assistantMessage) + this.loading = true + + let lastPartialLength = 0 + + this.chatService.streamChat(this.documentId, this.input).subscribe({ + next: (chunk) => { + const delta = chunk.substring(lastPartialLength) + lastPartialLength = chunk.length + this.enqueueTypewriter(delta, assistantMessage) + }, + error: () => { + assistantMessage.content += '\n\n⚠️ Error receiving response.' + assistantMessage.isStreaming = false + this.loading = false + }, + complete: () => { + assistantMessage.isStreaming = false + this.loading = false + this.scrollToBottom() + }, + }) + + this.input = '' + } + + enqueueTypewriter(chunk: string, message: ChatMessage): void { + if (!chunk) return + + this.typewriterBuffer.push(...chunk.split('')) + + if (!this.typewriterActive) { + this.typewriterActive = true + this.playTypewriter(message) + } + } + + playTypewriter(message: ChatMessage): void { + if (this.typewriterBuffer.length === 0) { + this.typewriterActive = false + return + } + + const nextChar = this.typewriterBuffer.shift() + message.content += nextChar + this.scrollToBottom() + + setTimeout(() => this.playTypewriter(message), 10) // 10ms per character + } + + private scrollToBottom(): void { + setTimeout(() => { + this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' }) + }, 50) + } + + public onOpenChange(open: boolean): void { + if (open) { + setTimeout(() => { + this.chatInput.nativeElement.focus() + }, 10) + } + } + + public searchInputKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault() + this.sendMessage() + } + } +} diff --git a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html index f58cfeeb9..f06f37dd0 100644 --- a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html +++ b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -1,7 +1,7 @@ -
    -
    diff --git a/src-ui/src/app/components/common/input/password/password.component.html b/src-ui/src/app/components/common/input/password/password.component.html index 9daa4be5f..b5d88f79c 100644 --- a/src-ui/src/app/components/common/input/password/password.component.html +++ b/src-ui/src/app/components/common/input/password/password.component.html @@ -1,17 +1,24 @@ -
    - -
    - - @if (showReveal) { - +
    +
    +
    + @if (title) { + + } +
    +
    +
    + + @if (showReveal) { + + } +
    +
    + {{error}} +
    + @if (hint) { + }
    -
    - {{error}} -
    - @if (hint) { - - }
    diff --git a/src-ui/src/app/components/common/input/text/text.component.html b/src-ui/src/app/components/common/input/text/text.component.html index dcc43f72c..5bf4dd93a 100644 --- a/src-ui/src/app/components/common/input/text/text.component.html +++ b/src-ui/src/app/components/common/input/text/text.component.html @@ -15,6 +15,12 @@ @if (hint) { } + @if (getSuggestion()?.length > 0) { + + Suggestion:  + {{getSuggestion()}}  + + }
    {{error}}
    diff --git a/src-ui/src/app/components/common/input/text/text.component.spec.ts b/src-ui/src/app/components/common/input/text/text.component.spec.ts index c5662b341..539c1eb6b 100644 --- a/src-ui/src/app/components/common/input/text/text.component.spec.ts +++ b/src-ui/src/app/components/common/input/text/text.component.spec.ts @@ -26,10 +26,20 @@ describe('TextComponent', () => { it('should support use of input field', () => { expect(component.value).toBeUndefined() - // TODO: why doesn't this work? - // input.value = 'foo' - // input.dispatchEvent(new Event('change')) - // fixture.detectChanges() - // expect(component.value).toEqual('foo') + input.value = 'foo' + input.dispatchEvent(new Event('input')) + fixture.detectChanges() + expect(component.value).toBe('foo') + }) + + it('should support suggestion', () => { + component.value = 'foo' + component.suggestion = 'foo' + expect(component.getSuggestion()).toBe('') + component.value = 'bar' + expect(component.getSuggestion()).toBe('foo') + component.applySuggestion() + fixture.detectChanges() + expect(component.value).toBe('foo') }) }) diff --git a/src-ui/src/app/components/common/input/text/text.component.ts b/src-ui/src/app/components/common/input/text/text.component.ts index 283a8eb71..22b1fed4a 100644 --- a/src-ui/src/app/components/common/input/text/text.component.ts +++ b/src-ui/src/app/components/common/input/text/text.component.ts @@ -4,6 +4,7 @@ import { NG_VALUE_ACCESSOR, ReactiveFormsModule, } from '@angular/forms' +import { RouterLink } from '@angular/router' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { AbstractInputComponent } from '../abstract-input' @@ -18,7 +19,12 @@ import { AbstractInputComponent } from '../abstract-input' selector: 'pngx-input-text', templateUrl: './text.component.html', styleUrls: ['./text.component.scss'], - imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule], + imports: [ + FormsModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + RouterLink, + ], }) export class TextComponent extends AbstractInputComponent { @Input() @@ -27,7 +33,19 @@ export class TextComponent extends AbstractInputComponent { @Input() placeholder: string = '' + @Input() + suggestion: string = '' + constructor() { super() } + + getSuggestion() { + return this.value !== this.suggestion ? this.suggestion : '' + } + + applySuggestion() { + this.value = this.suggestion + this.onChange(this.value) + } } diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html new file mode 100644 index 000000000..af207c05c --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html @@ -0,0 +1,49 @@ +
    + + + @if (aiEnabled) { +
    + + +
    +
    + @if (!suggestions?.suggested_tags && !suggestions?.suggested_document_types && !suggestions?.suggested_correspondents) { +
    + No novel suggestions +
    + } + @if (suggestions?.suggested_tags.length > 0) { + Tags + @for (tag of suggestions.suggested_tags; track tag) { + + } + } + @if (suggestions?.suggested_document_types.length > 0) { +
    Document Types
    + @for (type of suggestions.suggested_document_types; track type) { + + } + } + @if (suggestions?.suggested_correspondents.length > 0) { +
    Correspondents
    + @for (correspondent of suggestions.suggested_correspondents; track correspondent) { + + } + } +
    +
    +
    + } +
    diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss new file mode 100644 index 000000000..19aa1dc7d --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss @@ -0,0 +1,3 @@ +.suggestions-dropdown { + min-width: 250px; +} diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts new file mode 100644 index 000000000..801a56af3 --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { SuggestionsDropdownComponent } from './suggestions-dropdown.component' + +describe('SuggestionsDropdownComponent', () => { + let component: SuggestionsDropdownComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + NgxBootstrapIconsModule.pick(allIcons), + SuggestionsDropdownComponent, + ], + providers: [], + }) + fixture = TestBed.createComponent(SuggestionsDropdownComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should calculate totalSuggestions', () => { + component.suggestions = { + suggested_correspondents: ['John Doe'], + suggested_tags: ['Tag1', 'Tag2'], + suggested_document_types: ['Type1'], + } + expect(component.totalSuggestions).toBe(4) + }) + + it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => { + jest.spyOn(component.getSuggestions, 'emit') + component.suggestions = null + component.clickSuggest() + expect(component.getSuggestions.emit).toHaveBeenCalled() + }) + + it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => { + component.aiEnabled = true + fixture.detectChanges() + component.suggestions = { + suggested_correspondents: [], + suggested_tags: [], + suggested_document_types: [], + } + component.clickSuggest() + expect(component.dropdown.open).toBeTruthy() + }) +}) diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts new file mode 100644 index 000000000..b165f0a5e --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts @@ -0,0 +1,64 @@ +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' +import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { DocumentSuggestions } from 'src/app/data/document-suggestions' +import { pngxPopperOptions } from 'src/app/utils/popper-options' + +@Component({ + selector: 'pngx-suggestions-dropdown', + imports: [NgbDropdownModule, NgxBootstrapIconsModule], + templateUrl: './suggestions-dropdown.component.html', + styleUrl: './suggestions-dropdown.component.scss', +}) +export class SuggestionsDropdownComponent { + public popperOptions = pngxPopperOptions + + @ViewChild('dropdown') dropdown: NgbDropdown + + @Input() + suggestions: DocumentSuggestions = null + + @Input() + aiEnabled: boolean = false + + @Input() + loading: boolean = false + + @Input() + disabled: boolean = false + + @Output() + getSuggestions: EventEmitter = + new EventEmitter() + + @Output() + addTag: EventEmitter = new EventEmitter() + + @Output() + addDocumentType: EventEmitter = new EventEmitter() + + @Output() + addCorrespondent: EventEmitter = new EventEmitter() + + public clickSuggest(): void { + if (!this.suggestions) { + this.getSuggestions.emit(this) + } else { + this.dropdown?.toggle() + } + } + + get totalSuggestions(): number { + return ( + this.suggestions?.suggested_correspondents?.length + + this.suggestions?.suggested_tags?.length + + this.suggestions?.suggested_document_types?.length || 0 + ) + } +} diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html index 99fddbf2c..c34f984b2 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html @@ -266,6 +266,43 @@ } + @if (aiEnabled) { +
    AI Index
    +
    + + @if (currentUserIsSuperUser) { + @if (isRunning(PaperlessTaskName.LLMIndexUpdate)) { +
    + } @else { + + } + } +
    + + @if (status.tasks.llmindex_status === 'OK') { +
    Last Run:
    {{status.tasks.llmindex_last_modified | customDate:'medium'}} + } @else { +
    Error:
    {{status.tasks.llmindex_error}} + } +
    + }
    diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts index 1785459f4..0fd331b10 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts @@ -68,6 +68,9 @@ const status: SystemStatus = { sanity_check_status: SystemStatusItemStatus.OK, sanity_check_last_run: new Date().toISOString(), sanity_check_error: null, + llmindex_status: SystemStatusItemStatus.OK, + llmindex_last_modified: new Date().toISOString(), + llmindex_error: null, }, } diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts index f88d56ff6..d53bb74bf 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts @@ -13,9 +13,11 @@ import { SystemStatus, SystemStatusItemStatus, } from 'src/app/data/system-status' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { FileSizePipe } from 'src/app/pipes/file-size.pipe' import { PermissionsService } from 'src/app/services/permissions.service' +import { SettingsService } from 'src/app/services/settings.service' import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' import { ToastService } from 'src/app/services/toast.service' @@ -44,6 +46,7 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy { private toastService = inject(ToastService) private permissionsService = inject(PermissionsService) private websocketStatusService = inject(WebsocketStatusService) + private settingsService = inject(SettingsService) public SystemStatusItemStatus = SystemStatusItemStatus public PaperlessTaskName = PaperlessTaskName @@ -60,6 +63,10 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy { return this.permissionsService.isSuperUser() } + get aiEnabled(): boolean { + return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED) + } + public ngOnInit() { this.versionMismatch = environment.production && diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index f8a942ba3..44304c942 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -74,16 +74,6 @@
    - - - -
    + +
    + + +
    + +
    + + +
    +
    +
    @@ -129,7 +145,7 @@ Details
    - + @@ -139,7 +155,7 @@ (createNew)="createDocumentType($event)" [hideAddButton]="createDisabled(DataType.DocumentType)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> - + @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
    @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { @@ -361,14 +377,14 @@
    -
    +
    -
    +
    @if (hasNext()) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index b1b3650c6..198e7a7a4 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -157,6 +157,16 @@ describe('DocumentDetailComponent', () => { { provide: TagService, useValue: { + getCachedMany: (ids: number[]) => + of( + ids.map((id) => ({ + id, + name: `Tag${id}`, + is_inbox_tag: true, + color: '#ff0000', + text_color: '#000000', + })) + ), listAll: () => of({ count: 3, @@ -383,8 +393,32 @@ describe('DocumentDetailComponent', () => { currentUserCan = true }) - it('should support creating document type', () => { + it('should support creating tag, remove from suggestions', () => { initNormally() + component.suggestions = { + suggested_tags: ['Tag1', 'NewTag12'], + } + let openModal: NgbModalRef + modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) + const modalSpy = jest.spyOn(modalService, 'open') + component.createTag('NewTag12') + expect(modalSpy).toHaveBeenCalled() + openModal.componentInstance.succeeded.next({ + id: 12, + name: 'NewTag12', + is_inbox_tag: true, + color: '#ff0000', + text_color: '#000000', + }) + expect(component.tagsInput.value).toContain(12) + expect(component.suggestions.suggested_tags).not.toContain('NewTag12') + }) + + it('should support creating document type, remove from suggestions', () => { + initNormally() + component.suggestions = { + suggested_document_types: ['DocumentType1', 'NewDocType2'], + } let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') @@ -392,10 +426,16 @@ describe('DocumentDetailComponent', () => { expect(modalSpy).toHaveBeenCalled() openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' }) expect(component.documentForm.get('document_type').value).toEqual(12) + expect(component.suggestions.suggested_document_types).not.toContain( + 'NewDocType2' + ) }) - it('should support creating correspondent', () => { + it('should support creating correspondent, remove from suggestions', () => { initNormally() + component.suggestions = { + suggested_correspondents: ['Correspondent1', 'NewCorrrespondent12'], + } let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') @@ -406,6 +446,9 @@ describe('DocumentDetailComponent', () => { name: 'NewCorrrespondent12', }) expect(component.documentForm.get('correspondent').value).toEqual(12) + expect(component.suggestions.suggested_correspondents).not.toContain( + 'NewCorrrespondent12' + ) }) it('should support creating storage path', () => { @@ -996,7 +1039,7 @@ describe('DocumentDetailComponent', () => { expect(component.document.custom_fields).toHaveLength(initialLength - 1) expect(component.customFieldFormFields).toHaveLength(initialLength - 1) expect( - fixture.debugElement.query(By.css('form')).nativeElement.textContent + fixture.debugElement.query(By.css('form ul')).nativeElement.textContent ).not.toContain('Field 1') const patchSpy = jest.spyOn(documentService, 'patch') component.save(true) @@ -1087,10 +1130,22 @@ describe('DocumentDetailComponent', () => { it('should get suggestions', () => { const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') - suggestionsSpy.mockReturnValue(of({ tags: [42, 43] })) + suggestionsSpy.mockReturnValue( + of({ + tags: [42, 43], + suggested_tags: [], + suggested_document_types: [], + suggested_correspondents: [], + }) + ) initNormally() expect(suggestionsSpy).toHaveBeenCalled() - expect(component.suggestions).toEqual({ tags: [42, 43] }) + expect(component.suggestions).toEqual({ + tags: [42, 43], + suggested_tags: [], + suggested_document_types: [], + suggested_correspondents: [], + }) }) it('should show error if needed for get suggestions', () => { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index ce688f4ad..5054ed517 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -31,6 +31,7 @@ import { map, switchMap, takeUntil, + tap, } from 'rxjs/operators' import { Correspondent } from 'src/app/data/correspondent' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' @@ -76,6 +77,7 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service import { DocumentService } from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' +import { TagService } from 'src/app/services/rest/tag.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' @@ -89,6 +91,7 @@ import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspo import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component' import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' +import { TagEditDialogComponent } from '../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' import { EmailDocumentDialogComponent } from '../common/email-document-dialog/email-document-dialog.component' import { CheckComponent } from '../common/input/check/check.component' import { DateComponent } from '../common/input/date/date.component' @@ -107,6 +110,7 @@ import { PdfEditorEditMode, } from '../common/pdf-editor/pdf-editor.component' import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component' +import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component' import { DocumentHistoryComponent } from '../document-history/document-history.component' import { DocumentNotesComponent } from '../document-notes/document-notes.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' @@ -163,6 +167,7 @@ export enum ZoomSetting { NumberComponent, MonetaryComponent, UrlComponent, + SuggestionsDropdownComponent, CustomDatePipe, FileSizePipe, IfPermissionsDirective, @@ -184,6 +189,7 @@ export class DocumentDetailComponent { private documentsService = inject(DocumentService) private route = inject(ActivatedRoute) + private tagService = inject(TagService) private correspondentService = inject(CorrespondentService) private documentTypeService = inject(DocumentTypeService) private router = inject(Router) @@ -206,6 +212,8 @@ export class DocumentDetailComponent @ViewChild('inputTitle') titleInput: TextComponent + @ViewChild('tagsInput') tagsInput: TagsComponent + expandOriginalMetadata = false expandArchivedMetadata = false @@ -217,6 +225,7 @@ export class DocumentDetailComponent document: Document metadata: DocumentMetadata suggestions: DocumentSuggestions + suggestionsLoading: boolean = false users: User[] title: string @@ -298,6 +307,10 @@ export class DocumentDetailComponent return this.deviceDetectorService.isMobile() } + get aiEnabled(): boolean { + return this.settings.get(SETTINGS_KEYS.AI_ENABLED) + } + get archiveContentRenderType(): ContentRenderType { return this.document?.archived_file_name ? this.getRenderType('application/pdf') @@ -682,25 +695,12 @@ export class DocumentDetailComponent PermissionType.Document ) ) { - this.documentsService - .getSuggestions(doc.id) - .pipe( - first(), - takeUntil(this.unsubscribeNotifier), - takeUntil(this.docChangeNotifier) - ) - .subscribe({ - next: (result) => { - this.suggestions = result - }, - error: (error) => { - this.suggestions = null - this.toastService.showError( - $localize`Error retrieving suggestions.`, - error - ) - }, - }) + this.tagService.getCachedMany(doc.tags).subscribe((tags) => { + // only show suggestions if document has inbox tags + if (tags.some((tag) => tag.is_inbox_tag)) { + this.getSuggestions() + } + }) } this.title = this.documentTitlePipe.transform(doc.title) this.prepareForm(doc) @@ -710,6 +710,63 @@ export class DocumentDetailComponent return this.documentForm.get('custom_fields') as FormArray } + getSuggestions() { + this.suggestionsLoading = true + this.documentsService + .getSuggestions(this.documentId) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe({ + next: (result) => { + this.suggestions = result + this.suggestionsLoading = false + }, + error: (error) => { + this.suggestions = null + this.suggestionsLoading = false + this.toastService.showError( + $localize`Error retrieving suggestions.`, + error + ) + }, + }) + } + + createTag(newName: string) { + var modal = this.modalService.open(TagEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + if (newName) modal.componentInstance.object = { name: newName } + modal.componentInstance.succeeded + .pipe( + tap((newTag: Tag) => { + // remove from suggestions if present + if (this.suggestions) { + this.suggestions = { + ...this.suggestions, + suggested_tags: this.suggestions.suggested_tags.filter( + (tag) => tag !== newTag.name + ), + } + } + }), + switchMap((newTag: Tag) => { + return this.tagService + .listAll() + .pipe(map((tags) => ({ newTag, tags }))) + }), + takeUntil(this.unsubscribeNotifier) + ) + .subscribe(({ newTag, tags }) => { + this.tagsInput.tags = tags.results + this.tagsInput.addTag(newTag.id) + }) + } + createDocumentType(newName: string) { var modal = this.modalService.open(DocumentTypeEditDialogComponent, { backdrop: 'static', @@ -729,6 +786,12 @@ export class DocumentDetailComponent this.documentTypes = documentTypes.results this.documentForm.get('document_type').setValue(newDocumentType.id) this.documentForm.get('document_type').markAsDirty() + if (this.suggestions) { + this.suggestions.suggested_document_types = + this.suggestions.suggested_document_types.filter( + (dt) => dt !== newName + ) + } }) } @@ -753,6 +816,12 @@ export class DocumentDetailComponent this.correspondents = correspondents.results this.documentForm.get('correspondent').setValue(newCorrespondent.id) this.documentForm.get('correspondent').markAsDirty() + if (this.suggestions) { + this.suggestions.suggested_correspondents = + this.suggestions.suggested_correspondents.filter( + (c) => c !== newName + ) + } }) } diff --git a/src-ui/src/app/data/document-suggestions.ts b/src-ui/src/app/data/document-suggestions.ts index 85f9f9d22..447c4402b 100644 --- a/src-ui/src/app/data/document-suggestions.ts +++ b/src-ui/src/app/data/document-suggestions.ts @@ -1,11 +1,17 @@ export interface DocumentSuggestions { + title?: string + tags?: number[] + suggested_tags?: string[] correspondents?: number[] + suggested_correspondents?: string[] document_types?: number[] + suggested_document_types?: string[] storage_paths?: number[] + suggested_storage_paths?: string[] dates?: string[] // ISO-formatted date string e.g. 2022-11-03 } diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts index 3afca66ff..fd151002d 100644 --- a/src-ui/src/app/data/paperless-config.ts +++ b/src-ui/src/app/data/paperless-config.ts @@ -44,12 +44,24 @@ export enum ConfigOptionType { Boolean = 'boolean', JSON = 'json', File = 'file', + Password = 'password', } export const ConfigCategory = { General: $localize`General Settings`, OCR: $localize`OCR Settings`, Barcode: $localize`Barcode Settings`, + AI: $localize`AI Settings`, +} + +export const LLMEmbeddingBackendConfig = { + OPENAI: 'openai', + HUGGINGFACE: 'huggingface', +} + +export const LLMBackendConfig = { + OPENAI: 'openai', + OLLAMA: 'ollama', } export interface ConfigOption { @@ -59,6 +71,7 @@ export interface ConfigOption { choices?: Array<{ id: string; name: string }> config_key?: string category: string + note?: string } function mapToItems(enumObj: Object): Array<{ id: string; name: string }> { @@ -258,6 +271,58 @@ export const PaperlessConfigOptions: ConfigOption[] = [ config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING', category: ConfigCategory.Barcode, }, + { + key: 'ai_enabled', + title: $localize`AI Enabled`, + type: ConfigOptionType.Boolean, + config_key: 'PAPERLESS_AI_ENABLED', + category: ConfigCategory.AI, + note: $localize`Consider privacy implications when enabling AI features, especially if using a remote model.`, + }, + { + key: 'llm_embedding_backend', + title: $localize`LLM Embedding Backend`, + type: ConfigOptionType.Select, + choices: mapToItems(LLMEmbeddingBackendConfig), + config_key: 'PAPERLESS_AI_LLM_EMBEDDING_BACKEND', + category: ConfigCategory.AI, + }, + { + key: 'llm_embedding_model', + title: $localize`LLM Embedding Model`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL', + category: ConfigCategory.AI, + }, + { + key: 'llm_backend', + title: $localize`LLM Backend`, + type: ConfigOptionType.Select, + choices: mapToItems(LLMBackendConfig), + config_key: 'PAPERLESS_AI_LLM_BACKEND', + category: ConfigCategory.AI, + }, + { + key: 'llm_model', + title: $localize`LLM Model`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_MODEL', + category: ConfigCategory.AI, + }, + { + key: 'llm_api_key', + title: $localize`LLM API Key`, + type: ConfigOptionType.Password, + config_key: 'PAPERLESS_AI_LLM_API_KEY', + category: ConfigCategory.AI, + }, + { + key: 'llm_endpoint', + title: $localize`LLM Endpoint`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_ENDPOINT', + category: ConfigCategory.AI, + }, ] export interface PaperlessConfig extends ObjectWithId { @@ -287,4 +352,11 @@ export interface PaperlessConfig extends ObjectWithId { barcode_max_pages: number barcode_enable_tag: boolean barcode_tag_mapping: object + ai_enabled: boolean + llm_embedding_backend: string + llm_embedding_model: string + llm_backend: string + llm_model: string + llm_api_key: string + llm_endpoint: string } diff --git a/src-ui/src/app/data/paperless-task.ts b/src-ui/src/app/data/paperless-task.ts index 1bec277eb..b30af7cdd 100644 --- a/src-ui/src/app/data/paperless-task.ts +++ b/src-ui/src/app/data/paperless-task.ts @@ -11,6 +11,7 @@ export enum PaperlessTaskName { TrainClassifier = 'train_classifier', SanityCheck = 'check_sanity', IndexOptimize = 'index_optimize', + LLMIndexUpdate = 'llmindex_update', } export enum PaperlessTaskStatus { diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts index 334dc54f8..7dcbffa20 100644 --- a/src-ui/src/app/data/system-status.ts +++ b/src-ui/src/app/data/system-status.ts @@ -7,6 +7,7 @@ export enum SystemStatusItemStatus { OK = 'OK', ERROR = 'ERROR', WARNING = 'WARNING', + DISABLED = 'DISABLED', } export interface SystemStatus { @@ -43,6 +44,9 @@ export interface SystemStatus { sanity_check_status: SystemStatusItemStatus sanity_check_last_run: string // ISO date string sanity_check_error: string + llmindex_status: SystemStatusItemStatus + llmindex_last_modified: string // ISO date string + llmindex_error: string } websocket_connected?: SystemStatusItemStatus // added client-side } diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts index 6ace74810..e797fe9b3 100644 --- a/src-ui/src/app/data/ui-settings.ts +++ b/src-ui/src/app/data/ui-settings.ts @@ -76,6 +76,7 @@ export const SETTINGS_KEYS = { GMAIL_OAUTH_URL: 'gmail_oauth_url', OUTLOOK_OAUTH_URL: 'outlook_oauth_url', EMAIL_ENABLED: 'email_enabled', + AI_ENABLED: 'ai_enabled', } export const SETTINGS: UiSetting[] = [ @@ -289,4 +290,9 @@ export const SETTINGS: UiSetting[] = [ type: 'string', default: 'page-width', // ZoomSetting from 'document-detail.component' }, + { + key: SETTINGS_KEYS.AI_ENABLED, + type: 'boolean', + default: false, + }, ] diff --git a/src-ui/src/app/interceptors/csrf.interceptor.ts b/src-ui/src/app/interceptors/csrf.interceptor.ts index 2f590c5eb..03a2fa7b3 100644 --- a/src-ui/src/app/interceptors/csrf.interceptor.ts +++ b/src-ui/src/app/interceptors/csrf.interceptor.ts @@ -4,15 +4,15 @@ import { HttpInterceptor, HttpRequest, } from '@angular/common/http' -import { Injectable, inject } from '@angular/core' +import { inject, Injectable } from '@angular/core' import { Meta } from '@angular/platform-browser' import { CookieService } from 'ngx-cookie-service' import { Observable } from 'rxjs' @Injectable() export class CsrfInterceptor implements HttpInterceptor { - private cookieService = inject(CookieService) - private meta = inject(Meta) + private cookieService: CookieService = inject(CookieService) + private meta: Meta = inject(Meta) intercept( request: HttpRequest, diff --git a/src-ui/src/app/services/chat.service.spec.ts b/src-ui/src/app/services/chat.service.spec.ts new file mode 100644 index 000000000..b8ca957cb --- /dev/null +++ b/src-ui/src/app/services/chat.service.spec.ts @@ -0,0 +1,58 @@ +import { + HttpEventType, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { environment } from 'src/environments/environment' +import { ChatService } from './chat.service' + +describe('ChatService', () => { + let service: ChatService + let httpMock: HttpTestingController + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + ChatService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }) + service = TestBed.inject(ChatService) + httpMock = TestBed.inject(HttpTestingController) + }) + + afterEach(() => { + httpMock.verify() + }) + + it('should stream chat messages', (done) => { + const documentId = 1 + const prompt = 'Hello, world!' + const mockResponse = 'Partial response text' + const apiUrl = `${environment.apiBaseUrl}documents/chat/` + + service.streamChat(documentId, prompt).subscribe((chunk) => { + expect(chunk).toBe(mockResponse) + done() + }) + + const req = httpMock.expectOne(apiUrl) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual({ + document_id: documentId, + q: prompt, + }) + + req.event({ + type: HttpEventType.DownloadProgress, + partialText: mockResponse, + } as any) + }) +}) diff --git a/src-ui/src/app/services/chat.service.ts b/src-ui/src/app/services/chat.service.ts new file mode 100644 index 000000000..9ddfb8330 --- /dev/null +++ b/src-ui/src/app/services/chat.service.ts @@ -0,0 +1,46 @@ +import { + HttpClient, + HttpDownloadProgressEvent, + HttpEventType, +} from '@angular/common/http' +import { inject, Injectable } from '@angular/core' +import { filter, map, Observable } from 'rxjs' +import { environment } from 'src/environments/environment' + +export interface ChatMessage { + role: 'user' | 'assistant' + content: string + isStreaming?: boolean +} + +@Injectable({ + providedIn: 'root', +}) +export class ChatService { + private http: HttpClient = inject(HttpClient) + + streamChat(documentId: number, prompt: string): Observable { + return this.http + .post( + `${environment.apiBaseUrl}documents/chat/`, + { + document_id: documentId, + q: prompt, + }, + { + observe: 'events', + reportProgress: true, + responseType: 'text', + withCredentials: true, + } + ) + .pipe( + map((event) => { + if (event.type === HttpEventType.DownloadProgress) { + return (event as HttpDownloadProgressEvent).partialText! + } + }), + filter((chunk) => !!chunk) + ) + } +} diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts index 7f1a39fbe..b85d8ff35 100644 --- a/src-ui/src/main.ts +++ b/src-ui/src/main.ts @@ -10,6 +10,7 @@ import { DatePipe, registerLocaleData } from '@angular/common' import { HTTP_INTERCEPTORS, provideHttpClient, + withFetch, withInterceptorsFromDi, } from '@angular/common/http' import { FormsModule, ReactiveFormsModule } from '@angular/forms' @@ -49,6 +50,7 @@ import { caretDown, caretUp, chatLeftText, + chatSquareDots, check, check2All, checkAll, @@ -124,6 +126,7 @@ import { sliders2Vertical, sortAlphaDown, sortAlphaUpAlt, + stars, tag, tagFill, tags, @@ -266,6 +269,7 @@ const icons = { caretDown, caretUp, chatLeftText, + chatSquareDots, check, check2All, checkAll, @@ -341,6 +345,7 @@ const icons = { sliders2Vertical, sortAlphaDown, sortAlphaUpAlt, + stars, tagFill, tag, tags, @@ -407,6 +412,6 @@ bootstrapApplication(AppComponent, { CorrespondentNamePipe, DocumentTypeNamePipe, StoragePathNamePipe, - provideHttpClient(withInterceptorsFromDi()), + provideHttpClient(withInterceptorsFromDi(), withFetch()), ], }).catch((err) => console.error(err)) diff --git a/src/documents/apps.py b/src/documents/apps.py index f3b798c0b..32e49b160 100644 --- a/src/documents/apps.py +++ b/src/documents/apps.py @@ -11,6 +11,7 @@ class DocumentsConfig(AppConfig): from documents.signals import document_consumption_finished from documents.signals import document_updated from documents.signals.handlers import add_inbox_tags + from documents.signals.handlers import add_or_update_document_in_llm_index from documents.signals.handlers import add_to_index from documents.signals.handlers import run_workflows_added from documents.signals.handlers import run_workflows_updated @@ -26,6 +27,7 @@ class DocumentsConfig(AppConfig): document_consumption_finished.connect(set_storage_path) document_consumption_finished.connect(add_to_index) document_consumption_finished.connect(run_workflows_added) + document_consumption_finished.connect(add_or_update_document_in_llm_index) document_updated.connect(run_workflows_updated) import documents.schema # noqa: F401 diff --git a/src/documents/caching.py b/src/documents/caching.py index ed7f6dbc1..f2911e51e 100644 --- a/src/documents/caching.py +++ b/src/documents/caching.py @@ -41,6 +41,7 @@ class SuggestionCacheData: CLASSIFIER_VERSION_KEY: Final[str] = "classifier_version" CLASSIFIER_HASH_KEY: Final[str] = "classifier_hash" CLASSIFIER_MODIFIED_KEY: Final[str] = "classifier_modified" +LLM_CACHE_CLASSIFIER_VERSION: Final[int] = 1000 # Marker distinguishing LLM suggestions CACHE_1_MINUTE: Final[int] = 60 CACHE_5_MINUTES: Final[int] = 5 * CACHE_1_MINUTE @@ -196,6 +197,54 @@ def refresh_suggestions_cache( cache.touch(doc_key, timeout) +def get_llm_suggestion_cache( + document_id: int, + backend: str, +) -> SuggestionCacheData | None: + doc_key = get_suggestion_cache_key(document_id) + data: SuggestionCacheData = cache.get(doc_key) + + if data and data.classifier_hash == backend: + return data + + return None + + +def set_llm_suggestions_cache( + document_id: int, + suggestions: dict, + *, + backend: str, + timeout: int = CACHE_50_MINUTES, +) -> None: + """ + Cache LLM-generated suggestions using a backend-specific identifier (e.g. 'openai:gpt-4'). + """ + doc_key = get_suggestion_cache_key(document_id) + cache.set( + doc_key, + SuggestionCacheData( + classifier_version=LLM_CACHE_CLASSIFIER_VERSION, + classifier_hash=backend, + suggestions=suggestions, + ), + timeout, + ) + + +def invalidate_llm_suggestions_cache( + document_id: int, +) -> None: + """ + Invalidate the LLM suggestions cache for a specific document and backend. + """ + doc_key = get_suggestion_cache_key(document_id) + data: SuggestionCacheData = cache.get(doc_key) + + if data: + cache.delete(doc_key) + + def get_metadata_cache_key(document_id: int) -> str: """ Returns the basic key for a document's metadata diff --git a/src/documents/management/commands/document_llmindex.py b/src/documents/management/commands/document_llmindex.py new file mode 100644 index 000000000..d2df02ed9 --- /dev/null +++ b/src/documents/management/commands/document_llmindex.py @@ -0,0 +1,22 @@ +from django.core.management import BaseCommand +from django.db import transaction + +from documents.management.commands.mixins import ProgressBarMixin +from documents.tasks import llmindex_index + + +class Command(ProgressBarMixin, BaseCommand): + help = "Manages the LLM-based vector index for Paperless." + + def add_arguments(self, parser): + parser.add_argument("command", choices=["rebuild", "update"]) + self.add_argument_progress_bar_mixin(parser) + + def handle(self, *args, **options): + self.handle_progress_bar_mixin(**options) + with transaction.atomic(): + llmindex_index( + progress_bar_disable=self.no_progress_bar, + rebuild=options["command"] == "rebuild", + scheduled=False, + ) diff --git a/src/documents/migrations/1075_alter_paperlesstask_task_name.py b/src/documents/migrations/1075_alter_paperlesstask_task_name.py new file mode 100644 index 000000000..2df0eaeb9 --- /dev/null +++ b/src/documents/migrations/1075_alter_paperlesstask_task_name.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.8 on 2025-04-30 02:38 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="paperlesstask", + name="task_name", + field=models.CharField( + choices=[ + ("consume_file", "Consume File"), + ("train_classifier", "Train Classifier"), + ("check_sanity", "Check Sanity"), + ("index_optimize", "Index Optimize"), + ("llmindex_update", "LLM Index Update"), + ], + help_text="Name of the task that was run", + max_length=255, + null=True, + verbose_name="Task Name", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 12dab2b6d..0bf3d48dd 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -598,6 +598,7 @@ class PaperlessTask(ModelWithOwner): TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier")) CHECK_SANITY = ("check_sanity", _("Check Sanity")) INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize")) + LLMINDEX_UPDATE = ("llmindex_update", _("LLM Index Update")) task_id = models.CharField( max_length=255, diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 5f2c8b4b2..c06ffb641 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -26,6 +26,8 @@ from filelock import FileLock from documents import matching from documents.caching import clear_document_caches +from documents.caching import invalidate_llm_suggestions_cache +from documents.data_models import ConsumableDocument from documents.file_handling import create_source_path_directory from documents.file_handling import delete_empty_directories from documents.file_handling import generate_filename @@ -52,6 +54,7 @@ from documents.workflows.mutations import apply_assignment_to_overrides from documents.workflows.mutations import apply_removal_to_document from documents.workflows.mutations import apply_removal_to_overrides from documents.workflows.utils import get_workflows_for_trigger +from paperless.config import AIConfig if TYPE_CHECKING: from documents.classifier import DocumentClassifier @@ -638,6 +641,15 @@ def cleanup_custom_field_deletion(sender, instance: CustomField, **kwargs): ) +@receiver(models.signals.post_save, sender=Document) +def update_llm_suggestions_cache(sender, instance, **kwargs): + """ + Invalidate the LLM suggestions cache when a document is saved. + """ + # Invalidate the cache for the document + invalidate_llm_suggestions_cache(instance.pk) + + @receiver(models.signals.post_delete, sender=User) @receiver(models.signals.post_delete, sender=Group) def cleanup_user_deletion(sender, instance: User | Group, **kwargs): @@ -944,3 +956,26 @@ def close_connection_pool_on_worker_init(**kwargs): for conn in connections.all(initialized_only=True): if conn.alias == "default" and hasattr(conn, "pool") and conn.pool: conn.close_pool() + + +def add_or_update_document_in_llm_index(sender, document, **kwargs): + """ + Add or update a document in the LLM index when it is created or updated. + """ + ai_config = AIConfig() + if ai_config.llm_index_enabled: + from documents.tasks import update_document_in_llm_index + + update_document_in_llm_index.delay(document) + + +@receiver(models.signals.post_delete, sender=Document) +def delete_document_from_llm_index(sender, instance: Document, **kwargs): + """ + Delete a document from the LLM index when it is deleted. + """ + ai_config = AIConfig() + if ai_config.llm_index_enabled: + from documents.tasks import remove_document_from_llm_index + + remove_document_from_llm_index.delay(instance) diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 6c415ad69..fed8a65f7 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -54,6 +54,10 @@ from documents.signals import document_updated from documents.signals.handlers import cleanup_document_deletion from documents.signals.handlers import run_workflows from documents.workflows.utils import get_workflows_for_trigger +from paperless.config import AIConfig +from paperless_ai.indexing import llm_index_add_or_update_document +from paperless_ai.indexing import llm_index_remove_document +from paperless_ai.indexing import update_llm_index if settings.AUDIT_LOG_ENABLED: from auditlog.models import LogEntry @@ -242,6 +246,13 @@ def bulk_update_documents(document_ids): for doc in documents: index.update_document(writer, doc) + ai_config = AIConfig() + if ai_config.llm_index_enabled: + update_llm_index( + progress_bar_disable=True, + rebuild=False, + ) + @shared_task def update_document_content_maybe_archive_file(document_id): @@ -341,6 +352,10 @@ def update_document_content_maybe_archive_file(document_id): with index.open_index_writer() as writer: index.update_document(writer, document) + ai_config = AIConfig() + if ai_config.llm_index_enabled: + llm_index_add_or_update_document(document) + clear_document_caches(document.pk) except Exception: @@ -558,3 +573,55 @@ def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None: if affected: bulk_update_documents.delay(document_ids=list(affected)) + + +@shared_task +def llmindex_index( + *, + progress_bar_disable=True, + rebuild=False, + scheduled=True, + auto=False, +): + ai_config = AIConfig() + if ai_config.llm_index_enabled: + task = PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK + if scheduled + else PaperlessTask.TaskType.AUTO + if auto + else PaperlessTask.TaskType.MANUAL_TASK, + task_id=uuid.uuid4(), + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + status=states.STARTED, + date_created=timezone.now(), + date_started=timezone.now(), + ) + from paperless_ai.indexing import update_llm_index + + try: + result = update_llm_index( + progress_bar_disable=progress_bar_disable, + rebuild=rebuild, + ) + task.status = states.SUCCESS + task.result = result + except Exception as e: + logger.error("LLM index error: " + str(e)) + task.status = states.FAILURE + task.result = str(e) + + task.date_done = timezone.now() + task.save(update_fields=["status", "result", "date_done"]) + else: + logger.info("LLM index is disabled, skipping update.") + + +@shared_task +def update_document_in_llm_index(document): + llm_index_add_or_update_document(document) + + +@shared_task +def remove_document_from_llm_index(document): + llm_index_remove_document(document) diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 6f487f5b0..2480e52ac 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -1,6 +1,7 @@ import json from io import BytesIO from pathlib import Path +from unittest.mock import patch from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile @@ -66,6 +67,13 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): "barcode_max_pages": None, "barcode_enable_tag": None, "barcode_tag_mapping": None, + "ai_enabled": False, + "llm_embedding_backend": None, + "llm_embedding_model": None, + "llm_backend": None, + "llm_model": None, + "llm_api_key": None, + "llm_endpoint": None, }, ) @@ -611,3 +619,76 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): ) self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEqual(ApplicationConfiguration.objects.count(), 1) + + def test_update_llm_api_key(self): + """ + GIVEN: + - Existing config with llm_api_key specified + WHEN: + - API to update llm_api_key is called with all *s + - API to update llm_api_key is called with empty string + THEN: + - llm_api_key is unchanged + - llm_api_key is set to None + """ + config = ApplicationConfiguration.objects.first() + config.llm_api_key = "1234567890" + config.save() + + # Test with all * + response = self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "llm_api_key": "*" * 32, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + config.refresh_from_db() + self.assertEqual(config.llm_api_key, "1234567890") + # Test with empty string + response = self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "llm_api_key": "", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + config.refresh_from_db() + self.assertEqual(config.llm_api_key, None) + + def test_enable_ai_index_triggers_update(self): + """ + GIVEN: + - Existing config with AI disabled + WHEN: + - Config is updated to enable AI with llm_embedding_backend + THEN: + - LLM index is triggered to update + """ + config = ApplicationConfiguration.objects.first() + config.ai_enabled = False + config.llm_embedding_backend = None + config.save() + + with ( + patch("documents.tasks.llmindex_index.delay") as mock_update, + patch("paperless_ai.indexing.vector_store_file_exists") as mock_exists, + ): + mock_exists.return_value = False + self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "ai_enabled": True, + "llm_embedding_backend": "openai", + }, + ), + content_type="application/json", + ) + mock_update.assert_called_once() diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py index 9b7bf37ad..1700366d8 100644 --- a/src/documents/tests/test_api_status.py +++ b/src/documents/tests/test_api_status.py @@ -310,3 +310,69 @@ class TestSystemStatus(APITestCase): "ERROR", ) self.assertIsNotNone(response.data["tasks"]["sanity_check_error"]) + + def test_system_status_ai_disabled(self): + """ + GIVEN: + - The AI feature is disabled + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=False): + self.client.force_login(self.user) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "DISABLED") + self.assertIsNone(response.data["tasks"]["llmindex_error"]) + + def test_system_status_ai_enabled(self): + """ + GIVEN: + - The AI index feature is enabled, but no tasks are found + - The AI index feature is enabled and a task is found + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND="openai"): + self.client.force_login(self.user) + + # No tasks found + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "WARNING") + + PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK, + status=states.SUCCESS, + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "OK") + self.assertIsNone(response.data["tasks"]["llmindex_error"]) + + def test_system_status_ai_error(self): + """ + GIVEN: + - The AI index feature is enabled and a task is found with an error + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND="openai"): + PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK, + status=states.FAILURE, + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + result="AI index update failed", + ) + self.client.force_login(self.user) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "ERROR") + self.assertIsNotNone(response.data["tasks"]["llmindex_error"]) diff --git a/src/documents/tests/test_api_uisettings.py b/src/documents/tests/test_api_uisettings.py index 26c6f17ae..c733315e6 100644 --- a/src/documents/tests/test_api_uisettings.py +++ b/src/documents/tests/test_api_uisettings.py @@ -49,6 +49,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase): "backend_setting": "default", }, "email_enabled": False, + "ai_enabled": False, }, ) diff --git a/src/documents/tests/test_tasks.py b/src/documents/tests/test_tasks.py index 11712549a..475709dd0 100644 --- a/src/documents/tests/test_tasks.py +++ b/src/documents/tests/test_tasks.py @@ -3,14 +3,17 @@ from datetime import timedelta from pathlib import Path from unittest import mock +from celery import states from django.conf import settings from django.test import TestCase +from django.test import override_settings from django.utils import timezone from documents import tasks from documents.models import Correspondent from documents.models import Document from documents.models import DocumentType +from documents.models import PaperlessTask from documents.models import Tag from documents.sanity_checker import SanityCheckFailedException from documents.sanity_checker import SanityCheckMessages @@ -270,3 +273,103 @@ class TestUpdateContent(DirectoriesMixin, TestCase): tasks.update_document_content_maybe_archive_file(doc.pk) self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test") + + +class TestAIIndex(DirectoriesMixin, TestCase): + @override_settings( + AI_ENABLED=True, + LLM_EMBEDDING_BACKEND="huggingface", + ) + def test_ai_index_success(self): + """ + GIVEN: + - Document exists, AI is enabled, llm index backend is set + WHEN: + - llmindex_index task is called + THEN: + - update_llm_index is called, and the task is marked as success + """ + Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + # lazy-loaded so mock the actual function + with mock.patch("paperless_ai.indexing.update_llm_index") as update_llm_index: + update_llm_index.return_value = "LLM index updated successfully." + tasks.llmindex_index() + update_llm_index.assert_called_once() + task = PaperlessTask.objects.get( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + self.assertEqual(task.status, states.SUCCESS) + self.assertEqual(task.result, "LLM index updated successfully.") + + @override_settings( + AI_ENABLED=True, + LLM_EMBEDDING_BACKEND="huggingface", + ) + def test_ai_index_failure(self): + """ + GIVEN: + - Document exists, AI is enabled, llm index backend is set + WHEN: + - llmindex_index task is called + THEN: + - update_llm_index raises an exception, and the task is marked as failure + """ + Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + # lazy-loaded so mock the actual function + with mock.patch("paperless_ai.indexing.update_llm_index") as update_llm_index: + update_llm_index.side_effect = Exception("LLM index update failed.") + tasks.llmindex_index() + update_llm_index.assert_called_once() + task = PaperlessTask.objects.get( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + self.assertEqual(task.status, states.FAILURE) + self.assertIn("LLM index update failed.", task.result) + + def test_update_document_in_llm_index(self): + """ + GIVEN: + - Nothing + WHEN: + - update_document_in_llm_index task is called + THEN: + - llm_index_add_or_update_document is called + """ + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + with mock.patch( + "documents.tasks.llm_index_add_or_update_document", + ) as llm_index_add_or_update_document: + tasks.update_document_in_llm_index(doc) + llm_index_add_or_update_document.assert_called_once_with(doc) + + def test_remove_document_from_llm_index(self): + """ + GIVEN: + - Nothing + WHEN: + - remove_document_from_llm_index task is called + THEN: + - llm_index_remove_document is called + """ + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + with mock.patch( + "documents.tasks.llm_index_remove_document", + ) as llm_index_remove_document: + tasks.remove_document_from_llm_index(doc) + llm_index_remove_document.assert_called_once_with(doc) diff --git a/src/documents/tests/test_views.py b/src/documents/tests/test_views.py index 4fa8fa833..a73016c26 100644 --- a/src/documents/tests/test_views.py +++ b/src/documents/tests/test_views.py @@ -2,6 +2,8 @@ import json import tempfile from datetime import timedelta from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch from django.conf import settings from django.contrib.auth.models import Group @@ -15,9 +17,15 @@ from django.utils import timezone from guardian.shortcuts import assign_perm from rest_framework import status +from documents.caching import get_llm_suggestion_cache +from documents.caching import set_llm_suggestions_cache +from documents.models import Correspondent from documents.models import Document +from documents.models import DocumentType from documents.models import ShareLink +from documents.models import StoragePath from documents.models import Tag +from documents.signals.handlers import update_llm_suggestions_cache from documents.tests.utils import DirectoriesMixin from paperless.models import ApplicationConfiguration @@ -270,3 +278,176 @@ class TestViews(DirectoriesMixin, TestCase): f"Possible N+1 queries detected: {num_queries_small} queries for 2 tags, " f"but {num_queries_large} queries for 50 tags" ) + + +class TestAISuggestions(DirectoriesMixin, TestCase): + def setUp(self): + self.user = User.objects.create_superuser(username="testuser") + self.document = Document.objects.create( + title="Test Document", + filename="test.pdf", + mime_type="application/pdf", + ) + self.tag1 = Tag.objects.create(name="tag1") + self.correspondent1 = Correspondent.objects.create(name="correspondent1") + self.document_type1 = DocumentType.objects.create(name="type1") + self.path1 = StoragePath.objects.create(name="path1") + super().setUp() + + @patch("documents.views.get_llm_suggestion_cache") + @patch("documents.views.refresh_suggestions_cache") + @override_settings( + AI_ENABLED=True, + LLM_BACKEND="mock_backend", + ) + def test_suggestions_with_cached_llm(self, mock_refresh_cache, mock_get_cache): + mock_get_cache.return_value = MagicMock(suggestions={"tags": ["tag1", "tag2"]}) + + self.client.force_login(user=self.user) + response = self.client.get(f"/api/documents/{self.document.pk}/suggestions/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), {"tags": ["tag1", "tag2"]}) + mock_refresh_cache.assert_called_once_with(self.document.pk) + + @patch("documents.views.get_ai_document_classification") + @override_settings( + AI_ENABLED=True, + LLM_BACKEND="mock_backend", + ) + def test_suggestions_with_ai_enabled( + self, + mock_get_ai_classification, + ): + mock_get_ai_classification.return_value = { + "title": "AI Title", + "tags": ["tag1", "tag2"], + "correspondents": ["correspondent1"], + "document_types": ["type1"], + "storage_paths": ["path1"], + "dates": ["2023-01-01"], + } + + self.client.force_login(user=self.user) + response = self.client.get(f"/api/documents/{self.document.pk}/suggestions/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json(), + { + "title": "AI Title", + "tags": [self.tag1.pk], + "suggested_tags": ["tag2"], + "correspondents": [self.correspondent1.pk], + "suggested_correspondents": [], + "document_types": [self.document_type1.pk], + "suggested_document_types": [], + "storage_paths": [self.path1.pk], + "suggested_storage_paths": [], + "dates": ["2023-01-01"], + }, + ) + + def test_invalidate_suggestions_cache(self): + self.client.force_login(user=self.user) + suggestions = { + "title": "AI Title", + "tags": ["tag1", "tag2"], + "correspondents": ["correspondent1"], + "document_types": ["type1"], + "storage_paths": ["path1"], + "dates": ["2023-01-01"], + } + set_llm_suggestions_cache( + self.document.pk, + suggestions, + backend="mock_backend", + ) + self.assertEqual( + get_llm_suggestion_cache( + self.document.pk, + backend="mock_backend", + ).suggestions, + suggestions, + ) + # post_save signal triggered + update_llm_suggestions_cache( + sender=None, + instance=self.document, + ) + self.assertIsNone( + get_llm_suggestion_cache( + self.document.pk, + backend="mock_backend", + ), + ) + + +class TestAIChatStreamingView(DirectoriesMixin, TestCase): + ENDPOINT = "/api/documents/chat/" + + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="pass") + self.client.force_login(user=self.user) + self.document = Document.objects.create( + title="Test Document", + filename="test.pdf", + mime_type="application/pdf", + ) + super().setUp() + + @override_settings(AI_ENABLED=False) + def test_post_ai_disabled(self): + response = self.client.post( + self.ENDPOINT, + data='{"q": "question"}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + self.assertIn(b"AI is required for this feature", response.content) + + @patch("documents.views.stream_chat_with_documents") + @patch("documents.views.get_objects_for_user_owner_aware") + @override_settings(AI_ENABLED=True) + def test_post_no_document_id(self, mock_get_objects, mock_stream_chat): + mock_get_objects.return_value = [self.document] + mock_stream_chat.return_value = iter([b"data"]) + response = self.client.post( + self.ENDPOINT, + data='{"q": "question"}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/event-stream") + + @patch("documents.views.stream_chat_with_documents") + @override_settings(AI_ENABLED=True) + def test_post_with_document_id(self, mock_stream_chat): + mock_stream_chat.return_value = iter([b"data"]) + response = self.client.post( + self.ENDPOINT, + data=f'{{"q": "question", "document_id": {self.document.pk}}}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/event-stream") + + @override_settings(AI_ENABLED=True) + def test_post_with_invalid_document_id(self): + response = self.client.post( + self.ENDPOINT, + data='{"q": "question", "document_id": 999999}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + self.assertIn(b"Document not found", response.content) + + @patch("documents.views.has_perms_owner_aware") + @override_settings(AI_ENABLED=True) + def test_post_with_document_id_no_permission(self, mock_has_perms): + mock_has_perms.return_value = False + response = self.client.post( + self.ENDPOINT, + data=f'{{"q": "question", "document_id": {self.document.pk}}}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 403) + self.assertIn(b"Insufficient permissions", response.content) diff --git a/src/documents/views.py b/src/documents/views.py index 99b7eb00b..730a6dc1a 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -45,6 +45,7 @@ from django.http import HttpResponseBadRequest from django.http import HttpResponseForbidden from django.http import HttpResponseRedirect from django.http import HttpResponseServerError +from django.http import StreamingHttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.decorators import method_decorator @@ -52,6 +53,7 @@ from django.utils.timezone import make_aware from django.utils.translation import get_language from django.views import View from django.views.decorators.cache import cache_control +from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import condition from django.views.decorators.http import last_modified from django.views.generic import TemplateView @@ -91,10 +93,12 @@ from documents import index from documents.bulk_download import ArchiveOnlyStrategy from documents.bulk_download import OriginalAndArchiveStrategy from documents.bulk_download import OriginalsOnlyStrategy +from documents.caching import get_llm_suggestion_cache from documents.caching import get_metadata_cache from documents.caching import get_suggestion_cache from documents.caching import refresh_metadata_cache from documents.caching import refresh_suggestions_cache +from documents.caching import set_llm_suggestions_cache from documents.caching import set_metadata_cache from documents.caching import set_suggestions_cache from documents.classifier import load_classifier @@ -182,18 +186,27 @@ from documents.signals import document_updated from documents.tasks import consume_file from documents.tasks import empty_trash from documents.tasks import index_optimize +from documents.tasks import llmindex_index from documents.tasks import sanity_check from documents.tasks import train_classifier from documents.tasks import update_document_parent_tags from documents.utils import get_boolean from paperless import version from paperless.celery import app as celery_app +from paperless.config import AIConfig from paperless.config import GeneralConfig from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless.serialisers import GroupSerializer from paperless.serialisers import UserSerializer from paperless.views import StandardPagination +from paperless_ai.ai_classifier import get_ai_document_classification +from paperless_ai.chat import stream_chat_with_documents +from paperless_ai.matching import extract_unmatched_names +from paperless_ai.matching import match_correspondents_by_name +from paperless_ai.matching import match_document_types_by_name +from paperless_ai.matching import match_storage_paths_by_name +from paperless_ai.matching import match_tags_by_name from paperless_mail.models import MailAccount from paperless_mail.models import MailRule from paperless_mail.oauth import PaperlessMailOAuth2Manager @@ -934,37 +947,103 @@ class DocumentViewSet( ): return HttpResponseForbidden("Insufficient permissions") - document_suggestions = get_suggestion_cache(doc.pk) + ai_config = AIConfig() - if document_suggestions is not None: - refresh_suggestions_cache(doc.pk) - return Response(document_suggestions.suggestions) - - classifier = load_classifier() - - dates = [] - if settings.NUMBER_OF_SUGGESTED_DATES > 0: - gen = parse_date_generator(doc.filename, doc.content) - dates = sorted( - {i for i in itertools.islice(gen, settings.NUMBER_OF_SUGGESTED_DATES)}, + if ai_config.ai_enabled: + cached_llm_suggestions = get_llm_suggestion_cache( + doc.pk, + backend=ai_config.llm_backend, ) - resp_data = { - "correspondents": [ - c.id for c in match_correspondents(doc, classifier, request.user) - ], - "tags": [t.id for t in match_tags(doc, classifier, request.user)], - "document_types": [ - dt.id for dt in match_document_types(doc, classifier, request.user) - ], - "storage_paths": [ - dt.id for dt in match_storage_paths(doc, classifier, request.user) - ], - "dates": [date.strftime("%Y-%m-%d") for date in dates if date is not None], - } + if cached_llm_suggestions: + refresh_suggestions_cache(doc.pk) + return Response(cached_llm_suggestions.suggestions) - # Cache the suggestions and the classifier hash for later - set_suggestions_cache(doc.pk, resp_data, classifier) + llm_suggestions = get_ai_document_classification(doc, request.user) + + matched_tags = match_tags_by_name( + llm_suggestions.get("tags", []), + request.user, + ) + matched_correspondents = match_correspondents_by_name( + llm_suggestions.get("correspondents", []), + request.user, + ) + matched_types = match_document_types_by_name( + llm_suggestions.get("document_types", []), + request.user, + ) + matched_paths = match_storage_paths_by_name( + llm_suggestions.get("storage_paths", []), + request.user, + ) + + resp_data = { + "title": llm_suggestions.get("title"), + "tags": [t.id for t in matched_tags], + "suggested_tags": extract_unmatched_names( + llm_suggestions.get("tags", []), + matched_tags, + ), + "correspondents": [c.id for c in matched_correspondents], + "suggested_correspondents": extract_unmatched_names( + llm_suggestions.get("correspondents", []), + matched_correspondents, + ), + "document_types": [d.id for d in matched_types], + "suggested_document_types": extract_unmatched_names( + llm_suggestions.get("document_types", []), + matched_types, + ), + "storage_paths": [s.id for s in matched_paths], + "suggested_storage_paths": extract_unmatched_names( + llm_suggestions.get("storage_paths", []), + matched_paths, + ), + "dates": llm_suggestions.get("dates", []), + } + + set_llm_suggestions_cache(doc.pk, resp_data, backend=ai_config.llm_backend) + else: + document_suggestions = get_suggestion_cache(doc.pk) + + if document_suggestions is not None: + refresh_suggestions_cache(doc.pk) + return Response(document_suggestions.suggestions) + + classifier = load_classifier() + + dates = [] + if settings.NUMBER_OF_SUGGESTED_DATES > 0: + gen = parse_date_generator(doc.filename, doc.content) + dates = sorted( + { + i + for i in itertools.islice( + gen, + settings.NUMBER_OF_SUGGESTED_DATES, + ) + }, + ) + + resp_data = { + "correspondents": [ + c.id for c in match_correspondents(doc, classifier, request.user) + ], + "tags": [t.id for t in match_tags(doc, classifier, request.user)], + "document_types": [ + dt.id for dt in match_document_types(doc, classifier, request.user) + ], + "storage_paths": [ + dt.id for dt in match_storage_paths(doc, classifier, request.user) + ], + "dates": [ + date.strftime("%Y-%m-%d") for date in dates if date is not None + ], + } + + # Cache the suggestions and the classifier hash for later + set_suggestions_cache(doc.pk, resp_data, classifier) return Response(resp_data) @@ -1288,6 +1367,59 @@ class DocumentViewSet( ) +class ChatStreamingSerializer(serializers.Serializer): + q = serializers.CharField(required=True) + document_id = serializers.IntegerField(required=False, allow_null=True) + + +@method_decorator( + [ + ensure_csrf_cookie, + cache_control(no_cache=True), + ], + name="dispatch", +) +class ChatStreamingView(GenericAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = ChatStreamingSerializer + + def post(self, request, *args, **kwargs): + request.compress_exempt = True + ai_config = AIConfig() + if not ai_config.ai_enabled: + return HttpResponseBadRequest("AI is required for this feature") + + try: + question = request.data["q"] + except KeyError: + return HttpResponseBadRequest("Invalid request") + + doc_id = request.data.get("document_id") + + if doc_id: + try: + document = Document.objects.get(id=doc_id) + except Document.DoesNotExist: + return HttpResponseBadRequest("Document not found") + + if not has_perms_owner_aware(request.user, "view_document", document): + return HttpResponseForbidden("Insufficient permissions") + + documents = [document] + else: + documents = get_objects_for_user_owner_aware( + request.user, + "view_document", + Document, + ) + + response = StreamingHttpResponse( + stream_chat_with_documents(query_str=question, documents=documents), + content_type="text/event-stream", + ) + return response + + @extend_schema_view( list=extend_schema( description="Document views including search", @@ -2446,6 +2578,10 @@ class UiSettingsView(GenericAPIView): ui_settings["email_enabled"] = settings.EMAIL_ENABLED + ai_config = AIConfig() + + ui_settings["ai_enabled"] = ai_config.ai_enabled + user_resp = { "id": user.id, "username": user.username, @@ -2587,6 +2723,10 @@ class TasksViewSet(ReadOnlyModelViewSet): sanity_check, {"scheduled": False, "raise_on_error": False}, ), + PaperlessTask.TaskName.LLMINDEX_UPDATE: ( + llmindex_index, + {"scheduled": False, "rebuild": False}, + ), } def get_queryset(self): @@ -3106,6 +3246,31 @@ class SystemStatusView(PassUserMixin): last_sanity_check.date_done if last_sanity_check else None ) + ai_config = AIConfig() + if not ai_config.llm_index_enabled: + llmindex_status = "DISABLED" + llmindex_error = None + llmindex_last_modified = None + else: + last_llmindex_update = ( + PaperlessTask.objects.filter( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + .order_by("-date_done") + .first() + ) + llmindex_status = "OK" + llmindex_error = None + if last_llmindex_update is None: + llmindex_status = "WARNING" + llmindex_error = "No LLM index update tasks found" + elif last_llmindex_update and last_llmindex_update.status == states.FAILURE: + llmindex_status = "ERROR" + llmindex_error = last_llmindex_update.result + llmindex_last_modified = ( + last_llmindex_update.date_done if last_llmindex_update else None + ) + return Response( { "pngx_version": current_version, @@ -3143,6 +3308,9 @@ class SystemStatusView(PassUserMixin): "sanity_check_status": sanity_check_status, "sanity_check_last_run": sanity_check_last_run, "sanity_check_error": sanity_check_error, + "llmindex_status": llmindex_status, + "llmindex_last_modified": llmindex_last_modified, + "llmindex_error": llmindex_error, }, }, ) diff --git a/src/paperless/config.py b/src/paperless/config.py index fb3139d79..edebb232f 100644 --- a/src/paperless/config.py +++ b/src/paperless/config.py @@ -169,3 +169,37 @@ class GeneralConfig(BaseConfig): self.app_title = app_config.app_title or None self.app_logo = app_config.app_logo.url if app_config.app_logo else None + + +@dataclasses.dataclass +class AIConfig(BaseConfig): + """ + AI related settings that require global scope + """ + + ai_enabled: bool = dataclasses.field(init=False) + llm_embedding_backend: str = dataclasses.field(init=False) + llm_embedding_model: str = dataclasses.field(init=False) + llm_backend: str = dataclasses.field(init=False) + llm_model: str = dataclasses.field(init=False) + llm_api_key: str = dataclasses.field(init=False) + llm_endpoint: str = dataclasses.field(init=False) + + def __post_init__(self) -> None: + app_config = self._get_config_instance() + + self.ai_enabled = app_config.ai_enabled or settings.AI_ENABLED + self.llm_embedding_backend = ( + app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND + ) + self.llm_embedding_model = ( + app_config.llm_embedding_model or settings.LLM_EMBEDDING_MODEL + ) + self.llm_backend = app_config.llm_backend or settings.LLM_BACKEND + self.llm_model = app_config.llm_model or settings.LLM_MODEL + self.llm_api_key = app_config.llm_api_key or settings.LLM_API_KEY + self.llm_endpoint = app_config.llm_endpoint or settings.LLM_ENDPOINT + + @property + def llm_index_enabled(self) -> bool: + return bool(self.ai_enabled and self.llm_embedding_backend) diff --git a/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py b/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py new file mode 100644 index 000000000..f8a71ea6b --- /dev/null +++ b/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 5.2.6 on 2025-09-30 17:43 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("paperless", "0004_applicationconfiguration_barcode_asn_prefix_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="applicationconfiguration", + name="ai_enabled", + field=models.BooleanField( + default=False, + null=True, + verbose_name="Enables AI features", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_api_key", + field=models.CharField( + blank=True, + max_length=1024, + null=True, + verbose_name="Sets the LLM API key", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_backend", + field=models.CharField( + blank=True, + choices=[("openai", "OpenAI"), ("ollama", "Ollama")], + max_length=128, + null=True, + verbose_name="Sets the LLM backend", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_embedding_backend", + field=models.CharField( + blank=True, + choices=[("openai", "OpenAI"), ("huggingface", "Huggingface")], + max_length=128, + null=True, + verbose_name="Sets the LLM embedding backend", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_embedding_model", + field=models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="Sets the LLM embedding model", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_endpoint", + field=models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="Sets the LLM endpoint, optional", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_model", + field=models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="Sets the LLM model", + ), + ), + ] diff --git a/src/paperless/models.py b/src/paperless/models.py index 1c44f1414..0f727972a 100644 --- a/src/paperless/models.py +++ b/src/paperless/models.py @@ -74,6 +74,20 @@ class ColorConvertChoices(models.TextChoices): CMYK = ("CMYK", _("CMYK")) +class LLMEmbeddingBackend(models.TextChoices): + OPENAI = ("openai", _("OpenAI")) + HUGGINGFACE = ("huggingface", _("Huggingface")) + + +class LLMBackend(models.TextChoices): + """ + Matches to --llm-backend + """ + + OPENAI = ("openai", _("OpenAI")) + OLLAMA = ("ollama", _("Ollama")) + + class ApplicationConfiguration(AbstractSingletonModel): """ Settings which are common across more than 1 parser @@ -265,6 +279,60 @@ class ApplicationConfiguration(AbstractSingletonModel): null=True, ) + """ + AI related settings + """ + + ai_enabled = models.BooleanField( + verbose_name=_("Enables AI features"), + null=True, + default=False, + ) + + llm_embedding_backend = models.CharField( + verbose_name=_("Sets the LLM embedding backend"), + blank=True, + null=True, + max_length=128, + choices=LLMEmbeddingBackend.choices, + ) + + llm_embedding_model = models.CharField( + verbose_name=_("Sets the LLM embedding model"), + blank=True, + null=True, + max_length=128, + ) + + llm_backend = models.CharField( + verbose_name=_("Sets the LLM backend"), + blank=True, + null=True, + max_length=128, + choices=LLMBackend.choices, + ) + + llm_model = models.CharField( + verbose_name=_("Sets the LLM model"), + blank=True, + null=True, + max_length=128, + ) + + llm_api_key = models.CharField( + verbose_name=_("Sets the LLM API key"), + blank=True, + null=True, + max_length=1024, + ) + + llm_endpoint = models.CharField( + verbose_name=_("Sets the LLM endpoint, optional"), + blank=True, + null=True, + max_length=256, + ) + class Meta: verbose_name = _("paperless application settings") diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py index 256f40680..97a2bee7e 100644 --- a/src/paperless/serialisers.py +++ b/src/paperless/serialisers.py @@ -206,6 +206,10 @@ class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer): class ApplicationConfigurationSerializer(serializers.ModelSerializer): user_args = serializers.JSONField(binary=True, allow_null=True) barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True) + llm_api_key = ObfuscatedPasswordField( + required=False, + allow_null=True, + ) def run_validation(self, data): # Empty strings treated as None to avoid unexpected behavior @@ -215,6 +219,11 @@ class ApplicationConfigurationSerializer(serializers.ModelSerializer): data["barcode_tag_mapping"] = None if "language" in data and data["language"] == "": data["language"] = None + if "llm_api_key" in data and data["llm_api_key"] is not None: + if data["llm_api_key"] == "": + data["llm_api_key"] = None + elif len(data["llm_api_key"].replace("*", "")) == 0: + del data["llm_api_key"] return super().run_validation(data) def update(self, instance, validated_data): diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 1cd357f86..9b94ebb7b 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -12,6 +12,7 @@ from typing import Final from urllib.parse import urlparse from celery.schedules import crontab +from compression_middleware.middleware import CompressionMiddleware from dateparser.languages.loader import LocaleDataLoader from django.utils.translation import gettext_lazy as _ from dotenv import load_dotenv @@ -229,6 +230,17 @@ def _parse_beat_schedule() -> dict: "expires": 59.0 * 60.0, }, }, + { + "name": "Rebuild LLM index", + "env_key": "PAPERLESS_LLM_INDEX_TASK_CRON", + # Default daily at 02:10 + "env_default": "10 2 * * *", + "task": "documents.tasks.llmindex_index", + "options": { + # 1 hour before default schedule sends again + "expires": 23.0 * 60.0 * 60.0, + }, + }, ] for task in tasks: # Either get the environment setting or use the default @@ -287,6 +299,7 @@ MODEL_FILE = __get_path( "PAPERLESS_MODEL_FILE", DATA_DIR / "classification_model.pickle", ) +LLM_INDEX_DIR = DATA_DIR / "llm_index" LOGGING_DIR = __get_path("PAPERLESS_LOGGING_DIR", DATA_DIR / "log") @@ -380,6 +393,19 @@ MIDDLEWARE = [ if __get_boolean("PAPERLESS_ENABLE_COMPRESSION", "yes"): # pragma: no cover MIDDLEWARE.insert(0, "compression_middleware.middleware.CompressionMiddleware") +# Workaround to not compress streaming responses (e.g. chat). +# See https://github.com/friedelwolff/django-compression-middleware/pull/7 +original_process_response = CompressionMiddleware.process_response + + +def patched_process_response(self, request, response): + if getattr(request, "compress_exempt", False): + return response + return original_process_response(self, request, response) + + +CompressionMiddleware.process_response = patched_process_response + ROOT_URLCONF = "paperless.urls" @@ -585,6 +611,10 @@ X_FRAME_OPTIONS = "SAMEORIGIN" # The next 3 settings can also be set using just PAPERLESS_URL CSRF_TRUSTED_ORIGINS = __get_list("PAPERLESS_CSRF_TRUSTED_ORIGINS") +if DEBUG: + # Allow access from the angular development server during debugging + CSRF_TRUSTED_ORIGINS.append("http://localhost:4200") + # We allow CORS from localhost:8000 CORS_ALLOWED_ORIGINS = __get_list( "PAPERLESS_CORS_ALLOWED_HOSTS", @@ -595,6 +625,8 @@ if DEBUG: # Allow access from the angular development server during debugging CORS_ALLOWED_ORIGINS.append("http://localhost:4200") +CORS_ALLOW_CREDENTIALS = True + CORS_EXPOSE_HEADERS = [ "Content-Disposition", ] @@ -868,6 +900,7 @@ LOGGING = { "loggers": { "paperless": {"handlers": ["file_paperless"], "level": "DEBUG"}, "paperless_mail": {"handlers": ["file_mail"], "level": "DEBUG"}, + "paperless_ai": {"handlers": ["file_paperless"], "level": "DEBUG"}, "ocrmypdf": {"handlers": ["file_paperless"], "level": "INFO"}, "celery": {"handlers": ["file_celery"], "level": "DEBUG"}, "kombu": {"handlers": ["file_celery"], "level": "DEBUG"}, @@ -1404,3 +1437,16 @@ WEBHOOKS_ALLOW_INTERNAL_REQUESTS = __get_boolean( REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE") REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY") REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT") + +################################################################################ +# AI Settings # +################################################################################ +AI_ENABLED = __get_boolean("PAPERLESS_AI_ENABLED", "NO") +LLM_EMBEDDING_BACKEND = os.getenv( + "PAPERLESS_AI_LLM_EMBEDDING_BACKEND", +) # "huggingface" or "openai" +LLM_EMBEDDING_MODEL = os.getenv("PAPERLESS_AI_LLM_EMBEDDING_MODEL") +LLM_BACKEND = os.getenv("PAPERLESS_AI_LLM_BACKEND") # "ollama" or "openai" +LLM_MODEL = os.getenv("PAPERLESS_AI_LLM_MODEL") +LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY") +LLM_ENDPOINT = os.getenv("PAPERLESS_AI_LLM_ENDPOINT") diff --git a/src/paperless/tests/test_settings.py b/src/paperless/tests/test_settings.py index 10995291e..f09ddcefa 100644 --- a/src/paperless/tests/test_settings.py +++ b/src/paperless/tests/test_settings.py @@ -160,6 +160,7 @@ class TestCeleryScheduleParsing(TestCase): SANITY_EXPIRE_TIME = ((7.0 * 24.0) - 1.0) * 60.0 * 60.0 EMPTY_TRASH_EXPIRE_TIME = 23.0 * 60.0 * 60.0 RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME = 59.0 * 60.0 + LLM_INDEX_EXPIRE_TIME = 23.0 * 60.0 * 60.0 def test_schedule_configuration_default(self): """ @@ -204,6 +205,13 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -256,6 +264,13 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -300,6 +315,13 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -322,6 +344,7 @@ class TestCeleryScheduleParsing(TestCase): "PAPERLESS_INDEX_TASK_CRON": "disable", "PAPERLESS_EMPTY_TRASH_TASK_CRON": "disable", "PAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON": "disable", + "PAPERLESS_LLM_INDEX_TASK_CRON": "disable", }, ): schedule = _parse_beat_schedule() diff --git a/src/paperless/urls.py b/src/paperless/urls.py index e24d1a459..179af14e0 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -18,6 +18,7 @@ from rest_framework.routers import DefaultRouter from documents.views import BulkDownloadView from documents.views import BulkEditObjectsView from documents.views import BulkEditView +from documents.views import ChatStreamingView from documents.views import CorrespondentViewSet from documents.views import CustomFieldViewSet from documents.views import DocumentTypeViewSet @@ -139,6 +140,11 @@ urlpatterns = [ SelectionDataView.as_view(), name="selection_data", ), + re_path( + "^chat/", + ChatStreamingView.as_view(), + name="chat_streaming_view", + ), ], ), ), diff --git a/src/paperless/views.py b/src/paperless/views.py index e79c0e668..f9aa68297 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -35,6 +35,7 @@ from rest_framework.viewsets import ModelViewSet from documents.index import DelayedQuery from documents.permissions import PaperlessObjectPermissions +from documents.tasks import llmindex_index from paperless.filters import GroupFilterSet from paperless.filters import UserFilterSet from paperless.models import ApplicationConfiguration @@ -43,6 +44,7 @@ from paperless.serialisers import GroupSerializer from paperless.serialisers import PaperlessAuthTokenSerializer from paperless.serialisers import ProfileSerializer from paperless.serialisers import UserSerializer +from paperless_ai.indexing import vector_store_file_exists class PaperlessObtainAuthTokenView(ObtainAuthToken): @@ -358,6 +360,30 @@ class ApplicationConfigurationViewSet(ModelViewSet): def create(self, request, *args, **kwargs): return Response(status=405) # Not Allowed + def perform_update(self, serializer): + old_instance = ApplicationConfiguration.objects.all().first() + old_ai_index_enabled = ( + old_instance.ai_enabled and old_instance.llm_embedding_backend + ) + + new_instance: ApplicationConfiguration = serializer.save() + new_ai_index_enabled = ( + new_instance.ai_enabled and new_instance.llm_embedding_backend + ) + + if ( + not old_ai_index_enabled + and new_ai_index_enabled + and not vector_store_file_exists() + ): + # AI index was just enabled and vector store file does not exist + llmindex_index.delay( + progress_bar_disable=True, + rebuild=True, + scheduled=False, + auto=True, + ) + @extend_schema_view( post=extend_schema( diff --git a/src/paperless_ai/__init__.py b/src/paperless_ai/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_ai/ai_classifier.py b/src/paperless_ai/ai_classifier.py new file mode 100644 index 000000000..e60ca37ff --- /dev/null +++ b/src/paperless_ai/ai_classifier.py @@ -0,0 +1,102 @@ +import logging + +from django.contrib.auth.models import User + +from documents.models import Document +from documents.permissions import get_objects_for_user_owner_aware +from paperless.config import AIConfig +from paperless_ai.client import AIClient +from paperless_ai.indexing import query_similar_documents +from paperless_ai.indexing import truncate_content + +logger = logging.getLogger("paperless_ai.rag_classifier") + + +def build_prompt_without_rag(document: Document) -> str: + filename = document.filename or "" + content = truncate_content(document.content[:4000] or "") + + return f""" + You are a document classification assistant. + + Analyze the following document and extract the following information: + - A short descriptive title + - Tags that reflect the content + - Names of people or organizations mentioned + - The type or category of the document + - Suggested folder paths for storing the document + - Up to 3 relevant dates in YYYY-MM-DD format + + Filename: + {filename} + + Content: + {content} + """.strip() + + +def build_prompt_with_rag(document: Document, user: User | None = None) -> str: + base_prompt = build_prompt_without_rag(document) + context = truncate_content(get_context_for_document(document, user)) + + return f"""{base_prompt} + + Additional context from similar documents: + {context} + """.strip() + + +def get_context_for_document( + doc: Document, + user: User | None = None, + max_docs: int = 5, +) -> str: + visible_documents = ( + get_objects_for_user_owner_aware( + user, + "view_document", + Document, + ) + if user + else None + ) + similar_docs = query_similar_documents( + document=doc, + document_ids=[document.pk for document in visible_documents] + if visible_documents + else None, + )[:max_docs] + context_blocks = [] + for similar in similar_docs: + text = similar.content[:1000] or "" + title = similar.title or similar.filename or "Untitled" + context_blocks.append(f"TITLE: {title}\n{text}") + return "\n\n".join(context_blocks) + + +def parse_ai_response(raw: dict) -> dict: + return { + "title": raw.get("title", ""), + "tags": raw.get("tags", []), + "correspondents": raw.get("correspondents", []), + "document_types": raw.get("document_types", []), + "storage_paths": raw.get("storage_paths", []), + "dates": raw.get("dates", []), + } + + +def get_ai_document_classification( + document: Document, + user: User | None = None, +) -> dict: + ai_config = AIConfig() + + prompt = ( + build_prompt_with_rag(document, user) + if ai_config.llm_embedding_backend + else build_prompt_without_rag(document) + ) + + client = AIClient() + result = client.run_llm_query(prompt) + return parse_ai_response(result) diff --git a/src/paperless_ai/base_model.py b/src/paperless_ai/base_model.py new file mode 100644 index 000000000..2924f2c8c --- /dev/null +++ b/src/paperless_ai/base_model.py @@ -0,0 +1,10 @@ +from llama_index.core.bridge.pydantic import BaseModel + + +class DocumentClassifierSchema(BaseModel): + title: str + tags: list[str] + correspondents: list[str] + document_types: list[str] + storage_paths: list[str] + dates: list[str] diff --git a/src/paperless_ai/chat.py b/src/paperless_ai/chat.py new file mode 100644 index 000000000..f662a7bee --- /dev/null +++ b/src/paperless_ai/chat.py @@ -0,0 +1,105 @@ +import logging +import sys + +from llama_index.core import VectorStoreIndex +from llama_index.core.prompts import PromptTemplate +from llama_index.core.query_engine import RetrieverQueryEngine + +from documents.models import Document +from paperless_ai.client import AIClient +from paperless_ai.indexing import load_or_build_index + +logger = logging.getLogger("paperless_ai.chat") + +MAX_SINGLE_DOC_CONTEXT_CHARS = 15000 +SINGLE_DOC_SNIPPET_CHARS = 800 + +CHAT_PROMPT_TMPL = PromptTemplate( + template="""Context information is below. + --------------------- + {context_str} + --------------------- + Given the context information and not prior knowledge, answer the query. + Query: {query_str} + Answer:""", +) + + +def stream_chat_with_documents(query_str: str, documents: list[Document]): + client = AIClient() + index = load_or_build_index() + + doc_ids = [str(doc.pk) for doc in documents] + + # Filter only the node(s) that match the document IDs + nodes = [ + node + for node in index.docstore.docs.values() + if node.metadata.get("document_id") in doc_ids + ] + + if len(nodes) == 0: + logger.warning("No nodes found for the given documents.") + yield "Sorry, I couldn't find any content to answer your question." + return + + local_index = VectorStoreIndex(nodes=nodes) + retriever = local_index.as_retriever( + similarity_top_k=3 if len(documents) == 1 else 5, + ) + + if len(documents) == 1: + # Just one doc — provide full content + doc = documents[0] + # TODO: include document metadata in the context + content = doc.content or "" + context_body = content + + if len(content) > MAX_SINGLE_DOC_CONTEXT_CHARS: + logger.info( + "Truncating single-document context from %s to %s characters", + len(content), + MAX_SINGLE_DOC_CONTEXT_CHARS, + ) + context_body = content[:MAX_SINGLE_DOC_CONTEXT_CHARS] + + top_nodes = retriever.retrieve(query_str) + if len(top_nodes) > 0: + snippets = "\n\n".join( + f"TITLE: {node.metadata.get('title')}\n{node.text[:SINGLE_DOC_SNIPPET_CHARS]}" + for node in top_nodes + ) + context_body = f"{context_body}\n\nTOP MATCHES:\n{snippets}" + + context = f"TITLE: {doc.title or doc.filename}\n{context_body}" + else: + top_nodes = retriever.retrieve(query_str) + + if len(top_nodes) == 0: + logger.warning("Retriever returned no nodes for the given documents.") + yield "Sorry, I couldn't find any content to answer your question." + return + + context = "\n\n".join( + f"TITLE: {node.metadata.get('title')}\n{node.text[:SINGLE_DOC_SNIPPET_CHARS]}" + for node in top_nodes + ) + + prompt = CHAT_PROMPT_TMPL.partial_format( + context_str=context, + query_str=query_str, + ).format(llm=client.llm) + + query_engine = RetrieverQueryEngine.from_args( + retriever=retriever, + llm=client.llm, + streaming=True, + ) + + logger.debug("Document chat prompt: %s", prompt) + + response_stream = query_engine.query(prompt) + + for chunk in response_stream.response_gen: + yield chunk + sys.stdout.flush() diff --git a/src/paperless_ai/client.py b/src/paperless_ai/client.py new file mode 100644 index 000000000..57eedaa75 --- /dev/null +++ b/src/paperless_ai/client.py @@ -0,0 +1,69 @@ +import logging + +from llama_index.core.llms import ChatMessage +from llama_index.core.program.function_program import get_function_tool +from llama_index.llms.ollama import Ollama +from llama_index.llms.openai import OpenAI + +from paperless.config import AIConfig +from paperless_ai.base_model import DocumentClassifierSchema + +logger = logging.getLogger("paperless_ai.client") + + +class AIClient: + """ + A client for interacting with an LLM backend. + """ + + def __init__(self): + self.settings = AIConfig() + self.llm = self.get_llm() + + def get_llm(self) -> Ollama | OpenAI: + if self.settings.llm_backend == "ollama": + return Ollama( + model=self.settings.llm_model or "llama3", + base_url=self.settings.llm_endpoint or "http://localhost:11434", + request_timeout=120, + ) + elif self.settings.llm_backend == "openai": + return OpenAI( + model=self.settings.llm_model or "gpt-3.5-turbo", + api_base=self.settings.llm_endpoint or None, + api_key=self.settings.llm_api_key, + ) + else: + raise ValueError(f"Unsupported LLM backend: {self.settings.llm_backend}") + + def run_llm_query(self, prompt: str) -> str: + logger.debug( + "Running LLM query against %s with model %s", + self.settings.llm_backend, + self.settings.llm_model, + ) + + user_msg = ChatMessage(role="user", content=prompt) + tool = get_function_tool(DocumentClassifierSchema) + result = self.llm.chat_with_tools( + tools=[tool], + user_msg=user_msg, + chat_history=[], + ) + tool_calls = self.llm.get_tool_calls_from_response( + result, + error_on_no_tool_calls=True, + ) + logger.debug("LLM query result: %s", tool_calls) + parsed = DocumentClassifierSchema(**tool_calls[0].tool_kwargs) + return parsed.model_dump() + + def run_chat(self, messages: list[ChatMessage]) -> str: + logger.debug( + "Running chat query against %s with model %s", + self.settings.llm_backend, + self.settings.llm_model, + ) + result = self.llm.chat(messages) + logger.debug("Chat result: %s", result) + return result diff --git a/src/paperless_ai/embedding.py b/src/paperless_ai/embedding.py new file mode 100644 index 000000000..993c9ae30 --- /dev/null +++ b/src/paperless_ai/embedding.py @@ -0,0 +1,92 @@ +import json +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +from django.conf import settings +from llama_index.core.base.embeddings.base import BaseEmbedding +from llama_index.embeddings.huggingface import HuggingFaceEmbedding +from llama_index.embeddings.openai import OpenAIEmbedding + +from documents.models import Document +from documents.models import Note +from paperless.config import AIConfig +from paperless.models import LLMEmbeddingBackend + + +def get_embedding_model() -> BaseEmbedding: + config = AIConfig() + + match config.llm_embedding_backend: + case LLMEmbeddingBackend.OPENAI: + return OpenAIEmbedding( + model=config.llm_embedding_model or "text-embedding-3-small", + api_key=config.llm_api_key, + ) + case LLMEmbeddingBackend.HUGGINGFACE: + return HuggingFaceEmbedding( + model_name=config.llm_embedding_model + or "sentence-transformers/all-MiniLM-L6-v2", + ) + case _: + raise ValueError( + f"Unsupported embedding backend: {config.llm_embedding_backend}", + ) + + +def get_embedding_dim() -> int: + """ + Loads embedding dimension from meta.json if available, otherwise infers it + from a dummy embedding and stores it for future use. + """ + config = AIConfig() + model = config.llm_embedding_model or ( + "text-embedding-3-small" + if config.llm_embedding_backend == "openai" + else "sentence-transformers/all-MiniLM-L6-v2" + ) + + meta_path: Path = settings.LLM_INDEX_DIR / "meta.json" + if meta_path.exists(): + with meta_path.open() as f: + meta = json.load(f) + if meta.get("embedding_model") != model: + raise RuntimeError( + f"Embedding model changed from {meta.get('embedding_model')} to {model}. " + "You must rebuild the index.", + ) + return meta["dim"] + + embedding_model = get_embedding_model() + test_embed = embedding_model.get_text_embedding("test") + dim = len(test_embed) + + with meta_path.open("w") as f: + json.dump({"embedding_model": model, "dim": dim}, f) + + return dim + + +def build_llm_index_text(doc: Document) -> str: + lines = [ + f"Title: {doc.title}", + f"Filename: {doc.filename}", + f"Created: {doc.created}", + f"Added: {doc.added}", + f"Modified: {doc.modified}", + f"Tags: {', '.join(tag.name for tag in doc.tags.all())}", + f"Document Type: {doc.document_type.name if doc.document_type else ''}", + f"Correspondent: {doc.correspondent.name if doc.correspondent else ''}", + f"Storage Path: {doc.storage_path.name if doc.storage_path else ''}", + f"Archive Serial Number: {doc.archive_serial_number or ''}", + f"Notes: {','.join([str(c.note) for c in Note.objects.filter(document=doc)])}", + ] + + for instance in doc.custom_fields.all(): + lines.append(f"Custom Field - {instance.field.name}: {instance}") + + lines.append("\nContent:\n") + lines.append(doc.content or "") + + return "\n".join(lines) diff --git a/src/paperless_ai/indexing.py b/src/paperless_ai/indexing.py new file mode 100644 index 000000000..03c8aa9be --- /dev/null +++ b/src/paperless_ai/indexing.py @@ -0,0 +1,283 @@ +import logging +import shutil +from pathlib import Path + +import faiss +import llama_index.core.settings as llama_settings +import tqdm +from django.conf import settings +from llama_index.core import Document as LlamaDocument +from llama_index.core import StorageContext +from llama_index.core import VectorStoreIndex +from llama_index.core import load_index_from_storage +from llama_index.core.indices.prompt_helper import PromptHelper +from llama_index.core.node_parser import SimpleNodeParser +from llama_index.core.prompts import PromptTemplate +from llama_index.core.retrievers import VectorIndexRetriever +from llama_index.core.schema import BaseNode +from llama_index.core.storage.docstore import SimpleDocumentStore +from llama_index.core.storage.index_store import SimpleIndexStore +from llama_index.core.text_splitter import TokenTextSplitter +from llama_index.vector_stores.faiss import FaissVectorStore + +from documents.models import Document +from paperless_ai.embedding import build_llm_index_text +from paperless_ai.embedding import get_embedding_dim +from paperless_ai.embedding import get_embedding_model + +logger = logging.getLogger("paperless_ai.indexing") + + +def get_or_create_storage_context(*, rebuild=False): + """ + Loads or creates the StorageContext (vector store, docstore, index store). + If rebuild=True, deletes and recreates everything. + """ + if rebuild: + shutil.rmtree(settings.LLM_INDEX_DIR, ignore_errors=True) + settings.LLM_INDEX_DIR.mkdir(parents=True, exist_ok=True) + + if rebuild or not settings.LLM_INDEX_DIR.exists(): + embedding_dim = get_embedding_dim() + faiss_index = faiss.IndexFlatL2(embedding_dim) + vector_store = FaissVectorStore(faiss_index=faiss_index) + docstore = SimpleDocumentStore() + index_store = SimpleIndexStore() + else: + vector_store = FaissVectorStore.from_persist_dir(settings.LLM_INDEX_DIR) + docstore = SimpleDocumentStore.from_persist_dir(settings.LLM_INDEX_DIR) + index_store = SimpleIndexStore.from_persist_dir(settings.LLM_INDEX_DIR) + + return StorageContext.from_defaults( + docstore=docstore, + index_store=index_store, + vector_store=vector_store, + persist_dir=settings.LLM_INDEX_DIR, + ) + + +def build_document_node(document: Document) -> list[BaseNode]: + """ + Given a Document, returns parsed Nodes ready for indexing. + """ + text = build_llm_index_text(document) + metadata = { + "document_id": str(document.id), + "title": document.title, + "tags": [t.name for t in document.tags.all()], + "correspondent": document.correspondent.name + if document.correspondent + else None, + "document_type": document.document_type.name + if document.document_type + else None, + "created": document.created.isoformat() if document.created else None, + "added": document.added.isoformat() if document.added else None, + "modified": document.modified.isoformat(), + } + doc = LlamaDocument(text=text, metadata=metadata) + parser = SimpleNodeParser() + return parser.get_nodes_from_documents([doc]) + + +def load_or_build_index(nodes=None): + """ + Load an existing VectorStoreIndex if present, + or build a new one using provided nodes if storage is empty. + """ + embed_model = get_embedding_model() + llama_settings.Settings.embed_model = embed_model + storage_context = get_or_create_storage_context() + try: + return load_index_from_storage(storage_context=storage_context) + except ValueError as e: + logger.warning("Failed to load index from storage: %s", e) + if not nodes: + logger.info("No nodes provided for index creation.") + raise + return VectorStoreIndex( + nodes=nodes, + storage_context=storage_context, + embed_model=embed_model, + ) + + +def remove_document_docstore_nodes(document: Document, index: VectorStoreIndex): + """ + Removes existing documents from docstore for a given document from the index. + This is necessary because FAISS IndexFlatL2 is append-only. + """ + all_node_ids = list(index.docstore.docs.keys()) + existing_nodes = [ + node.node_id + for node in index.docstore.get_nodes(all_node_ids) + if node.metadata.get("document_id") == str(document.id) + ] + for node_id in existing_nodes: + # Delete from docstore, FAISS IndexFlatL2 are append-only + index.docstore.delete_document(node_id) + + +def vector_store_file_exists(): + """ + Check if the vector store file exists in the LLM index directory. + """ + return Path(settings.LLM_INDEX_DIR / "default__vector_store.json").exists() + + +def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str: + """ + Rebuild or update the LLM index. + """ + nodes = [] + + documents = Document.objects.all() + if not documents.exists(): + msg = "No documents found to index." + logger.warning(msg) + return msg + + if rebuild or not vector_store_file_exists(): + # remove meta.json to force re-detection of embedding dim + (settings.LLM_INDEX_DIR / "meta.json").unlink(missing_ok=True) + # Rebuild index from scratch + logger.info("Rebuilding LLM index.") + embed_model = get_embedding_model() + llama_settings.Settings.embed_model = embed_model + storage_context = get_or_create_storage_context(rebuild=True) + for document in tqdm.tqdm(documents, disable=progress_bar_disable): + document_nodes = build_document_node(document) + nodes.extend(document_nodes) + + index = VectorStoreIndex( + nodes=nodes, + storage_context=storage_context, + embed_model=embed_model, + show_progress=not progress_bar_disable, + ) + msg = "LLM index rebuilt successfully." + else: + # Update existing index + index = load_or_build_index() + all_node_ids = list(index.docstore.docs.keys()) + existing_nodes = { + node.metadata.get("document_id"): node + for node in index.docstore.get_nodes(all_node_ids) + } + + for document in tqdm.tqdm(documents, disable=progress_bar_disable): + doc_id = str(document.id) + document_modified = document.modified.isoformat() + + if doc_id in existing_nodes: + node = existing_nodes[doc_id] + node_modified = node.metadata.get("modified") + + if node_modified == document_modified: + continue + + # Again, delete from docstore, FAISS IndexFlatL2 are append-only + index.docstore.delete_document(node.node_id) + nodes.extend(build_document_node(document)) + else: + # New document, add it + nodes.extend(build_document_node(document)) + + if nodes: + msg = "LLM index updated successfully." + logger.info( + "Updating %d nodes in LLM index.", + len(nodes), + ) + index.insert_nodes(nodes) + else: + msg = "No changes detected in LLM index." + logger.info(msg) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + return msg + + +def llm_index_add_or_update_document(document: Document): + """ + Adds or updates a document in the LLM index. + If the document already exists, it will be replaced. + """ + new_nodes = build_document_node(document) + + index = load_or_build_index(nodes=new_nodes) + + remove_document_docstore_nodes(document, index) + + index.insert_nodes(new_nodes) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + + +def llm_index_remove_document(document: Document): + """ + Removes a document from the LLM index. + """ + index = load_or_build_index() + + remove_document_docstore_nodes(document, index) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + + +def truncate_content(content: str) -> str: + prompt_helper = PromptHelper( + context_window=8192, + num_output=512, + chunk_overlap_ratio=0.1, + chunk_size_limit=None, + ) + splitter = TokenTextSplitter(separator=" ", chunk_size=512, chunk_overlap=50) + content_chunks = splitter.split_text(content) + truncated_chunks = prompt_helper.truncate( + prompt=PromptTemplate(template="{content}"), + text_chunks=content_chunks, + padding=5, + ) + return " ".join(truncated_chunks) + + +def query_similar_documents( + document: Document, + top_k: int = 5, + document_ids: list[int] | None = None, +) -> list[Document]: + """ + Runs a similarity query and returns top-k similar Document objects. + """ + index = load_or_build_index() + + # constrain only the node(s) that match the document IDs, if given + doc_node_ids = ( + [ + node.node_id + for node in index.docstore.docs.values() + if node.metadata.get("document_id") in document_ids + ] + if document_ids + else None + ) + + retriever = VectorIndexRetriever( + index=index, + similarity_top_k=top_k, + doc_ids=doc_node_ids, + ) + + query_text = truncate_content( + (document.title or "") + "\n" + (document.content or ""), + ) + results = retriever.retrieve(query_text) + + document_ids = [ + int(node.metadata["document_id"]) + for node in results + if "document_id" in node.metadata + ] + + return list(Document.objects.filter(pk__in=document_ids)) diff --git a/src/paperless_ai/matching.py b/src/paperless_ai/matching.py new file mode 100644 index 000000000..f1dfc62db --- /dev/null +++ b/src/paperless_ai/matching.py @@ -0,0 +1,102 @@ +import difflib +import logging +import re + +from django.contrib.auth.models import User + +from documents.models import Correspondent +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from documents.permissions import get_objects_for_user_owner_aware + +MATCH_THRESHOLD = 0.8 + +logger = logging.getLogger("paperless_ai.matching") + + +def match_tags_by_name(names: list[str], user: User) -> list[Tag]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_tag"], + Tag, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_correspondents_by_name(names: list[str], user: User) -> list[Correspondent]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_correspondent"], + Correspondent, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_document_types_by_name(names: list[str], user: User) -> list[DocumentType]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_documenttype"], + DocumentType, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_storage_paths_by_name(names: list[str], user: User) -> list[StoragePath]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_storagepath"], + StoragePath, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def _normalize(s: str) -> str: + s = s.lower() + s = re.sub(r"[^\w\s]", "", s) # remove punctuation + s = s.strip() + return s + + +def _match_names_to_queryset(names: list[str], queryset, attr: str): + results = [] + objects = list(queryset) + object_names = [_normalize(getattr(obj, attr)) for obj in objects] + + for name in names: + if not name: + continue + target = _normalize(name) + + # First try exact match + if target in object_names: + index = object_names.index(target) + matched = objects.pop(index) + object_names.pop(index) # keep object list aligned after removal + results.append(matched) + continue + + # Fuzzy match fallback + matches = difflib.get_close_matches( + target, + object_names, + n=1, + cutoff=MATCH_THRESHOLD, + ) + if matches: + index = object_names.index(matches[0]) + matched = objects.pop(index) + object_names.pop(index) + results.append(matched) + else: + pass + return results + + +def extract_unmatched_names( + names: list[str], + matched_objects: list, + attr="name", +) -> list[str]: + matched_names = {getattr(obj, attr).lower() for obj in matched_objects} + return [name for name in names if name.lower() not in matched_names] diff --git a/src/paperless_ai/tests/__init__.py b/src/paperless_ai/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_ai/tests/test_ai_classifier.py b/src/paperless_ai/tests/test_ai_classifier.py new file mode 100644 index 000000000..115d51cd4 --- /dev/null +++ b/src/paperless_ai/tests/test_ai_classifier.py @@ -0,0 +1,186 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from django.test import override_settings + +from documents.models import Document +from paperless_ai.ai_classifier import build_prompt_with_rag +from paperless_ai.ai_classifier import build_prompt_without_rag +from paperless_ai.ai_classifier import get_ai_document_classification +from paperless_ai.ai_classifier import get_context_for_document + + +@pytest.fixture +def mock_document(): + doc = MagicMock(spec=Document) + doc.title = "Test Title" + doc.filename = "test_file.pdf" + doc.created = "2023-01-01" + doc.added = "2023-01-02" + doc.modified = "2023-01-03" + + tag1 = MagicMock() + tag1.name = "Tag1" + tag2 = MagicMock() + tag2.name = "Tag2" + doc.tags.all = MagicMock(return_value=[tag1, tag2]) + + doc.document_type = MagicMock() + doc.document_type.name = "Invoice" + doc.correspondent = MagicMock() + doc.correspondent.name = "Test Correspondent" + doc.archive_serial_number = "12345" + doc.content = "This is the document content." + + cf1 = MagicMock(__str__=lambda x: "Value1") + cf1.field = MagicMock() + cf1.field.name = "Field1" + cf1.value = "Value1" + cf2 = MagicMock(__str__=lambda x: "Value2") + cf2.field = MagicMock() + cf2.field.name = "Field2" + cf2.value = "Value2" + doc.custom_fields.all = MagicMock(return_value=[cf1, cf2]) + + return doc + + +@pytest.fixture +def mock_similar_documents(): + doc1 = MagicMock() + doc1.content = "Content of document 1" + doc1.title = "Title 1" + doc1.filename = "file1.txt" + + doc2 = MagicMock() + doc2.content = "Content of document 2" + doc2.title = None + doc2.filename = "file2.txt" + + doc3 = MagicMock() + doc3.content = None + doc3.title = None + doc3.filename = None + + return [doc1, doc2, doc3] + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@override_settings( + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_get_ai_document_classification_success(mock_run_llm_query, mock_document): + mock_run_llm_query.return_value = { + "title": "Test Title", + "tags": ["test", "document"], + "correspondents": ["John Doe"], + "document_types": ["report"], + "storage_paths": ["Reports"], + "dates": ["2023-01-01"], + } + + result = get_ai_document_classification(mock_document) + + assert result["title"] == "Test Title" + assert result["tags"] == ["test", "document"] + assert result["correspondents"] == ["John Doe"] + assert result["document_types"] == ["report"] + assert result["storage_paths"] == ["Reports"] + assert result["dates"] == ["2023-01-01"] + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +def test_get_ai_document_classification_failure(mock_run_llm_query, mock_document): + mock_run_llm_query.side_effect = Exception("LLM query failed") + + # assert raises an exception + with pytest.raises(Exception): + get_ai_document_classification(mock_document) + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@patch("paperless_ai.ai_classifier.build_prompt_with_rag") +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_EMBEDDING_MODEL="some_model", + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_use_rag_if_configured( + mock_build_prompt_with_rag, + mock_run_llm_query, + mock_document, +): + mock_build_prompt_with_rag.return_value = "Prompt with RAG" + mock_run_llm_query.return_value.text = json.dumps({}) + get_ai_document_classification(mock_document) + mock_build_prompt_with_rag.assert_called_once() + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@patch("paperless_ai.ai_classifier.build_prompt_without_rag") +@patch("paperless.config.AIConfig") +@override_settings( + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_use_without_rag_if_not_configured( + mock_ai_config, + mock_build_prompt_without_rag, + mock_run_llm_query, + mock_document, +): + mock_ai_config.llm_embedding_backend = None + mock_build_prompt_without_rag.return_value = "Prompt without RAG" + mock_run_llm_query.return_value.text = json.dumps({}) + get_ai_document_classification(mock_document) + mock_build_prompt_without_rag.assert_called_once() + + +@pytest.mark.django_db +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_prompt_with_without_rag(mock_document): + with patch( + "paperless_ai.ai_classifier.get_context_for_document", + return_value="Context from similar documents", + ): + prompt = build_prompt_without_rag(mock_document) + assert "Additional context from similar documents:" not in prompt + + prompt = build_prompt_with_rag(mock_document) + assert "Additional context from similar documents:" in prompt + + +@patch("paperless_ai.ai_classifier.query_similar_documents") +def test_get_context_for_document( + mock_query_similar_documents, + mock_document, + mock_similar_documents, +): + mock_query_similar_documents.return_value = mock_similar_documents + + result = get_context_for_document(mock_document, max_docs=2) + + expected_result = ( + "TITLE: Title 1\nContent of document 1\n\n" + "TITLE: file2.txt\nContent of document 2" + ) + assert result == expected_result + mock_query_similar_documents.assert_called_once() + + +def test_get_context_for_document_no_similar_docs(mock_document): + with patch("paperless_ai.ai_classifier.query_similar_documents", return_value=[]): + result = get_context_for_document(mock_document) + assert result == "" diff --git a/src/paperless_ai/tests/test_ai_indexing.py b/src/paperless_ai/tests/test_ai_indexing.py new file mode 100644 index 000000000..bd217fb89 --- /dev/null +++ b/src/paperless_ai/tests/test_ai_indexing.py @@ -0,0 +1,334 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from django.test import override_settings +from django.utils import timezone +from llama_index.core.base.embeddings.base import BaseEmbedding + +from documents.models import Document +from paperless_ai import indexing + + +@pytest.fixture +def temp_llm_index_dir(tmp_path): + original_dir = indexing.settings.LLM_INDEX_DIR + indexing.settings.LLM_INDEX_DIR = tmp_path + yield tmp_path + indexing.settings.LLM_INDEX_DIR = original_dir + + +@pytest.fixture +def real_document(db): + return Document.objects.create( + title="Test Document", + content="This is some test content.", + added=timezone.now(), + ) + + +@pytest.fixture +def mock_embed_model(): + fake = FakeEmbedding() + with ( + patch("paperless_ai.indexing.get_embedding_model") as mock_index, + patch( + "paperless_ai.embedding.get_embedding_model", + ) as mock_embedding, + ): + mock_index.return_value = fake + mock_embedding.return_value = fake + yield mock_index + + +class FakeEmbedding(BaseEmbedding): + # TODO: maybe a better way to do this? + def _aget_query_embedding(self, query: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def _get_query_embedding(self, query: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def _get_text_embedding(self, text: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def get_query_embedding_dim(self) -> int: + return 384 # Match your real FAISS config + + +@pytest.mark.django_db +def test_build_document_node(real_document): + nodes = indexing.build_document_node(real_document) + assert len(nodes) > 0 + assert nodes[0].metadata["document_id"] == str(real_document.id) + + +@pytest.mark.django_db +def test_update_llm_index( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document]) + mock_all.return_value = mock_queryset + indexing.update_llm_index(rebuild=True) + + assert any(temp_llm_index_dir.glob("*.json")) + + +@pytest.mark.django_db +def test_update_llm_index_removes_meta( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + # Pre-create a meta.json with incorrect data + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "old", "dim": 1}), + ) + + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document]) + mock_all.return_value = mock_queryset + indexing.update_llm_index(rebuild=True) + + meta = json.loads((temp_llm_index_dir / "meta.json").read_text()) + from paperless.config import AIConfig + + config = AIConfig() + expected_model = config.llm_embedding_model or ( + "text-embedding-3-small" + if config.llm_embedding_backend == "openai" + else "sentence-transformers/all-MiniLM-L6-v2" + ) + assert meta == {"embedding_model": expected_model, "dim": 384} + + +@pytest.mark.django_db +def test_update_llm_index_partial_update( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + doc2 = Document.objects.create( + title="Test Document 2", + content="This is some test content 2.", + added=timezone.now(), + checksum="1234567890abcdef", + ) + # Initial index + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document, doc2]) + mock_all.return_value = mock_queryset + + indexing.update_llm_index(rebuild=True) + + # modify document + updated_document = real_document + updated_document.modified = timezone.now() # simulate modification + + # new doc + doc3 = Document.objects.create( + title="Test Document 3", + content="This is some test content 3.", + added=timezone.now(), + checksum="abcdef1234567890", + ) + + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3]) + mock_all.return_value = mock_queryset + + # assert logs "Updating LLM index with %d new nodes and removing %d old nodes." + with patch("paperless_ai.indexing.logger") as mock_logger: + indexing.update_llm_index(rebuild=False) + mock_logger.info.assert_called_once_with( + "Updating %d nodes in LLM index.", + 2, + ) + indexing.update_llm_index(rebuild=False) + + assert any(temp_llm_index_dir.glob("*.json")) + + +def test_get_or_create_storage_context_raises_exception( + temp_llm_index_dir, + mock_embed_model, +): + with pytest.raises(Exception): + indexing.get_or_create_storage_context(rebuild=False) + + +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", +) +def test_load_or_build_index_builds_when_nodes_given( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.VectorStoreIndex", + return_value=MagicMock(), + ) as mock_index_cls, + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ) as mock_storage, + ): + mock_storage.return_value.persist_dir = temp_llm_index_dir + indexing.load_or_build_index( + nodes=[indexing.build_document_node(real_document)], + ) + mock_index_cls.assert_called_once() + + +def test_load_or_build_index_raises_exception_when_no_nodes( + temp_llm_index_dir, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ), + ): + with pytest.raises(Exception): + indexing.load_or_build_index() + + +@pytest.mark.django_db +def test_load_or_build_index_succeeds_when_nodes_given( + temp_llm_index_dir, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.VectorStoreIndex", + return_value=MagicMock(), + ) as mock_index_cls, + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ) as mock_storage, + ): + mock_storage.return_value.persist_dir = temp_llm_index_dir + indexing.load_or_build_index( + nodes=[MagicMock()], + ) + mock_index_cls.assert_called_once() + + +@pytest.mark.django_db +def test_add_or_update_document_updates_existing_entry( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + indexing.update_llm_index(rebuild=True) + indexing.llm_index_add_or_update_document(real_document) + + assert any(temp_llm_index_dir.glob("*.json")) + + +@pytest.mark.django_db +def test_remove_document_deletes_node_from_docstore( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + indexing.update_llm_index(rebuild=True) + index = indexing.load_or_build_index() + assert len(index.docstore.docs) == 1 + + indexing.llm_index_remove_document(real_document) + index = indexing.load_or_build_index() + assert len(index.docstore.docs) == 0 + + +@pytest.mark.django_db +def test_update_llm_index_no_documents( + temp_llm_index_dir, + mock_embed_model, +): + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = False + mock_queryset.__iter__.return_value = iter([]) + mock_all.return_value = mock_queryset + + # check log message + with patch("paperless_ai.indexing.logger") as mock_logger: + indexing.update_llm_index(rebuild=True) + mock_logger.warning.assert_called_once_with( + "No documents found to index.", + ) + + +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_BACKEND="ollama", +) +def test_query_similar_documents( + temp_llm_index_dir, + real_document, +): + with ( + patch("paperless_ai.indexing.get_or_create_storage_context") as mock_storage, + patch("paperless_ai.indexing.load_or_build_index") as mock_load_or_build_index, + patch("paperless_ai.indexing.VectorIndexRetriever") as mock_retriever_cls, + patch("paperless_ai.indexing.Document.objects.filter") as mock_filter, + ): + mock_storage.return_value = MagicMock() + mock_storage.return_value.persist_dir = temp_llm_index_dir + + mock_index = MagicMock() + mock_load_or_build_index.return_value = mock_index + + mock_retriever = MagicMock() + mock_retriever_cls.return_value = mock_retriever + + mock_node1 = MagicMock() + mock_node1.metadata = {"document_id": 1} + + mock_node2 = MagicMock() + mock_node2.metadata = {"document_id": 2} + + mock_retriever.retrieve.return_value = [mock_node1, mock_node2] + + mock_filtered_docs = [MagicMock(pk=1), MagicMock(pk=2)] + mock_filter.return_value = mock_filtered_docs + + result = indexing.query_similar_documents(real_document, top_k=3) + + mock_load_or_build_index.assert_called_once() + mock_retriever_cls.assert_called_once() + mock_retriever.retrieve.assert_called_once_with( + "Test Document\nThis is some test content.", + ) + mock_filter.assert_called_once_with(pk__in=[1, 2]) + + assert result == mock_filtered_docs diff --git a/src/paperless_ai/tests/test_chat.py b/src/paperless_ai/tests/test_chat.py new file mode 100644 index 000000000..c91488cf1 --- /dev/null +++ b/src/paperless_ai/tests/test_chat.py @@ -0,0 +1,142 @@ +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from llama_index.core import VectorStoreIndex +from llama_index.core.schema import TextNode + +from paperless_ai.chat import stream_chat_with_documents + + +@pytest.fixture(autouse=True) +def patch_embed_model(): + from llama_index.core import settings as llama_settings + + mock_embed_model = MagicMock() + mock_embed_model._get_text_embedding_batch.return_value = [ + [0.1] * 1536, + ] # 1 vector per input + llama_settings.Settings._embed_model = mock_embed_model + yield + llama_settings.Settings._embed_model = None + + +@pytest.fixture(autouse=True) +def patch_embed_nodes(): + with patch( + "llama_index.core.indices.vector_store.base.embed_nodes", + ) as mock_embed_nodes: + mock_embed_nodes.side_effect = lambda nodes, *_args, **_kwargs: { + node.node_id: [0.1] * 1536 for node in nodes + } + yield + + +@pytest.fixture +def mock_document(): + doc = MagicMock() + doc.pk = 1 + doc.title = "Test Document" + doc.filename = "test_file.pdf" + doc.content = "This is the document content." + return doc + + +def test_stream_chat_with_one_document_full_content(mock_document): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + patch( + "paperless_ai.chat.RetrieverQueryEngine.from_args", + ) as mock_query_engine_cls, + ): + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + mock_node = TextNode( + text="This is node content.", + metadata={"document_id": str(mock_document.pk), "title": "Test Document"}, + ) + mock_index = MagicMock() + mock_index.docstore.docs.values.return_value = [mock_node] + mock_load_index.return_value = mock_index + + mock_response_stream = MagicMock() + mock_response_stream.response_gen = iter(["chunk1", "chunk2"]) + mock_query_engine = MagicMock() + mock_query_engine_cls.return_value = mock_query_engine + mock_query_engine.query.return_value = mock_response_stream + + output = list(stream_chat_with_documents("What is this?", [mock_document])) + + assert output == ["chunk1", "chunk2"] + + +def test_stream_chat_with_multiple_documents_retrieval(patch_embed_nodes): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + patch( + "paperless_ai.chat.RetrieverQueryEngine.from_args", + ) as mock_query_engine_cls, + patch.object(VectorStoreIndex, "as_retriever") as mock_as_retriever, + ): + # Mock AIClient and LLM + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + # Create two real TextNodes + mock_node1 = TextNode( + text="Content for doc 1.", + metadata={"document_id": "1", "title": "Document 1"}, + ) + mock_node2 = TextNode( + text="Content for doc 2.", + metadata={"document_id": "2", "title": "Document 2"}, + ) + mock_index = MagicMock() + mock_index.docstore.docs.values.return_value = [mock_node1, mock_node2] + mock_load_index.return_value = mock_index + + # Patch as_retriever to return a retriever whose retrieve() returns mock_node1 and mock_node2 + mock_retriever = MagicMock() + mock_retriever.retrieve.return_value = [mock_node1, mock_node2] + mock_as_retriever.return_value = mock_retriever + + # Mock response stream + mock_response_stream = MagicMock() + mock_response_stream.response_gen = iter(["chunk1", "chunk2"]) + + # Mock RetrieverQueryEngine + mock_query_engine = MagicMock() + mock_query_engine_cls.return_value = mock_query_engine + mock_query_engine.query.return_value = mock_response_stream + + # Fake documents + doc1 = MagicMock(pk=1) + doc2 = MagicMock(pk=2) + + output = list(stream_chat_with_documents("What's up?", [doc1, doc2])) + + assert output == ["chunk1", "chunk2"] + + +def test_stream_chat_no_matching_nodes(): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + ): + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + mock_index = MagicMock() + # No matching nodes + mock_index.docstore.docs.values.return_value = [] + mock_load_index.return_value = mock_index + + output = list(stream_chat_with_documents("Any info?", [MagicMock(pk=1)])) + + assert output == ["Sorry, I couldn't find any content to answer your question."] diff --git a/src/paperless_ai/tests/test_client.py b/src/paperless_ai/tests/test_client.py new file mode 100644 index 000000000..47053ab20 --- /dev/null +++ b/src/paperless_ai/tests/test_client.py @@ -0,0 +1,111 @@ +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from llama_index.core.llms import ChatMessage +from llama_index.core.llms.llm import ToolSelection + +from paperless_ai.client import AIClient + + +@pytest.fixture +def mock_ai_config(): + with patch("paperless_ai.client.AIConfig") as MockAIConfig: + mock_config = MagicMock() + MockAIConfig.return_value = mock_config + yield mock_config + + +@pytest.fixture +def mock_ollama_llm(): + with patch("paperless_ai.client.Ollama") as MockOllama: + yield MockOllama + + +@pytest.fixture +def mock_openai_llm(): + with patch("paperless_ai.client.OpenAI") as MockOpenAI: + yield MockOpenAI + + +def test_get_llm_ollama(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + client = AIClient() + + mock_ollama_llm.assert_called_once_with( + model="test_model", + base_url="http://test-url", + request_timeout=120, + ) + assert client.llm == mock_ollama_llm.return_value + + +def test_get_llm_openai(mock_ai_config, mock_openai_llm): + mock_ai_config.llm_backend = "openai" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_api_key = "test_api_key" + mock_ai_config.llm_endpoint = "http://test-url" + + client = AIClient() + + mock_openai_llm.assert_called_once_with( + model="test_model", + api_base="http://test-url", + api_key="test_api_key", + ) + assert client.llm == mock_openai_llm.return_value + + +def test_get_llm_unsupported_backend(mock_ai_config): + mock_ai_config.llm_backend = "unsupported" + + with pytest.raises(ValueError, match="Unsupported LLM backend: unsupported"): + AIClient() + + +def test_run_llm_query(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + mock_llm_instance = mock_ollama_llm.return_value + + tool_selection = ToolSelection( + tool_id="call_test", + tool_name="DocumentClassifierSchema", + tool_kwargs={ + "title": "Test Title", + "tags": ["test", "document"], + "correspondents": ["John Doe"], + "document_types": ["report"], + "storage_paths": ["Reports"], + "dates": ["2023-01-01"], + }, + ) + + mock_llm_instance.chat_with_tools.return_value = MagicMock() + mock_llm_instance.get_tool_calls_from_response.return_value = [tool_selection] + + client = AIClient() + result = client.run_llm_query("test_prompt") + + assert result["title"] == "Test Title" + + +def test_run_chat(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + mock_llm_instance = mock_ollama_llm.return_value + mock_llm_instance.chat.return_value = "test_chat_result" + + client = AIClient() + messages = [ChatMessage(role="user", content="Hello")] + result = client.run_chat(messages) + + mock_llm_instance.chat.assert_called_once_with(messages) + assert result == "test_chat_result" diff --git a/src/paperless_ai/tests/test_embedding.py b/src/paperless_ai/tests/test_embedding.py new file mode 100644 index 000000000..9430205fa --- /dev/null +++ b/src/paperless_ai/tests/test_embedding.py @@ -0,0 +1,169 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from django.conf import settings + +from documents.models import Document +from paperless.models import LLMEmbeddingBackend +from paperless_ai.embedding import build_llm_index_text +from paperless_ai.embedding import get_embedding_dim +from paperless_ai.embedding import get_embedding_model + + +@pytest.fixture +def mock_ai_config(): + with patch("paperless_ai.embedding.AIConfig") as MockAIConfig: + yield MockAIConfig + + +@pytest.fixture +def temp_llm_index_dir(tmp_path): + original_dir = settings.LLM_INDEX_DIR + settings.LLM_INDEX_DIR = tmp_path + yield tmp_path + settings.LLM_INDEX_DIR = original_dir + + +@pytest.fixture +def mock_document(): + doc = MagicMock(spec=Document) + doc.title = "Test Title" + doc.filename = "test_file.pdf" + doc.created = "2023-01-01" + doc.added = "2023-01-02" + doc.modified = "2023-01-03" + + tag1 = MagicMock() + tag1.name = "Tag1" + tag2 = MagicMock() + tag2.name = "Tag2" + doc.tags.all = MagicMock(return_value=[tag1, tag2]) + + doc.document_type = MagicMock() + doc.document_type.name = "Invoice" + doc.correspondent = MagicMock() + doc.correspondent.name = "Test Correspondent" + doc.archive_serial_number = "12345" + doc.content = "This is the document content." + + cf1 = MagicMock(__str__=lambda x: "Value1") + cf1.field = MagicMock() + cf1.field.name = "Field1" + cf1.value = "Value1" + cf2 = MagicMock(__str__=lambda x: "Value2") + cf2.field = MagicMock() + cf2.field.name = "Field2" + cf2.value = "Value2" + doc.custom_fields.all = MagicMock(return_value=[cf1, cf2]) + + return doc + + +def test_get_embedding_model_openai(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.OPENAI + mock_ai_config.return_value.llm_embedding_model = "text-embedding-3-small" + mock_ai_config.return_value.llm_api_key = "test_api_key" + + with patch("paperless_ai.embedding.OpenAIEmbedding") as MockOpenAIEmbedding: + model = get_embedding_model() + MockOpenAIEmbedding.assert_called_once_with( + model="text-embedding-3-small", + api_key="test_api_key", + ) + assert model == MockOpenAIEmbedding.return_value + + +def test_get_embedding_model_huggingface(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.HUGGINGFACE + mock_ai_config.return_value.llm_embedding_model = ( + "sentence-transformers/all-MiniLM-L6-v2" + ) + + with patch( + "paperless_ai.embedding.HuggingFaceEmbedding", + ) as MockHuggingFaceEmbedding: + model = get_embedding_model() + MockHuggingFaceEmbedding.assert_called_once_with( + model_name="sentence-transformers/all-MiniLM-L6-v2", + ) + assert model == MockHuggingFaceEmbedding.return_value + + +def test_get_embedding_model_invalid_backend(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "INVALID_BACKEND" + + with pytest.raises( + ValueError, + match="Unsupported embedding backend: INVALID_BACKEND", + ): + get_embedding_model() + + +def test_get_embedding_dim_infers_and_saves(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + class DummyEmbedding: + def get_text_embedding(self, text): + return [0.0] * 7 + + with patch( + "paperless_ai.embedding.get_embedding_model", + return_value=DummyEmbedding(), + ) as mock_get: + dim = get_embedding_dim() + mock_get.assert_called_once() + + assert dim == 7 + meta = json.loads((temp_llm_index_dir / "meta.json").read_text()) + assert meta == {"embedding_model": "text-embedding-3-small", "dim": 7} + + +def test_get_embedding_dim_reads_existing_meta(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "text-embedding-3-small", "dim": 11}), + ) + + with patch("paperless_ai.embedding.get_embedding_model") as mock_get: + assert get_embedding_dim() == 11 + mock_get.assert_not_called() + + +def test_get_embedding_dim_raises_on_model_change(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "old", "dim": 11}), + ) + + with pytest.raises( + RuntimeError, + match="Embedding model changed from old to text-embedding-3-small", + ): + get_embedding_dim() + + +def test_build_llm_index_text(mock_document): + with patch("documents.models.Note.objects.filter") as mock_notes_filter: + mock_notes_filter.return_value = [ + MagicMock(note="Note1"), + MagicMock(note="Note2"), + ] + + result = build_llm_index_text(mock_document) + + assert "Title: Test Title" in result + assert "Filename: test_file.pdf" in result + assert "Created: 2023-01-01" in result + assert "Tags: Tag1, Tag2" in result + assert "Document Type: Invoice" in result + assert "Correspondent: Test Correspondent" in result + assert "Notes: Note1,Note2" in result + assert "Content:\n\nThis is the document content." in result + assert "Custom Field - Field1: Value1\nCustom Field - Field2: Value2" in result diff --git a/src/paperless_ai/tests/test_matching.py b/src/paperless_ai/tests/test_matching.py new file mode 100644 index 000000000..87a42a1a4 --- /dev/null +++ b/src/paperless_ai/tests/test_matching.py @@ -0,0 +1,86 @@ +from unittest.mock import patch + +from django.test import TestCase + +from documents.models import Correspondent +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from paperless_ai.matching import extract_unmatched_names +from paperless_ai.matching import match_correspondents_by_name +from paperless_ai.matching import match_document_types_by_name +from paperless_ai.matching import match_storage_paths_by_name +from paperless_ai.matching import match_tags_by_name + + +class TestAIMatching(TestCase): + def setUp(self): + # Create test data for Tag + self.tag1 = Tag.objects.create(name="Test Tag 1") + self.tag2 = Tag.objects.create(name="Test Tag 2") + + # Create test data for Correspondent + self.correspondent1 = Correspondent.objects.create(name="Test Correspondent 1") + self.correspondent2 = Correspondent.objects.create(name="Test Correspondent 2") + + # Create test data for DocumentType + self.document_type1 = DocumentType.objects.create(name="Test Document Type 1") + self.document_type2 = DocumentType.objects.create(name="Test Document Type 2") + + # Create test data for StoragePath + self.storage_path1 = StoragePath.objects.create(name="Test Storage Path 1") + self.storage_path2 = StoragePath.objects.create(name="Test Storage Path 2") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_by_name(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = ["Test Tag 1", "Nonexistent Tag"] + result = match_tags_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Tag 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_correspondents_by_name(self, mock_get_objects): + mock_get_objects.return_value = Correspondent.objects.all() + names = ["Test Correspondent 1", "Nonexistent Correspondent"] + result = match_correspondents_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Correspondent 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_document_types_by_name(self, mock_get_objects): + mock_get_objects.return_value = DocumentType.objects.all() + names = ["Test Document Type 1", "Nonexistent Document Type"] + result = match_document_types_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Document Type 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_storage_paths_by_name(self, mock_get_objects): + mock_get_objects.return_value = StoragePath.objects.all() + names = ["Test Storage Path 1", "Nonexistent Storage Path"] + result = match_storage_paths_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Storage Path 1") + + def test_extract_unmatched_names(self): + llm_names = ["Test Tag 1", "Nonexistent Tag"] + matched_objects = [self.tag1] + unmatched_names = extract_unmatched_names(llm_names, matched_objects) + self.assertEqual(unmatched_names, ["Nonexistent Tag"]) + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_by_name_with_empty_names(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = [None, "", " "] + result = match_tags_by_name(names, user=None) + self.assertEqual(result, []) + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_with_fuzzy_matching(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = ["Test Taag 1", "Teest Tag 2"] + result = match_tags_by_name(names, user=None) + self.assertEqual(len(result), 2) + self.assertEqual(result[0].name, "Test Tag 1") + self.assertEqual(result[1].name, "Test Tag 2") diff --git a/uv.lock b/uv.lock index c621b203d..1a6b6b1d7 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,13 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] supported-markers = [ @@ -14,6 +16,101 @@ supported-markers = [ "sys_platform == 'linux'", ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.11.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "aiosignal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "async-timeout", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/c3/e5f64af7e97a02f547020e6ff861595766bb5ecb37c7492fac9fe3c14f6c/aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4", size = 711703, upload-time = "2025-04-21T09:40:25.487Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2f/53c26e96efa5fd01ebcfe1fefdfb7811f482bb21f4fa103d85eca4dcf888/aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6", size = 471348, upload-time = "2025-04-21T09:40:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/80/47/dcc248464c9b101532ee7d254a46f6ed2c1fd3f4f0f794cf1f2358c0d45b/aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609", size = 457611, upload-time = "2025-04-21T09:40:28.978Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ca/67d816ef075e8ac834b5f1f6b18e8db7d170f7aebaf76f1be462ea10cab0/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55", size = 1591976, upload-time = "2025-04-21T09:40:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/0c120287aa51c744438d99e9aae9f8c55ca5b9911c42706966c91c9d68d6/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f", size = 1632819, upload-time = "2025-04-21T09:40:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/3923c9040cd4927dfee1aa017513701e35adcfc35d10729909688ecaa465/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94", size = 1666567, upload-time = "2025-04-21T09:40:34.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ab/40dacb15c0c58f7f17686ea67bc186e9f207341691bdb777d1d5ff4671d5/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1", size = 1594959, upload-time = "2025-04-21T09:40:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/d40c2b7c4a5483f9a16ef0adffce279ced3cc44522e84b6ba9e906be5168/aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415", size = 1538516, upload-time = "2025-04-21T09:40:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/cf/10/e0bf3a03524faac45a710daa034e6f1878b24a1fef9c968ac8eb786ae657/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7", size = 1529037, upload-time = "2025-04-21T09:40:40.349Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d6/5ff5282e00e4eb59c857844984cbc5628f933e2320792e19f93aff518f52/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb", size = 1546813, upload-time = "2025-04-21T09:40:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/de/96/f1014f84101f9b9ad2d8acf3cc501426475f7f0cc62308ae5253e2fac9a7/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d", size = 1523852, upload-time = "2025-04-21T09:40:44.164Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/ec772c6838dd6bae3229065af671891496ac1834b252f305cee8152584b2/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421", size = 1603766, upload-time = "2025-04-21T09:40:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/84/38/31f85459c9402d409c1499284fc37a96f69afadce3cfac6a1b5ab048cbf1/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643", size = 1620647, upload-time = "2025-04-21T09:40:48.168Z" }, + { url = "https://files.pythonhosted.org/packages/31/2f/54aba0040764dd3d362fb37bd6aae9b3034fcae0b27f51b8a34864e48209/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868", size = 1559260, upload-time = "2025-04-21T09:40:50.219Z" }, + { url = "https://files.pythonhosted.org/packages/2f/10/fd9ee4f9e042818c3c2390054c08ccd34556a3cb209d83285616434cf93e/aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9", size = 712088, upload-time = "2025-04-21T09:40:55.776Z" }, + { url = "https://files.pythonhosted.org/packages/22/eb/6a77f055ca56f7aae2cd2a5607a3c9e7b9554f1497a069dcfcb52bfc9540/aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b", size = 471450, upload-time = "2025-04-21T09:40:57.301Z" }, + { url = "https://files.pythonhosted.org/packages/78/dc/5f3c0d27c91abf0bb5d103e9c9b0ff059f60cf6031a5f06f456c90731f42/aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66", size = 457836, upload-time = "2025-04-21T09:40:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/49/7b/55b65af9ef48b9b811c91ff8b5b9de9650c71147f10523e278d297750bc8/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756", size = 1690978, upload-time = "2025-04-21T09:41:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5a/3f8938c4f68ae400152b42742653477fc625d6bfe02e764f3521321c8442/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717", size = 1745307, upload-time = "2025-04-21T09:41:02.89Z" }, + { url = "https://files.pythonhosted.org/packages/b4/42/89b694a293333ef6f771c62da022163bcf44fb03d4824372d88e3dc12530/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4", size = 1780692, upload-time = "2025-04-21T09:41:04.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/1a75384e01dd1bf546898b6062b1b5f7a59b6692ef802e4dd6db64fed264/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f", size = 1676934, upload-time = "2025-04-21T09:41:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/442483276e6c368ab5169797d9873b5875213cbcf7e74b95ad1c5003098a/aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361", size = 1621190, upload-time = "2025-04-21T09:41:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/83/90274bf12c079457966008a58831a99675265b6a34b505243e004b408934/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1", size = 1658947, upload-time = "2025-04-21T09:41:11.054Z" }, + { url = "https://files.pythonhosted.org/packages/91/c1/da9cee47a0350b78fdc93670ebe7ad74103011d7778ab4c382ca4883098d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421", size = 1654443, upload-time = "2025-04-21T09:41:13.213Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/73cbe18dc25d624f79a09448adfc4972f82ed6088759ddcf783cd201956c/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e", size = 1644169, upload-time = "2025-04-21T09:41:14.827Z" }, + { url = "https://files.pythonhosted.org/packages/5b/32/970b0a196c4dccb1b0cfa5b4dc3b20f63d76f1c608f41001a84b2fd23c3d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d", size = 1728532, upload-time = "2025-04-21T09:41:17.168Z" }, + { url = "https://files.pythonhosted.org/packages/0b/50/b1dc810a41918d2ea9574e74125eb053063bc5e14aba2d98966f7d734da0/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f", size = 1750310, upload-time = "2025-04-21T09:41:19.353Z" }, + { url = "https://files.pythonhosted.org/packages/95/24/39271f5990b35ff32179cc95537e92499d3791ae82af7dcf562be785cd15/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd", size = 1691580, upload-time = "2025-04-21T09:41:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" }, + { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" }, + { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" }, + { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" }, + { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" }, + { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" }, + { url = "https://files.pythonhosted.org/packages/0a/18/be8b5dd6b9cf1b2172301dbed28e8e5e878ee687c21947a6c81d6ceaa15d/aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", size = 699833, upload-time = "2025-04-21T09:42:00.298Z" }, + { url = "https://files.pythonhosted.org/packages/0d/84/ecdc68e293110e6f6f6d7b57786a77555a85f70edd2b180fb1fafaff361a/aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", size = 462774, upload-time = "2025-04-21T09:42:02.015Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/f07718cca55884dad83cc2433746384d267ee970e91f0dcc75c6d5544079/aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", size = 454429, upload-time = "2025-04-21T09:42:03.728Z" }, + { url = "https://files.pythonhosted.org/packages/82/02/7f669c3d4d39810db8842c4e572ce4fe3b3a9b82945fdd64affea4c6947e/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", size = 1670283, upload-time = "2025-04-21T09:42:06.053Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/b82a12f67009b377b6c07a26bdd1b81dab7409fc2902d669dbfa79e5ac02/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", size = 1717231, upload-time = "2025-04-21T09:42:07.953Z" }, + { url = "https://files.pythonhosted.org/packages/a6/38/d5a1f28c3904a840642b9a12c286ff41fc66dfa28b87e204b1f242dbd5e6/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", size = 1769621, upload-time = "2025-04-21T09:42:09.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/deb3749ba293e716b5714dda06e257f123c5b8679072346b1eb28b766a0b/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", size = 1678667, upload-time = "2025-04-21T09:42:11.741Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a8/04b6e11683a54e104b984bd19a9790eb1ae5f50968b601bb202d0406f0ff/aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", size = 1601592, upload-time = "2025-04-21T09:42:14.137Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c33305ae8370b789423623f0e073d09ac775cd9c831ac0f11338b81c16e0/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", size = 1621679, upload-time = "2025-04-21T09:42:16.056Z" }, + { url = "https://files.pythonhosted.org/packages/56/45/8e9a27fff0538173d47ba60362823358f7a5f1653c6c30c613469f94150e/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", size = 1656878, upload-time = "2025-04-21T09:42:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/84/5b/8c5378f10d7a5a46b10cb9161a3aac3eeae6dba54ec0f627fc4ddc4f2e72/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", size = 1620509, upload-time = "2025-04-21T09:42:20.141Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/99dee7bd91c62c5ff0aa3c55f4ae7e1bc99c6affef780d7777c60c5b3735/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", size = 1680263, upload-time = "2025-04-21T09:42:21.993Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/378745e4ff88acb83e2d5c884a4fe993a6e9f04600a4560ce0e9b19936e3/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", size = 1715014, upload-time = "2025-04-21T09:42:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/b5524b3bb4b01e91bc4323aad0c2fcaebdf2f1b4d2eb22743948ba364958/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", size = 1666614, upload-time = "2025-04-21T09:42:25.764Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, +] + [[package]] name = "amqp" version = "5.3.1" @@ -26,6 +123,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" version = "4.11.0" @@ -146,6 +252,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] +[[package]] +name = "banks" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/34/2b6697f02ffb68bee50e5fd37d6c64432244d3245603fd62950169dfed7e/banks-2.1.2.tar.gz", hash = "sha256:a0651db9d14b57fa2e115e78f68dbb1b36fe226ad6eef96192542908b1d20c1f", size = 173332, upload-time = "2025-04-20T07:09:21.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4a/7fdca29d1db62f5f5c3446bf8f668beacdb0b5a8aff4247574ddfddc6bcd/banks-2.1.2-py3-none-any.whl", hash = "sha256:7fba451069f6bea376483b8136a0f29cb1e6883133626d00e077e20a3d102c0e", size = 28064, upload-time = "2025-04-20T07:09:20.201Z" }, +] + [[package]] name = "billiard" version = "4.2.2" @@ -654,6 +776,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/34/6171ab34715ed210bcd6c2b38839cc792993cff4fe2493f50bc92b0086a0/daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055", size = 29015, upload-time = "2025-07-02T12:57:03.793Z" }, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + [[package]] name = "dateparser" version = "1.2.2" @@ -693,6 +828,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } +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" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -702,6 +846,15 @@ 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]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "django" version = "5.2.7" @@ -1038,6 +1191,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036, upload-time = "2025-02-03T09:49:01.659Z" }, ] +[[package]] +name = "faiss-cpu" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", 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 = "numpy", version = "2.3.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 = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/56/87eb506d8634f08fc7c63d1ca5631aeec7d6b9afbfabedf2cb7a2a804b13/faiss_cpu-1.10.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6693474be296a7142ade1051ea18e7d85cedbfdee4b7eac9c52f83fed0467855", size = 7693034, upload-time = "2025-01-31T07:44:31.908Z" }, + { url = "https://files.pythonhosted.org/packages/51/46/f4d9de34ed1b06300b1a75b824d4857963216f5826de33f291af78088e39/faiss_cpu-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70ebe60a560414dc8dd6cfe8fed105c8f002c0d11f765f5adfe8d63d42c0467f", size = 3234656, upload-time = "2025-01-31T07:44:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/74/3a/e146861019d9290e0198b3470b8d13a658c3b5f228abefc3658ce0afd63d/faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:74c5712d4890f15c661ab7b1b75867812e9596e1469759956fad900999bedbb5", size = 3663789, upload-time = "2025-01-31T07:44:36.698Z" }, + { url = "https://files.pythonhosted.org/packages/aa/40/624f0002bb777e37aac1aadfadec1eb4391be6ad05b7fcfbf66049b99a48/faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:473d158fbd638d6ad5fb64469ba79a9f09d3494b5f4e8dfb4f40ce2fc335dca4", size = 30673545, upload-time = "2025-01-31T07:44:40.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/81800f41cb2c719c199d3eb534fcc154853123261d841e37482e8e468619/faiss_cpu-1.10.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8ff6924b0f00df278afe70940ae86302066466580724c2f3238860039e9946f1", size = 7693037, upload-time = "2025-01-31T07:44:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/8d/83/fc9028f6d6aec2c2f219f53a5d4a2b279434715643242e59a2e9755b1ce0/faiss_cpu-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb80b530a9ded44a7d4031a7355a237aaa0ff1f150c1176df050e0254ea5f6f6", size = 3234657, upload-time = "2025-01-31T07:44:51.399Z" }, + { url = "https://files.pythonhosted.org/packages/af/45/588a02e60daa73f6052611334fbbdffcedf37122320f1c91cb90f3e69b96/faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a9fef4039ed877d40e41d5563417b154c7f8cd57621487dad13c4eb4f32515f", size = 3663710, upload-time = "2025-01-31T07:44:53.198Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cf/9caa08ca4e21ab935f82be0713e5d60566140414c3fff7932d9427c8fd72/faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49b6647aa9e159a2c4603cbff2e1b313becd98ad6e851737ab325c74fe8e0278", size = 30673629, upload-time = "2025-01-31T07:44:56.652Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cc/f6aa1288dbb40b2a4f101d16900885e056541f37d8d08ec70462e92cf277/faiss_cpu-1.10.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:2aca486fe2d680ea64a18d356206c91ff85db99fd34c19a757298c67c23262b1", size = 7720242, upload-time = "2025-01-31T07:45:03.871Z" }, + { url = "https://files.pythonhosted.org/packages/be/56/40901306324a17fbc1eee8a6e86ba67bd99a67e768ce9908f271e648e9e0/faiss_cpu-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1108a4059c66c37c403183e566ca1ed0974a6af7557c92d49207639aab661bc", size = 3239223, upload-time = "2025-01-31T07:45:06.585Z" }, + { url = "https://files.pythonhosted.org/packages/2e/34/5b1463c450c9a6de3109caf8f38fbf0c329ef940ed1973fcf8c8ec7fa27e/faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:449f3eb778d6d937e01a16a3170de4bb8aabfe87c7cb479b458fb790276310c5", size = 3671461, upload-time = "2025-01-31T07:45:09.099Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/0b78c474289f23b31283d8fb64c8e6a522a7fa47b131a3c6c141c8e6639d/faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9899c340f92bd94071d6faf4bef0ccb5362843daea42144d4ba857a2a1f67511", size = 30663859, upload-time = "2025-01-31T07:45:13.027Z" }, + { url = "https://files.pythonhosted.org/packages/93/25/23239a83142faa319c4f8c025e25fec6cccc7418995eba3515218a57a45b/faiss_cpu-1.10.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:cb8473d69c3964c1bf3f8eb3e04287bb3275f536e6d9635ef32242b5f506b45d", size = 7720240, upload-time = "2025-01-31T07:45:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/18/f1/0e979277831af337739dbacf386d8a359a05eef9642df23d36e6c7d1b1a9/faiss_cpu-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82ca5098de694e7b8495c1a8770e2c08df6e834922546dad0ae1284ff519ced6", size = 3239224, upload-time = "2025-01-31T07:45:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fa/c2ad85b017a5754f6cdb09c179f8c4f4198d2a264046a8daa7a4d080521f/faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:035e4d797e2db7fc0d0c90531d4a655d089ad5d1382b7a49358c1f2307b3a309", size = 3671236, upload-time = "2025-01-31T07:45:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9b/759962f2c34800058f6a76457df3b0ab93b24f383650ea1ef0231acd322c/faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574", size = 30663948, upload-time = "2025-01-31T07:45:27.271Z" }, +] + [[package]] name = "faker" version = "37.8.0" @@ -1071,6 +1252,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "flower" version = "2.0.1" @@ -1087,6 +1277,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/ff/ee2f67c0ff146ec98b5df1df637b2bc2d17beeb05df9f427a67bd7a7d79c/flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2", size = 383553, upload-time = "2023-08-13T14:37:41.552Z" }, ] +[[package]] +name = "frozenlist" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831, upload-time = "2025-04-17T22:38:53.099Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/03/22e4eb297981d48468c3d9982ab6076b10895106d3039302a943bb60fd70/frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e", size = 160584, upload-time = "2025-04-17T22:35:48.163Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/c213e35bcf1c20502c6fd491240b08cdd6ceec212ea54873f4cae99a51e4/frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352", size = 124099, upload-time = "2025-04-17T22:35:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/2b/33/df17b921c2e37b971407b4045deeca6f6de7caf0103c43958da5e1b85e40/frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b", size = 122106, upload-time = "2025-04-17T22:35:51.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/09/93f0293e8a95c05eea7cf9277fef8929fb4d0a2234ad9394cd2a6b6a6bb4/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc", size = 287205, upload-time = "2025-04-17T22:35:53.441Z" }, + { url = "https://files.pythonhosted.org/packages/5e/34/35612f6f1b1ae0f66a4058599687d8b39352ade8ed329df0890fb553ea1e/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869", size = 295079, upload-time = "2025-04-17T22:35:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ca/51577ef6cc4ec818aab94a0034ef37808d9017c2e53158fef8834dbb3a07/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106", size = 308068, upload-time = "2025-04-17T22:35:57.119Z" }, + { url = "https://files.pythonhosted.org/packages/36/27/c63a23863b9dcbd064560f0fea41b516bbbf4d2e8e7eec3ff880a96f0224/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24", size = 305640, upload-time = "2025-04-17T22:35:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/33/c2/91720b3562a6073ba604547a417c8d3bf5d33e4c8f1231f3f8ff6719e05c/frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd", size = 278509, upload-time = "2025-04-17T22:36:00.199Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6e/1b64671ab2fca1ebf32c5b500205724ac14c98b9bc1574b2ef55853f4d71/frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8", size = 287318, upload-time = "2025-04-17T22:36:02.179Z" }, + { url = "https://files.pythonhosted.org/packages/66/30/589a8d8395d5ebe22a6b21262a4d32876df822c9a152e9f2919967bb8e1a/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c", size = 290923, upload-time = "2025-04-17T22:36:03.766Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e0/2bd0d2a4a7062b7e4b5aad621697cd3579e5d1c39d99f2833763d91e746d/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75", size = 304847, upload-time = "2025-04-17T22:36:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/a1a44204398a4b308c3ee1b7bf3bf56b9dcbcc4e61c890e038721d1498db/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249", size = 285580, upload-time = "2025-04-17T22:36:07.538Z" }, + { url = "https://files.pythonhosted.org/packages/78/ed/3862bc9abe05839a6a5f5bab8b6bbdf0fc9369505cb77cd15b8c8948f6a0/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769", size = 304033, upload-time = "2025-04-17T22:36:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9c/1c48454a9e1daf810aa6d977626c894b406651ca79d722fce0f13c7424f1/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02", size = 307566, upload-time = "2025-04-17T22:36:10.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/ef/cb43655c21f1bad5c42bcd540095bba6af78bf1e474b19367f6fd67d029d/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3", size = 295354, upload-time = "2025-04-17T22:36:12.181Z" }, + { url = "https://files.pythonhosted.org/packages/53/b5/bc883b5296ec902115c00be161da93bf661199c465ec4c483feec6ea4c32/frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", size = 160912, upload-time = "2025-04-17T22:36:17.235Z" }, + { url = "https://files.pythonhosted.org/packages/6f/93/51b058b563d0704b39c56baa222828043aafcac17fd3734bec5dbeb619b1/frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", size = 124315, upload-time = "2025-04-17T22:36:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e0/46cd35219428d350558b874d595e132d1c17a9471a1bd0d01d518a261e7c/frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", size = 122230, upload-time = "2025-04-17T22:36:20.6Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0f/7ad2ce928ad06d6dd26a61812b959ded573d3e9d0ee6109d96c2be7172e9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", size = 314842, upload-time = "2025-04-17T22:36:22.088Z" }, + { url = "https://files.pythonhosted.org/packages/34/76/98cbbd8a20a5c3359a2004ae5e5b216af84a150ccbad67c8f8f30fb2ea91/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", size = 304919, upload-time = "2025-04-17T22:36:24.247Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fa/258e771ce3a44348c05e6b01dffc2bc67603fba95761458c238cd09a2c77/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", size = 324074, upload-time = "2025-04-17T22:36:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a4/047d861fd8c538210e12b208c0479912273f991356b6bdee7ea8356b07c9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", size = 321292, upload-time = "2025-04-17T22:36:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/c0/25/cfec8af758b4525676cabd36efcaf7102c1348a776c0d1ad046b8a7cdc65/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", size = 301569, upload-time = "2025-04-17T22:36:29.448Z" }, + { url = "https://files.pythonhosted.org/packages/87/2f/0c819372fa9f0c07b153124bf58683b8d0ca7bb73ea5ccde9b9ef1745beb/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", size = 313625, upload-time = "2025-04-17T22:36:31.55Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/f0cf8b0fdedffdb76b3745aa13d5dbe404d63493cc211ce8250f2025307f/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", size = 312523, upload-time = "2025-04-17T22:36:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6c/38c49108491272d3e84125bbabf2c2d0b304899b52f49f0539deb26ad18d/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", size = 322657, upload-time = "2025-04-17T22:36:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/3bd3bad5be06a9d1b04b1c22be80b5fe65b502992d62fab4bdb25d9366ee/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", size = 303414, upload-time = "2025-04-17T22:36:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/5b/89/7e225a30bef6e85dbfe22622c24afe932e9444de3b40d58b1ea589a14ef8/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", size = 320321, upload-time = "2025-04-17T22:36:38.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/7e3acef4dd9e86366cb8f4d8f28e852c2b7e116927e9722b31a6f71ea4b0/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", size = 323975, upload-time = "2025-04-17T22:36:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/d8/85/e5da03d20507e13c66ce612c9792b76811b7a43e3320cce42d95b85ac755/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", size = 316553, upload-time = "2025-04-17T22:36:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193, upload-time = "2025-04-17T22:36:47.382Z" }, + { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831, upload-time = "2025-04-17T22:36:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862, upload-time = "2025-04-17T22:36:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361, upload-time = "2025-04-17T22:36:53.402Z" }, + { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115, upload-time = "2025-04-17T22:36:55.016Z" }, + { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505, upload-time = "2025-04-17T22:36:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666, upload-time = "2025-04-17T22:36:58.735Z" }, + { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119, upload-time = "2025-04-17T22:37:00.512Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226, upload-time = "2025-04-17T22:37:02.102Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788, upload-time = "2025-04-17T22:37:03.578Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914, upload-time = "2025-04-17T22:37:05.213Z" }, + { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283, upload-time = "2025-04-17T22:37:06.985Z" }, + { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264, upload-time = "2025-04-17T22:37:08.618Z" }, + { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482, upload-time = "2025-04-17T22:37:10.196Z" }, + { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248, upload-time = "2025-04-17T22:37:12.284Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e5/04c7090c514d96ca00887932417f04343ab94904a56ab7f57861bf63652d/frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", size = 158182, upload-time = "2025-04-17T22:37:16.837Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/60d0555c61eec855783a6356268314d204137f5e0c53b59ae2fc28938c99/frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", size = 122838, upload-time = "2025-04-17T22:37:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a7/d0ec890e3665b4b3b7c05dc80e477ed8dc2e2e77719368e78e2cd9fec9c8/frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", size = 120980, upload-time = "2025-04-17T22:37:19.857Z" }, + { url = "https://files.pythonhosted.org/packages/cc/19/9b355a5e7a8eba903a008579964192c3e427444752f20b2144b10bb336df/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", size = 305463, upload-time = "2025-04-17T22:37:21.328Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8d/5b4c758c2550131d66935ef2fa700ada2461c08866aef4229ae1554b93ca/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", size = 297985, upload-time = "2025-04-17T22:37:23.55Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/537ec09e032b5865715726b2d1d9813e6589b571d34d01550c7aeaad7e53/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", size = 311188, upload-time = "2025-04-17T22:37:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/31/2f/1aa74b33f74d54817055de9a4961eff798f066cdc6f67591905d4fc82a84/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", size = 311874, upload-time = "2025-04-17T22:37:26.791Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/cfec18838f13ebf4b37cfebc8649db5ea71a1b25dacd691444a10729776c/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f", size = 291897, upload-time = "2025-04-17T22:37:28.958Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a5/deb39325cbbea6cd0a46db8ccd76150ae2fcbe60d63243d9df4a0b8c3205/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", size = 305799, upload-time = "2025-04-17T22:37:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/78/22/6ddec55c5243a59f605e4280f10cee8c95a449f81e40117163383829c241/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", size = 302804, upload-time = "2025-04-17T22:37:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/d9ca9bab87f28855063c4d202936800219e39db9e46f9fb004d521152623/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", size = 316404, upload-time = "2025-04-17T22:37:34.59Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/1255305db7874d0b9eddb4fe4a27469e1fb63720f1fc6d325a5118492d18/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", size = 295572, upload-time = "2025-04-17T22:37:36.337Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f2/8d38eeee39a0e3a91b75867cc102159ecccf441deb6ddf67be96d3410b84/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", size = 307601, upload-time = "2025-04-17T22:37:37.923Z" }, + { url = "https://files.pythonhosted.org/packages/38/04/80ec8e6b92f61ef085422d7b196822820404f940950dde5b2e367bede8bc/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", size = 314232, upload-time = "2025-04-17T22:37:39.669Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/93b41fb23e75f38f453ae92a2f987274c64637c450285577bd81c599b715/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", size = 308187, upload-time = "2025-04-17T22:37:41.662Z" }, + { url = "https://files.pythonhosted.org/packages/df/bd/cc6d934991c1e5d9cafda83dfdc52f987c7b28343686aef2e58a9cf89f20/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", size = 174937, upload-time = "2025-04-17T22:37:46.635Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/daf945f335abdbfdd5993e9dc348ef4507436936ab3c26d7cfe72f4843bf/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", size = 136029, upload-time = "2025-04-17T22:37:48.192Z" }, + { url = "https://files.pythonhosted.org/packages/51/65/4c3145f237a31247c3429e1c94c384d053f69b52110a0d04bfc8afc55fb2/frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", size = 134831, upload-time = "2025-04-17T22:37:50.485Z" }, + { url = "https://files.pythonhosted.org/packages/77/38/03d316507d8dea84dfb99bdd515ea245628af964b2bf57759e3c9205cc5e/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", size = 392981, upload-time = "2025-04-17T22:37:52.558Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/46285ef9828f318ba400a51d5bb616ded38db8466836a9cfa39f3903260b/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", size = 371999, upload-time = "2025-04-17T22:37:54.092Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/1212fea37a112c3c5c05bfb5f0a81af4836ce349e69be75af93f99644da9/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", size = 392200, upload-time = "2025-04-17T22:37:55.951Z" }, + { url = "https://files.pythonhosted.org/packages/81/ce/9a6ea1763e3366e44a5208f76bf37c76c5da570772375e4d0be85180e588/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", size = 390134, upload-time = "2025-04-17T22:37:57.633Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/939738b0b495b2c6d0c39ba51563e453232813042a8d908b8f9544296c29/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", size = 365208, upload-time = "2025-04-17T22:37:59.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8b/939e62e93c63409949c25220d1ba8e88e3960f8ef6a8d9ede8f94b459d27/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", size = 385548, upload-time = "2025-04-17T22:38:01.416Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/22d2873c90102e06a7c5a3a5b82ca47e393c6079413e8a75c72bff067fa8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", size = 391123, upload-time = "2025-04-17T22:38:03.049Z" }, + { url = "https://files.pythonhosted.org/packages/44/78/63aaaf533ee0701549500f6d819be092c6065cb5c577edb70c09df74d5d0/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", size = 394199, upload-time = "2025-04-17T22:38:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/54/45/71a6b48981d429e8fbcc08454dc99c4c2639865a646d549812883e9c9dd3/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", size = 373854, upload-time = "2025-04-17T22:38:06.576Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f3/dbf2a5e11736ea81a66e37288bf9f881143a7822b288a992579ba1b4204d/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", size = 395412, upload-time = "2025-04-17T22:38:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f1/c63166806b331f05104d8ea385c4acd511598568b1f3e4e8297ca54f2676/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", size = 394936, upload-time = "2025-04-17T22:38:10.056Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/4f3e69e179a430473eaa1a75ff986526571215fefc6b9281cdc1f09a4eb8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", size = 391459, upload-time = "2025-04-17T22:38:11.826Z" }, + { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/d8/8425e6ba5fcec61a1d16e41b1b71d2bf9344f1fe48012c2b48b9620feae5/fsspec-2025.3.2.tar.gz", hash = "sha256:e52c77ef398680bbd6a98c0e628fbc469491282981209907bbc8aea76a04fdc6", size = 299281, upload-time = "2025-03-31T15:27:08.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4b/e0cfc1a6f17e990f3e64b7d941ddc4acdc7b19d6edd51abf495f32b1a9e4/fsspec-2025.3.2-py3-none-any.whl", hash = "sha256:2daf8dc3d1dfa65b6aa37748d112773a7a08416f6c70d96b264c96476ecaf711", size = 194435, upload-time = "2025-03-31T15:27:07.028Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1201,6 +1484,66 @@ uvloop = [ { name = "uvloop", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, ] +[[package]] +name = "greenlet" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/74/907bb43af91782e0366b0960af62a8ce1f9398e4291cac7beaeffbee0c04/greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7", size = 184475, upload-time = "2025-04-22T14:40:18.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3e/6332bb2d1e43ec6270e0b97bf253cd704691ee55e4e52196cb7da8f774e9/greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0", size = 267364, upload-time = "2025-04-22T14:25:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/73/c1/c47cc96878c4eda993a2deaba15af3cfdc87cf8e2e3c4c20726dea541a8c/greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157", size = 625721, upload-time = "2025-04-22T14:53:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/df1ff1a505a62b08d31da498ddc0c9992e9c536c01944f8b800a7cf17ac6/greenlet-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1a40a17e2c7348f5eee5d8e1b4fa6a937f0587eba89411885a36a8e1fc29bd2", size = 636983, upload-time = "2025-04-22T14:54:55.568Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1d/29944dcaaf5e482f7bff617de15f29e17cc0e74c7393888f8a43d7f6229e/greenlet-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5193135b3a8d0017cb438de0d49e92bf2f6c1c770331d24aa7500866f4db4017", size = 632880, upload-time = "2025-04-22T15:04:32.187Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c6/6c0891fd775b4fc5613593181526ba282771682dfe7bd0206d283403bcbb/greenlet-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639a94d001fe874675b553f28a9d44faed90f9864dc57ba0afef3f8d76a18b04", size = 631638, upload-time = "2025-04-22T14:27:02.856Z" }, + { url = "https://files.pythonhosted.org/packages/c0/50/3d8cadd4dfab17ef72bf0476cc2dacab368273ed29a79bbe66c36c6007a4/greenlet-3.2.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fe303381e7e909e42fb23e191fc69659910909fdcd056b92f6473f80ef18543", size = 580577, upload-time = "2025-04-22T14:25:54.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/bb0fc421318c69a840e5b98fdeea29d8dcb38f43ffe8b49664aeb10cc3dc/greenlet-3.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:72c9b668454e816b5ece25daac1a42c94d1c116d5401399a11b77ce8d883110c", size = 1109788, upload-time = "2025-04-22T14:58:54.243Z" }, + { url = "https://files.pythonhosted.org/packages/89/e9/db23a39effaef855deac9083a9054cbe34e1623dcbabed01e34a9d4174c7/greenlet-3.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6079ae990bbf944cf66bea64a09dcb56085815630955109ffa98984810d71565", size = 1133412, upload-time = "2025-04-22T14:28:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/a6ee52c59f75a387ec1f0c0075cf7981fb4644e4162afd3401dabeaa83ca/greenlet-3.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aa30066fd6862e1153eaae9b51b449a6356dcdb505169647f69e6ce315b9468b", size = 268609, upload-time = "2025-04-22T14:26:58.208Z" }, + { url = "https://files.pythonhosted.org/packages/ad/11/bd7a900629a4dd0e691dda88f8c2a7bfa44d0c4cffdb47eb5302f87a30d0/greenlet-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0f3a0a67786facf3b907a25db80efe74310f9d63cc30869e49c79ee3fcef7e", size = 628776, upload-time = "2025-04-22T14:53:43.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/f1/686754913fcc2707addadf815c884fd49c9f00a88e6dac277a1e1a8b8086/greenlet-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64a4d0052de53ab3ad83ba86de5ada6aeea8f099b4e6c9ccce70fb29bc02c6a2", size = 640827, upload-time = "2025-04-22T14:54:57.409Z" }, + { url = "https://files.pythonhosted.org/packages/03/74/bef04fa04125f6bcae2c1117e52f99c5706ac6ee90b7300b49b3bc18fc7d/greenlet-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852ef432919830022f71a040ff7ba3f25ceb9fe8f3ab784befd747856ee58530", size = 636752, upload-time = "2025-04-22T15:04:33.707Z" }, + { url = "https://files.pythonhosted.org/packages/aa/08/e8d493ab65ae1e9823638b8d0bf5d6b44f062221d424c5925f03960ba3d0/greenlet-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4818116e75a0dd52cdcf40ca4b419e8ce5cb6669630cb4f13a6c384307c9543f", size = 635993, upload-time = "2025-04-22T14:27:04.408Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9d/3a3a979f2b019fb756c9a92cd5e69055aded2862ebd0437de109cf7472a2/greenlet-3.2.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9afa05fe6557bce1642d8131f87ae9462e2a8e8c46f7ed7929360616088a3975", size = 583927, upload-time = "2025-04-22T14:25:55.896Z" }, + { url = "https://files.pythonhosted.org/packages/59/21/a00d27d9abb914c1213926be56b2a2bf47999cf0baf67d9ef5b105b8eb5b/greenlet-3.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5c12f0d17a88664757e81a6e3fc7c2452568cf460a2f8fb44f90536b2614000b", size = 1112891, upload-time = "2025-04-22T14:58:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/20/c7/922082bf41f0948a78d703d75261d5297f3db894758317409e4677dc1446/greenlet-3.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dbb4e1aa2000852937dd8f4357fb73e3911da426df8ca9b8df5db231922da474", size = 1138318, upload-time = "2025-04-22T14:28:09.451Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d1/e4777b188a04726f6cf69047830d37365b9191017f54caf2f7af336a6f18/greenlet-3.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0ba2811509a30e5f943be048895a983a8daf0b9aa0ac0ead526dfb5d987d80ea", size = 270381, upload-time = "2025-04-22T14:25:43.69Z" }, + { url = "https://files.pythonhosted.org/packages/59/e7/b5b738f5679247ddfcf2179c38945519668dced60c3164c20d55c1a7bb4a/greenlet-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4245246e72352b150a1588d43ddc8ab5e306bef924c26571aafafa5d1aaae4e8", size = 637195, upload-time = "2025-04-22T14:53:44.563Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9f/57968c88a5f6bc371364baf983a2e5549cca8f503bfef591b6dd81332cbc/greenlet-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7abc0545d8e880779f0c7ce665a1afc3f72f0ca0d5815e2b006cafc4c1cc5840", size = 651381, upload-time = "2025-04-22T14:54:59.439Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/1533c9a458e9f2ebccb3ae22f1463b2093b0eb448a88aac36182f1c2cd3d/greenlet-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dcc6d604a6575c6225ac0da39df9335cc0c6ac50725063fa90f104f3dbdb2c9", size = 646110, upload-time = "2025-04-22T15:04:35.739Z" }, + { url = "https://files.pythonhosted.org/packages/06/66/25f7e4b1468ebe4a520757f2e41c2a36a2f49a12e963431b82e9f98df2a0/greenlet-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2273586879affca2d1f414709bb1f61f0770adcabf9eda8ef48fd90b36f15d12", size = 648070, upload-time = "2025-04-22T14:27:05.976Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4c/49d366565c4c4d29e6f666287b9e2f471a66c3a3d8d5066692e347f09e27/greenlet-3.2.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff38c869ed30fff07f1452d9a204ece1ec6d3c0870e0ba6e478ce7c1515acf22", size = 603816, upload-time = "2025-04-22T14:25:57.224Z" }, + { url = "https://files.pythonhosted.org/packages/04/15/1612bb61506f44b6b8b6bebb6488702b1fe1432547e95dda57874303a1f5/greenlet-3.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e934591a7a4084fa10ee5ef50eb9d2ac8c4075d5c9cf91128116b5dca49d43b1", size = 1119572, upload-time = "2025-04-22T14:58:58.277Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2f/002b99dacd1610e825876f5cbbe7f86740aa2a6b76816e5eca41c8457e85/greenlet-3.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:063bcf7f8ee28eb91e7f7a8148c65a43b73fbdc0064ab693e024b5a940070145", size = 1147442, upload-time = "2025-04-22T14:28:11.243Z" }, + { url = "https://files.pythonhosted.org/packages/77/2a/581b3808afec55b2db838742527c40b4ce68b9b64feedff0fd0123f4b19a/greenlet-3.2.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e1967882f0c42eaf42282a87579685c8673c51153b845fde1ee81be720ae27ac", size = 269119, upload-time = "2025-04-22T14:25:01.798Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f3/1c4e27fbdc84e13f05afc2baf605e704668ffa26e73a43eca93e1120813e/greenlet-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77ae69032a95640a5fe8c857ec7bee569a0997e809570f4c92048691ce4b437", size = 637314, upload-time = "2025-04-22T14:53:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/9fc43cb0044f425f7252da9847893b6de4e3b20c0a748bce7ab3f063d5bc/greenlet-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3227c6ec1149d4520bc99edac3b9bc8358d0034825f3ca7572165cb502d8f29a", size = 651421, upload-time = "2025-04-22T14:55:00.852Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/d47c03cdc62c6680206b7420c4a98363ee997e87a5e9da1e83bd7eeb57a8/greenlet-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ddda0197c5b46eedb5628d33dad034c455ae77708c7bf192686e760e26d6a0c", size = 645789, upload-time = "2025-04-22T15:04:37.702Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/0faf8bee1b106c241780f377b9951dd4564ef0972de1942ef74687aa6bba/greenlet-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de62b542e5dcf0b6116c310dec17b82bb06ef2ceb696156ff7bf74a7a498d982", size = 648262, upload-time = "2025-04-22T14:27:07.55Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a8/73305f713183c2cb08f3ddd32eaa20a6854ba9c37061d682192db9b021c3/greenlet-3.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07a0c01010df42f1f058b3973decc69c4d82e036a951c3deaf89ab114054c07", size = 606770, upload-time = "2025-04-22T14:25:58.34Z" }, + { url = "https://files.pythonhosted.org/packages/c3/05/7d726e1fb7f8a6ac55ff212a54238a36c57db83446523c763e20cd30b837/greenlet-3.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2530bfb0abcd451ea81068e6d0a1aac6dabf3f4c23c8bd8e2a8f579c2dd60d95", size = 1117960, upload-time = "2025-04-22T14:59:00.373Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/2b6cb1bd9f1537e7b08c08705c4a1d7bd4f64489c67d102225c4fd262bda/greenlet-3.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c472adfca310f849903295c351d297559462067f618944ce2650a1878b84123", size = 1145500, upload-time = "2025-04-22T14:28:12.441Z" }, + { url = "https://files.pythonhosted.org/packages/f1/72/2a251d74a596af7bb1717e891ad4275a3fd5ac06152319d7ad8c77f876af/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175d583f7d5ee57845591fc30d852b75b144eb44b05f38b67966ed6df05c8526", size = 629889, upload-time = "2025-04-22T14:53:48.434Z" }, + { url = "https://files.pythonhosted.org/packages/29/2e/d7ed8bf97641bf704b6a43907c0e082cdf44d5bc026eb8e1b79283e7a719/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ecc9d33ca9428e4536ea53e79d781792cee114d2fa2695b173092bdbd8cd6d5", size = 635261, upload-time = "2025-04-22T14:55:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/802aa27848a6fcb5e566f69c64534f572e310f0f12d41e9201a81e741551/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f56382ac4df3860ebed8ed838f268f03ddf4e459b954415534130062b16bc32", size = 632523, upload-time = "2025-04-22T15:04:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/56/09/f7c1c3bab9b4c589ad356503dd71be00935e9c4db4db516ed88fc80f1187/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc45a7189c91c0f89aaf9d69da428ce8301b0fd66c914a499199cfb0c28420fc", size = 628816, upload-time = "2025-04-22T14:27:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/1bb90d30b5450eac2dffeaac6b692857c4bd642c21883b79faa8fa056cf2/greenlet-3.2.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51a2f49da08cff79ee42eb22f1658a2aed60c72792f0a0a95f5f0ca6d101b1fb", size = 593687, upload-time = "2025-04-22T14:25:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/adbe03c8b4c178add20cc716021183ae6b0326d56ba8793d7828c94286f6/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:0c68bbc639359493420282d2f34fa114e992a8724481d700da0b10d10a7611b8", size = 1105754, upload-time = "2025-04-22T14:59:02.585Z" }, + { url = "https://files.pythonhosted.org/packages/39/93/84582d7ef38dec009543ccadec6ab41079a6cbc2b8c0566bcd07bf1aaf6c/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:e775176b5c203a1fa4be19f91da00fd3bff536868b77b237da3f4daa5971ae5d", size = 1125160, upload-time = "2025-04-22T14:28:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/01/e6/f9d759788518a6248684e3afeb3691f3ab0276d769b6217a1533362298c8/greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189", size = 269897, upload-time = "2025-04-22T14:27:14.044Z" }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1342,6 +1685,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/4b/2b81e876abf77b4af3372aff731f4f6722840ebc7dcfd85778eaba271733/httpx_oauth-0.16.1-py3-none-any.whl", hash = "sha256:2fcad82f80f28d0473a0fc4b4eda223dc952050af7e3a8c8781342d850f09fb5", size = 38056, upload-time = "2024-12-20T07:23:00.394Z" }, ] +[[package]] +name = "huggingface-hub" +version = "0.30.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868, upload-time = "2025-04-08T08:32:45.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433, upload-time = "2025-04-08T08:32:43.305Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] + [[package]] name = "humanize" version = "4.13.0" @@ -1500,6 +1866,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/82/39f7c9e67b3b0121f02a0b90d433626caa95a565c3d2449fea6bcfa3f5f5/jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad", size = 314540, upload-time = "2025-03-10T21:35:02.218Z" }, + { url = "https://files.pythonhosted.org/packages/01/07/7bf6022c5a152fca767cf5c086bb41f7c28f70cf33ad259d023b53c0b858/jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea", size = 321065, upload-time = "2025-03-10T21:35:04.274Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b2/de3f3446ecba7c48f317568e111cc112613da36c7b29a6de45a1df365556/jiter-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1339f839b91ae30b37c409bf16ccd3dc453e8b8c3ed4bd1d6a567193651a4a51", size = 341664, upload-time = "2025-03-10T21:35:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/cf/6485a4012af5d407689c91296105fcdb080a3538e0658d2abf679619c72f/jiter-0.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ffba79584b3b670fefae66ceb3a28822365d25b7bf811e030609a3d5b876f538", size = 364635, upload-time = "2025-03-10T21:35:07.749Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f7/4a491c568f005553240b486f8e05c82547340572d5018ef79414b4449327/jiter-0.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cfc7d0a8e899089d11f065e289cb5b2daf3d82fbe028f49b20d7b809193958d", size = 406288, upload-time = "2025-03-10T21:35:09.238Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ca/f4263ecbce7f5e6bded8f52a9f1a66540b270c300b5c9f5353d163f9ac61/jiter-0.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e00a1a2bbfaaf237e13c3d1592356eab3e9015d7efd59359ac8b51eb56390a12", size = 397499, upload-time = "2025-03-10T21:35:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a2/522039e522a10bac2f2194f50e183a49a360d5f63ebf46f6d890ef8aa3f9/jiter-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1d9870561eb26b11448854dce0ff27a9a27cb616b632468cafc938de25e9e51", size = 352926, upload-time = "2025-03-10T21:35:13.85Z" }, + { url = "https://files.pythonhosted.org/packages/b1/67/306a5c5abc82f2e32bd47333a1c9799499c1c3a415f8dde19dbf876f00cb/jiter-0.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9872aeff3f21e437651df378cb75aeb7043e5297261222b6441a620218b58708", size = 384506, upload-time = "2025-03-10T21:35:15.735Z" }, + { url = "https://files.pythonhosted.org/packages/0f/89/c12fe7b65a4fb74f6c0d7b5119576f1f16c79fc2953641f31b288fad8a04/jiter-0.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fd19112d1049bdd47f17bfbb44a2c0001061312dcf0e72765bfa8abd4aa30e5", size = 520621, upload-time = "2025-03-10T21:35:17.55Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/d57900c5c06e6273fbaa76a19efa74dbc6e70c7427ab421bf0095dfe5d4a/jiter-0.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef5da104664e526836070e4a23b5f68dec1cc673b60bf1edb1bfbe8a55d0678", size = 512613, upload-time = "2025-03-10T21:35:19.178Z" }, + { url = "https://files.pythonhosted.org/packages/23/44/e241a043f114299254e44d7e777ead311da400517f179665e59611ab0ee4/jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af", size = 314654, upload-time = "2025-03-10T21:35:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/a7e5e42db9fa262baaa9489d8d14ca93f8663e7f164ed5e9acc9f467fc00/jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58", size = 320909, upload-time = "2025-03-10T21:35:26.127Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/8ebdfce77bc04b81abf2ea316e9c03b4a866a7d739cf355eae4d6fd9f6fe/jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b", size = 341733, upload-time = "2025-03-10T21:35:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/754ebce77cff9ab34d1d0fa0fe98f5d42590fd33622509a3ba6ec37ff466/jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b", size = 365097, upload-time = "2025-03-10T21:35:29.605Z" }, + { url = "https://files.pythonhosted.org/packages/32/2c/6019587e6f5844c612ae18ca892f4cd7b3d8bbf49461ed29e384a0f13d98/jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5", size = 406603, upload-time = "2025-03-10T21:35:31.696Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/c9e6546c817ab75a1a7dab6dcc698e62e375e1017113e8e983fccbd56115/jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572", size = 396625, upload-time = "2025-03-10T21:35:33.182Z" }, + { url = "https://files.pythonhosted.org/packages/be/bd/976b458add04271ebb5a255e992bd008546ea04bb4dcadc042a16279b4b4/jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15", size = 351832, upload-time = "2025-03-10T21:35:35.394Z" }, + { url = "https://files.pythonhosted.org/packages/07/51/fe59e307aaebec9265dbad44d9d4381d030947e47b0f23531579b9a7c2df/jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419", size = 384590, upload-time = "2025-03-10T21:35:37.171Z" }, + { url = "https://files.pythonhosted.org/packages/db/55/5dcd2693794d8e6f4889389ff66ef3be557a77f8aeeca8973a97a7c00557/jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043", size = 520690, upload-time = "2025-03-10T21:35:38.717Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/9f51dc90985e9eb251fbbb747ab2b13b26601f16c595a7b8baba964043bd/jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965", size = 512649, upload-time = "2025-03-10T21:35:40.157Z" }, + { url = "https://files.pythonhosted.org/packages/af/d7/c55086103d6f29b694ec79156242304adf521577530d9031317ce5338c59/jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11", size = 309203, upload-time = "2025-03-10T21:35:44.852Z" }, + { url = "https://files.pythonhosted.org/packages/b0/01/f775dfee50beb420adfd6baf58d1c4d437de41c9b666ddf127c065e5a488/jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e", size = 319678, upload-time = "2025-03-10T21:35:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b8/09b73a793714726893e5d46d5c534a63709261af3d24444ad07885ce87cb/jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2", size = 341816, upload-time = "2025-03-10T21:35:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/6f/b8f89ec5398b2b0d344257138182cc090302854ed63ed9c9051e9c673441/jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75", size = 364152, upload-time = "2025-03-10T21:35:49.397Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/978cc3183113b8e4484cc7e210a9ad3c6614396e7abd5407ea8aa1458eef/jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d", size = 406991, upload-time = "2025-03-10T21:35:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/72861883e11a36d6aa314b4922125f6ae90bdccc225cd96d24cc78a66385/jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42", size = 395824, upload-time = "2025-03-10T21:35:52.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/67/22728a86ef53589c3720225778f7c5fdb617080e3deaed58b04789418212/jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc", size = 351318, upload-time = "2025-03-10T21:35:53.566Z" }, + { url = "https://files.pythonhosted.org/packages/69/b9/f39728e2e2007276806d7a6609cda7fac44ffa28ca0d02c49a4f397cc0d9/jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc", size = 384591, upload-time = "2025-03-10T21:35:54.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8f/8a708bc7fd87b8a5d861f1c118a995eccbe6d672fe10c9753e67362d0dd0/jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e", size = 520746, upload-time = "2025-03-10T21:35:56.444Z" }, + { url = "https://files.pythonhosted.org/packages/95/1e/65680c7488bd2365dbd2980adaf63c562d3d41d3faac192ebc7ef5b4ae25/jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d", size = 512754, upload-time = "2025-03-10T21:35:58.789Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1b/4cd165c362e8f2f520fdb43245e2b414f42a255921248b4f8b9c8d871ff1/jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7", size = 308197, upload-time = "2025-03-10T21:36:03.828Z" }, + { url = "https://files.pythonhosted.org/packages/13/aa/7a890dfe29c84c9a82064a9fe36079c7c0309c91b70c380dc138f9bea44a/jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b", size = 318160, upload-time = "2025-03-10T21:36:05.281Z" }, + { url = "https://files.pythonhosted.org/packages/6a/38/5888b43fc01102f733f085673c4f0be5a298f69808ec63de55051754e390/jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69", size = 341259, upload-time = "2025-03-10T21:36:06.716Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5e/bbdbb63305bcc01006de683b6228cd061458b9b7bb9b8d9bc348a58e5dc2/jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103", size = 363730, upload-time = "2025-03-10T21:36:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/75/85/53a3edc616992fe4af6814c25f91ee3b1e22f7678e979b6ea82d3bc0667e/jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635", size = 405126, upload-time = "2025-03-10T21:36:10.934Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/1ee26b12b2693bd3f0b71d3188e4e5d817b12e3c630a09e099e0a89e28fa/jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4", size = 393668, upload-time = "2025-03-10T21:36:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/11/87/e084ce261950c1861773ab534d49127d1517b629478304d328493f980791/jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d", size = 352350, upload-time = "2025-03-10T21:36:14.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/06/7dca84b04987e9df563610aa0bc154ea176e50358af532ab40ffb87434df/jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3", size = 384204, upload-time = "2025-03-10T21:36:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/16/2f/82e1c6020db72f397dd070eec0c85ebc4df7c88967bc86d3ce9864148f28/jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5", size = 520322, upload-time = "2025-03-10T21:36:17.016Z" }, + { url = "https://files.pythonhosted.org/packages/36/fd/4f0cd3abe83ce208991ca61e7e5df915aa35b67f1c0633eb7cf2f2e88ec7/jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d", size = 512184, upload-time = "2025-03-10T21:36:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/78/0f/77a63ca7aa5fed9a1b9135af57e190d905bcd3702b36aca46a01090d39ad/jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001", size = 317281, upload-time = "2025-03-10T21:36:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/a3a1571712c2bf6ec4c657f0d66da114a63a2e32b7e4eb8e0b83295ee034/jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a", size = 350273, upload-time = "2025-03-10T21:36:24.414Z" }, +] + [[package]] name = "joblib" version = "1.5.2" @@ -1565,6 +1981,106 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } +[[package]] +name = "llama-index-core" +version = "0.12.33.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "banks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dataclasses-json", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dirtyjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "filetype", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "nest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "nltk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", 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 = "numpy", version = "2.3.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 = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sqlalchemy", extra = ["asyncio"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tiktoken", 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-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/51/e99358e80b0d80777c84081159d351f51feaa6c7d7054486bbbb49f6c9c0/llama_index_core-0.12.33.post1.tar.gz", hash = "sha256:d257f6f594dfd9cf6435af02761a3d21f1427df5347f0e5e9fffe4024db6a724", size = 7282200, upload-time = "2025-04-23T18:48:42.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/01/6fcf557a72ad25734327515db506744f8f8ba95846a0f7e055c8fa95a54d/llama_index_core-0.12.33.post1-py3-none-any.whl", hash = "sha256:2c4a316a1ae9ec86c817d44961d1058691632acb3a7021e6af56fcfb8735fd3d", size = 7650733, upload-time = "2025-04-23T18:48:33.433Z" }, +] + +[[package]] +name = "llama-index-embeddings-huggingface" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", extra = ["inference"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/35c4f1623fc33b60a91490c0849b8bdb6c704f18cade400ee5baaf064c0d/llama_index_embeddings_huggingface-0.5.3.tar.gz", hash = "sha256:3fecb363cc0d05890689aefc2d1bda2f02431857e0456fdaf2e8c4960f9daeaf", size = 7947, upload-time = "2025-04-08T21:48:42.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/21/8c201efcd3c69b4ccb7a876ccd80464525d2c1ba49bec9a99f91604055d9/llama_index_embeddings_huggingface-0.5.3-py3-none-any.whl", hash = "sha256:f181d6490ebb29f0e7ada93b53fdd9acf5808387cdd69b9f1b499a2380d38758", size = 8951, upload-time = "2025-04-08T21:48:41.032Z" }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", 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/a1/02/a2604ef3a167131fdd701888f45f16c8efa6d523d02efe8c4e640238f4ea/llama_index_embeddings_openai-0.3.1.tar.gz", hash = "sha256:1368aad3ce24cbaed23d5ad251343cef1eb7b4a06d6563d6606d59cb347fef20", size = 5492, upload-time = "2024-11-27T16:04:17.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/45/ca55b91c4ac1b6251d4099fa44121a6c012129822906cadcc27b8cfb33a4/llama_index_embeddings_openai-0.3.1-py3-none-any.whl", hash = "sha256:f15a3d13da9b6b21b8bd51d337197879a453d1605e625a1c6d45e741756c0290", size = 6177, upload-time = "2024-11-27T16:04:15.981Z" }, +] + +[[package]] +name = "llama-index-llms-ollama" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/05/bd6a1006d93566a65cb5ef8318bca8695a9a7cfc6064c1bd218aae3a5ded/llama_index_llms_ollama-0.5.4.tar.gz", hash = "sha256:e5e7e7a4e65478c762906d08f594647d8148ffdc32ae908d56b73c0df8ea04f2", size = 8218, upload-time = "2025-03-27T01:05:04.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/1d/f000f50525dff87e6a78d7d7623720fdfc14b95a04b25e2bc4e936af2907/llama_index_llms_ollama-0.5.4-py3-none-any.whl", hash = "sha256:203688429a99454ae261877dd48f68f65374edf92ba25796a6a8a2346fd005f0", size = 7790, upload-time = "2025-03-27T01:05:03.611Z" }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.3.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", 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/4d/bd/b0ceae2d5d697feb5d18a7402214cdad30bc20d8cbe1619e9e6355361ca5/llama_index_llms_openai-0.3.38.tar.gz", hash = "sha256:bcd1d5212bf7c948301958719a1df361be62b37b5620732e4c9ce804bc078b77", size = 22738, upload-time = "2025-04-21T21:52:08.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/e1/1c185e22ca1fd1ac813d225be046c4223dbe2fdf64d90a6e86608e6d17ad/llama_index_llms_openai-0.3.38-py3-none-any.whl", hash = "sha256:d724b809d5e81e15cd1c3def65f023c4c74f2a097e542e5c002793ffbaa33a96", size = 23839, upload-time = "2025-04-21T21:52:06.99Z" }, +] + +[[package]] +name = "llama-index-vector-stores-faiss" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ec/6890f61bbdd5afa52c4b133e145d06f22121334f7d615a1f6b8879beb35b/llama_index_vector_stores_faiss-0.3.0.tar.gz", hash = "sha256:c9df99dd00fe7058606ef4fce113535fa30b73edd650136be87c9b5b240df3f9", size = 3454, upload-time = "2024-11-17T22:55:00.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/16/d3f512f47d40a27ee1c75ce86faa91004de9c20202ca376fe35bc92ba413/llama_index_vector_stores_faiss-0.3.0-py3-none-any.whl", hash = "sha256:2148163dba1222c855bd367a7b796bc35d46dc2e77d57bafd321ba14aac00177", size = 3868, upload-time = "2024-11-17T22:54:59.249Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -1763,6 +2279,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, ] +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1861,6 +2389,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "msgpack" version = "1.1.1" @@ -1901,6 +2438,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, ] +[[package]] +name = "multidict" +version = "6.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372, upload-time = "2025-04-10T22:20:17.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/44/45e798d4cd1b5dfe41ddf36266c7aca6d954e3c7a8b0d599ad555ce2b4f8/multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", size = 65822, upload-time = "2025-04-10T22:17:32.83Z" }, + { url = "https://files.pythonhosted.org/packages/10/fb/9ea024f928503f8c758f8463759d21958bf27b1f7a1103df73e5022e6a7c/multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", size = 38706, upload-time = "2025-04-10T22:17:35.028Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/7013316febca37414c0e1469fccadcb1a0e4315488f8f57ca5d29b384863/multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", size = 37979, upload-time = "2025-04-10T22:17:36.626Z" }, + { url = "https://files.pythonhosted.org/packages/64/28/5a7bf4e7422613ea80f9ebc529d3845b20a422cfa94d4355504ac98047ee/multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", size = 220233, upload-time = "2025-04-10T22:17:37.807Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/b4c58850f71befde6a16548968b48331a155a80627750b150bb5962e4dea/multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", size = 217762, upload-time = "2025-04-10T22:17:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/99/a3/393e23bba1e9a00f95b3957acd8f5e3ee3446e78c550f593be25f9de0483/multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", size = 230699, upload-time = "2025-04-10T22:17:41.207Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a7/52c63069eb1a079f824257bb8045d93e692fa2eb34d08323d1fdbdfc398a/multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", size = 226801, upload-time = "2025-04-10T22:17:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/40d2b73e7d6574d91074d83477a990e3701affbe8b596010d4f5e6c7a6fa/multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", size = 219833, upload-time = "2025-04-10T22:17:44.046Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6a/0572b22fe63c632254f55a1c1cb7d29f644002b1d8731d6103a290edc754/multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", size = 212920, upload-time = "2025-04-10T22:17:45.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/fe/c63735db9dece0053868b2d808bcc2592a83ce1830bc98243852a2b34d42/multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", size = 225263, upload-time = "2025-04-10T22:17:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/2db296d64d41525110c27ed38fadd5eb571c6b936233e75a5ea61b14e337/multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", size = 214249, upload-time = "2025-04-10T22:17:48.95Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/8bc26e54c79f9a0f111350b1b28a9cacaaee53ecafccd53c90e59754d55a/multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", size = 221650, upload-time = "2025-04-10T22:17:50.265Z" }, + { url = "https://files.pythonhosted.org/packages/af/d7/2ce87606e3799d9a08a941f4c170930a9895886ea8bd0eca75c44baeebe3/multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", size = 231235, upload-time = "2025-04-10T22:17:51.579Z" }, + { url = "https://files.pythonhosted.org/packages/07/e1/d191a7ad3b90c613fc4b130d07a41c380e249767586148709b54d006ca17/multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", size = 226056, upload-time = "2025-04-10T22:17:53.092Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/a57490cf6a8d5854f4af2d17dfc54924f37fbb683986e133b76710a36079/multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", size = 220014, upload-time = "2025-04-10T22:17:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/16/e0/53cf7f27eda48fffa53cfd4502329ed29e00efb9e4ce41362cbf8aa54310/multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", size = 65259, upload-time = "2025-04-10T22:17:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/1dcd93ce7070cf01c2ee29f781c42b33c64fce20033808f1cc9ec8413d6e/multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", size = 38451, upload-time = "2025-04-10T22:18:01.202Z" }, + { url = "https://files.pythonhosted.org/packages/f4/35/2292cf29ab5f0d0b3613fad1b75692148959d3834d806be1885ceb49a8ff/multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad", size = 37706, upload-time = "2025-04-10T22:18:02.276Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d1/6b157110b2b187b5a608b37714acb15ee89ec773e3800315b0107ea648cd/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", size = 226669, upload-time = "2025-04-10T22:18:03.436Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/61a476450651f177c5570e04bd55947f693077ba7804fe9717ee9ae8de04/multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", size = 223182, upload-time = "2025-04-10T22:18:04.922Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/eaf7502ac4824cdd8edcf5723e2e99f390c879866aec7b0c420267b53749/multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", size = 235025, upload-time = "2025-04-10T22:18:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f6/facdbbd73c96b67a93652774edd5778ab1167854fa08ea35ad004b1b70ad/multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", size = 231481, upload-time = "2025-04-10T22:18:07.742Z" }, + { url = "https://files.pythonhosted.org/packages/70/57/c008e861b3052405eebf921fd56a748322d8c44dcfcab164fffbccbdcdc4/multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", size = 223492, upload-time = "2025-04-10T22:18:09.095Z" }, + { url = "https://files.pythonhosted.org/packages/30/4d/7d8440d3a12a6ae5d6b202d6e7f2ac6ab026e04e99aaf1b73f18e6bc34bc/multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", size = 217279, upload-time = "2025-04-10T22:18:10.474Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e7/bca0df4dd057597b94138d2d8af04eb3c27396a425b1b0a52e082f9be621/multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", size = 228733, upload-time = "2025-04-10T22:18:11.793Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/383827c3f1c38d7c92dbad00a8a041760228573b1c542fbf245c37bbca8a/multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", size = 218089, upload-time = "2025-04-10T22:18:13.153Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/a5174e8a7d8b94b4c8f9c1e2cf5d07451f41368ffe94d05fc957215b8e72/multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", size = 225257, upload-time = "2025-04-10T22:18:14.654Z" }, + { url = "https://files.pythonhosted.org/packages/8c/76/1d4b7218f0fd00b8e5c90b88df2e45f8af127f652f4e41add947fa54c1c4/multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", size = 234728, upload-time = "2025-04-10T22:18:16.236Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/18372a4f6273fc7ca25630d7bf9ae288cde64f29593a078bff450c7170b6/multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", size = 230087, upload-time = "2025-04-10T22:18:17.979Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/28728c314a698d8a6d9491fcacc897077348ec28dd85884d09e64df8a855/multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", size = 223137, upload-time = "2025-04-10T22:18:19.362Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019, upload-time = "2025-04-10T22:18:23.174Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925, upload-time = "2025-04-10T22:18:24.834Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008, upload-time = "2025-04-10T22:18:26.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374, upload-time = "2025-04-10T22:18:27.714Z" }, + { url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869, upload-time = "2025-04-10T22:18:29.162Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949, upload-time = "2025-04-10T22:18:30.679Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032, upload-time = "2025-04-10T22:18:32.146Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517, upload-time = "2025-04-10T22:18:33.538Z" }, + { url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291, upload-time = "2025-04-10T22:18:34.962Z" }, + { url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982, upload-time = "2025-04-10T22:18:36.443Z" }, + { url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823, upload-time = "2025-04-10T22:18:37.924Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714, upload-time = "2025-04-10T22:18:39.807Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739, upload-time = "2025-04-10T22:18:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809, upload-time = "2025-04-10T22:18:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934, upload-time = "2025-04-10T22:18:44.311Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831, upload-time = "2025-04-10T22:18:48.748Z" }, + { url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888, upload-time = "2025-04-10T22:18:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852, upload-time = "2025-04-10T22:18:51.246Z" }, + { url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644, upload-time = "2025-04-10T22:18:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446, upload-time = "2025-04-10T22:18:54.509Z" }, + { url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070, upload-time = "2025-04-10T22:18:56.019Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956, upload-time = "2025-04-10T22:18:59.146Z" }, + { url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599, upload-time = "2025-04-10T22:19:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136, upload-time = "2025-04-10T22:19:02.244Z" }, + { url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139, upload-time = "2025-04-10T22:19:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251, upload-time = "2025-04-10T22:19:06.117Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868, upload-time = "2025-04-10T22:19:07.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106, upload-time = "2025-04-10T22:19:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163, upload-time = "2025-04-10T22:19:11Z" }, + { url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906, upload-time = "2025-04-10T22:19:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642, upload-time = "2025-04-10T22:19:17.527Z" }, + { url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028, upload-time = "2025-04-10T22:19:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424, upload-time = "2025-04-10T22:19:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178, upload-time = "2025-04-10T22:19:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617, upload-time = "2025-04-10T22:19:23.773Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919, upload-time = "2025-04-10T22:19:25.35Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097, upload-time = "2025-04-10T22:19:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706, upload-time = "2025-04-10T22:19:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728, upload-time = "2025-04-10T22:19:30.481Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276, upload-time = "2025-04-10T22:19:32.454Z" }, + { url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069, upload-time = "2025-04-10T22:19:34.17Z" }, + { url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858, upload-time = "2025-04-10T22:19:35.879Z" }, + { url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988, upload-time = "2025-04-10T22:19:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435, upload-time = "2025-04-10T22:19:39.005Z" }, + { url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494, upload-time = "2025-04-10T22:19:41.447Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -1956,6 +2580,24 @@ version = "2.2.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" } +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + [[package]] name = "nltk" version = "3.9.2" @@ -2040,10 +2682,12 @@ name = "numpy" version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } wheels = [ @@ -2103,6 +2747,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.6.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.6.80" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.5.1.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, + { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.11.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.7.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, + { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.26.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, +] + [[package]] name = "oauthlib" version = "3.3.1" @@ -2132,6 +2909,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/34/d9d04420e6f7a71e2135b41599dae273e4ef36e2ce79b065b65fb2471636/ocrmypdf-16.12.0-py3-none-any.whl", hash = "sha256:0ea5c42027db9cf3bd12b0d0b4190689027ef813fdad3377106ea66bba0012c3", size = 163415, upload-time = "2025-11-11T22:30:11.56Z" }, ] +[[package]] +name = "ollama" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/64/709dc99030f8f46ec552f0a7da73bbdcc2da58666abfec4742ccdb2e800e/ollama-0.4.8.tar.gz", hash = "sha256:1121439d49b96fa8339842965d0616eba5deb9f8c790786cdf4c0b3df4833802", size = 12972, upload-time = "2025-04-16T21:55:14.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/3f/164de150e983b3a16e8bf3d4355625e51a357e7b3b1deebe9cc1f7cb9af8/ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c", size = 13325, upload-time = "2025-04-16T21:55:12.779Z" }, +] + +[[package]] +name = "openai" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jiter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sniffio", 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660, upload-time = "2025-04-23T16:33:53.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201, upload-time = "2025-04-23T16:33:51.12Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -2181,6 +2990,7 @@ dependencies = [ { name = "drf-spectacular", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "drf-spectacular-sidecar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "drf-writable-nested", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "faiss-cpu", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "flower", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "gotenberg-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2189,8 +2999,15 @@ dependencies = [ { name = "inotifyrecursive", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "langdetect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-embeddings-huggingface", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-embeddings-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-llms-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-llms-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-vector-stores-faiss", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "nltk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ocrmypdf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pathvalidate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pdf2image", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2203,6 +3020,7 @@ dependencies = [ { name = "redis", extra = ["hiredis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "scikit-learn", 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 = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2319,6 +3137,7 @@ requires-dist = [ { name = "drf-spectacular", specifier = "~=0.28" }, { name = "drf-spectacular-sidecar", specifier = "~=2025.10.1" }, { name = "drf-writable-nested", specifier = "~=0.7.1" }, + { name = "faiss-cpu", specifier = ">=1.10" }, { name = "filelock", specifier = "~=3.20.0" }, { name = "flower", specifier = "~=2.0.1" }, { name = "gotenberg-client", specifier = "~=0.12.0" }, @@ -2328,9 +3147,16 @@ requires-dist = [ { name = "inotifyrecursive", specifier = "~=0.3" }, { name = "jinja2", specifier = "~=3.1.5" }, { name = "langdetect", specifier = "~=1.0.9" }, + { name = "llama-index-core", specifier = ">=0.12.33.post1" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.5.3" }, + { name = "llama-index-embeddings-openai", specifier = ">=0.3.1" }, + { name = "llama-index-llms-ollama", specifier = ">=0.5.4" }, + { name = "llama-index-llms-openai", specifier = ">=0.3.38" }, + { name = "llama-index-vector-stores-faiss", specifier = ">=0.3" }, { name = "mysqlclient", marker = "extra == 'mariadb'", specifier = "~=2.2.7" }, { name = "nltk", specifier = "~=3.9.1" }, { name = "ocrmypdf", specifier = "~=16.12.0" }, + { name = "openai", specifier = ">=1.76" }, { name = "pathvalidate", specifier = "~=3.3.1" }, { name = "pdf2image", specifier = "~=1.17.0" }, { name = "psycopg", extras = ["c", "pool"], marker = "extra == 'postgres'", specifier = "==3.2.12" }, @@ -2348,6 +3174,7 @@ requires-dist = [ { name = "redis", extras = ["hiredis"], specifier = "~=5.2.1" }, { name = "regex", specifier = ">=2025.9.18" }, { name = "scikit-learn", specifier = "~=1.7.0" }, + { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, { name = "tika-client", specifier = "~=0.10.0" }, { name = "tqdm", specifier = "~=4.67.1" }, @@ -2714,6 +3541,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "propcache" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651, upload-time = "2025-03-26T03:06:12.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/e27c136101addf877c8291dbda1b3b86ae848f3837ce758510a0d806c92f/propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", size = 80224, upload-time = "2025-03-26T03:03:35.81Z" }, + { url = "https://files.pythonhosted.org/packages/63/bd/88e98836544c4f04db97eefd23b037c2002fa173dd2772301c61cd3085f9/propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", size = 46491, upload-time = "2025-03-26T03:03:38.107Z" }, + { url = "https://files.pythonhosted.org/packages/15/43/0b8eb2a55753c4a574fc0899885da504b521068d3b08ca56774cad0bea2b/propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", size = 45927, upload-time = "2025-03-26T03:03:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6c/d01f9dfbbdc613305e0a831016844987a1fb4861dd221cd4c69b1216b43f/propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", size = 206135, upload-time = "2025-03-26T03:03:40.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8a/e6e1c77394088f4cfdace4a91a7328e398ebed745d59c2f6764135c5342d/propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", size = 220517, upload-time = "2025-03-26T03:03:42.657Z" }, + { url = "https://files.pythonhosted.org/packages/19/3b/6c44fa59d6418f4239d5db8b1ece757351e85d6f3ca126dfe37d427020c8/propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", size = 218952, upload-time = "2025-03-26T03:03:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/4aeb95a1cd085e0558ab0de95abfc5187329616193a1012a6c4c930e9f7a/propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", size = 206593, upload-time = "2025-03-26T03:03:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/29fa75de1cbbb302f1e1d684009b969976ca603ee162282ae702287b6621/propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", size = 196745, upload-time = "2025-03-26T03:03:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/19/7e/2237dad1dbffdd2162de470599fa1a1d55df493b16b71e5d25a0ac1c1543/propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", size = 203369, upload-time = "2025-03-26T03:03:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/a4/bc/a82c5878eb3afb5c88da86e2cf06e1fe78b7875b26198dbb70fe50a010dc/propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", size = 198723, upload-time = "2025-03-26T03:03:51.091Z" }, + { url = "https://files.pythonhosted.org/packages/17/76/9632254479c55516f51644ddbf747a45f813031af5adcb8db91c0b824375/propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", size = 200751, upload-time = "2025-03-26T03:03:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c3/a90b773cf639bd01d12a9e20c95be0ae978a5a8abe6d2d343900ae76cd71/propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", size = 210730, upload-time = "2025-03-26T03:03:54.498Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ec/ad5a952cdb9d65c351f88db7c46957edd3d65ffeee72a2f18bd6341433e0/propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", size = 213499, upload-time = "2025-03-26T03:03:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/ea5133dda43e298cd2010ec05c2821b391e10980e64ee72c0a76cdbb813a/propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", size = 207132, upload-time = "2025-03-26T03:03:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243, upload-time = "2025-03-26T03:04:01.912Z" }, + { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503, upload-time = "2025-03-26T03:04:03.704Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934, upload-time = "2025-03-26T03:04:05.257Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633, upload-time = "2025-03-26T03:04:07.044Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124, upload-time = "2025-03-26T03:04:08.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283, upload-time = "2025-03-26T03:04:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498, upload-time = "2025-03-26T03:04:11.616Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486, upload-time = "2025-03-26T03:04:13.102Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675, upload-time = "2025-03-26T03:04:14.658Z" }, + { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727, upload-time = "2025-03-26T03:04:16.207Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878, upload-time = "2025-03-26T03:04:18.11Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558, upload-time = "2025-03-26T03:04:19.562Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754, upload-time = "2025-03-26T03:04:21.065Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088, upload-time = "2025-03-26T03:04:22.718Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430, upload-time = "2025-03-26T03:04:26.436Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637, upload-time = "2025-03-26T03:04:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123, upload-time = "2025-03-26T03:04:30.659Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031, upload-time = "2025-03-26T03:04:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100, upload-time = "2025-03-26T03:04:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170, upload-time = "2025-03-26T03:04:35.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000, upload-time = "2025-03-26T03:04:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262, upload-time = "2025-03-26T03:04:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772, upload-time = "2025-03-26T03:04:41.109Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133, upload-time = "2025-03-26T03:04:42.544Z" }, + { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741, upload-time = "2025-03-26T03:04:44.06Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047, upload-time = "2025-03-26T03:04:45.983Z" }, + { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467, upload-time = "2025-03-26T03:04:47.699Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022, upload-time = "2025-03-26T03:04:49.195Z" }, + { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865, upload-time = "2025-03-26T03:04:53.406Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452, upload-time = "2025-03-26T03:04:54.624Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800, upload-time = "2025-03-26T03:04:55.844Z" }, + { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804, upload-time = "2025-03-26T03:04:57.158Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650, upload-time = "2025-03-26T03:04:58.61Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235, upload-time = "2025-03-26T03:05:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249, upload-time = "2025-03-26T03:05:02.11Z" }, + { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964, upload-time = "2025-03-26T03:05:03.599Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501, upload-time = "2025-03-26T03:05:05.107Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917, upload-time = "2025-03-26T03:05:06.59Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089, upload-time = "2025-03-26T03:05:08.1Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102, upload-time = "2025-03-26T03:05:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122, upload-time = "2025-03-26T03:05:11.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818, upload-time = "2025-03-26T03:05:12.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613, upload-time = "2025-03-26T03:05:16.913Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763, upload-time = "2025-03-26T03:05:18.607Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175, upload-time = "2025-03-26T03:05:19.85Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265, upload-time = "2025-03-26T03:05:21.654Z" }, + { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412, upload-time = "2025-03-26T03:05:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290, upload-time = "2025-03-26T03:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926, upload-time = "2025-03-26T03:05:26.459Z" }, + { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808, upload-time = "2025-03-26T03:05:28.188Z" }, + { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916, upload-time = "2025-03-26T03:05:29.757Z" }, + { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661, upload-time = "2025-03-26T03:05:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384, upload-time = "2025-03-26T03:05:32.984Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420, upload-time = "2025-03-26T03:05:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880, upload-time = "2025-03-26T03:05:36.256Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407, upload-time = "2025-03-26T03:05:37.799Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, +] + [[package]] name = "psycopg" version = "3.2.12" @@ -2741,9 +3647,11 @@ name = "psycopg-c" version = "3.2.12" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] sdist = { url = "https://files.pythonhosted.org/packages/68/27/33699874745d7bb195e78fd0a97349908b64d3ec5fea7b8e5e52f56df04c/psycopg_c-3.2.12.tar.gz", hash = "sha256:1c80042067d5df90d184c6fbd58661350b3620f99d87a01c882953c4d5dfa52b", size = 608386, upload-time = "2025-10-26T00:46:08.727Z" } @@ -2812,6 +3720,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload-time = "2025-04-08T13:27:06.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload-time = "2025-04-08T13:27:03.789Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload-time = "2025-04-02T09:49:41.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021, upload-time = "2025-04-02T09:46:45.065Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742, upload-time = "2025-04-02T09:46:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414, upload-time = "2025-04-02T09:46:48.263Z" }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848, upload-time = "2025-04-02T09:46:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055, upload-time = "2025-04-02T09:46:50.602Z" }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806, upload-time = "2025-04-02T09:46:52.116Z" }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777, upload-time = "2025-04-02T09:46:53.675Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803, upload-time = "2025-04-02T09:46:55.789Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755, upload-time = "2025-04-02T09:46:56.956Z" }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358, upload-time = "2025-04-02T09:46:58.445Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916, upload-time = "2025-04-02T09:46:59.726Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224, upload-time = "2025-04-02T09:47:04.199Z" }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845, upload-time = "2025-04-02T09:47:05.686Z" }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029, upload-time = "2025-04-02T09:47:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784, upload-time = "2025-04-02T09:47:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075, upload-time = "2025-04-02T09:47:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849, upload-time = "2025-04-02T09:47:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794, upload-time = "2025-04-02T09:47:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237, upload-time = "2025-04-02T09:47:14.355Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351, upload-time = "2025-04-02T09:47:15.676Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914, upload-time = "2025-04-02T09:47:17Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385, upload-time = "2025-04-02T09:47:18.631Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640, upload-time = "2025-04-02T09:47:25.394Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649, upload-time = "2025-04-02T09:47:27.417Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472, upload-time = "2025-04-02T09:47:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509, upload-time = "2025-04-02T09:47:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702, upload-time = "2025-04-02T09:47:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428, upload-time = "2025-04-02T09:47:37.315Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753, upload-time = "2025-04-02T09:47:39.013Z" }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849, upload-time = "2025-04-02T09:47:40.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541, upload-time = "2025-04-02T09:47:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225, upload-time = "2025-04-02T09:47:43.425Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373, upload-time = "2025-04-02T09:47:44.979Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload-time = "2025-04-02T09:47:51.648Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload-time = "2025-04-02T09:47:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload-time = "2025-04-02T09:47:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload-time = "2025-04-02T09:47:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload-time = "2025-04-02T09:47:58.088Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload-time = "2025-04-02T09:47:59.591Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload-time = "2025-04-02T09:48:01.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload-time = "2025-04-02T09:48:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload-time = "2025-04-02T09:48:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload-time = "2025-04-02T09:48:06.226Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload-time = "2025-04-02T09:48:08.114Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload-time = "2025-04-02T09:48:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload-time = "2025-04-02T09:48:16.222Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659, upload-time = "2025-04-02T09:48:45.342Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294, upload-time = "2025-04-02T09:48:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771, upload-time = "2025-04-02T09:48:49.468Z" }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558, upload-time = "2025-04-02T09:48:51.409Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038, upload-time = "2025-04-02T09:48:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315, upload-time = "2025-04-02T09:48:55.555Z" }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063, upload-time = "2025-04-02T09:48:57.479Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631, upload-time = "2025-04-02T09:48:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858, upload-time = "2025-04-02T09:49:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745, upload-time = "2025-04-02T09:49:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188, upload-time = "2025-04-02T09:49:07.352Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479, upload-time = "2025-04-02T09:49:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415, upload-time = "2025-04-02T09:49:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623, upload-time = "2025-04-02T09:49:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175, upload-time = "2025-04-02T09:49:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674, upload-time = "2025-04-02T09:49:17.61Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -3085,10 +4081,12 @@ name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] dependencies = [ { name = "numpy", version = "2.3.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')" }, @@ -3564,6 +4562,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, ] +[[package]] +name = "safetensors" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210, upload-time = "2025-02-26T09:15:13.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917, upload-time = "2025-02-26T09:15:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419, upload-time = "2025-02-26T09:15:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493, upload-time = "2025-02-26T09:14:51.812Z" }, + { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400, upload-time = "2025-02-26T09:14:53.549Z" }, + { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891, upload-time = "2025-02-26T09:14:55.717Z" }, + { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694, upload-time = "2025-02-26T09:14:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642, upload-time = "2025-02-26T09:15:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241, upload-time = "2025-02-26T09:14:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001, upload-time = "2025-02-26T09:15:05.79Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013, upload-time = "2025-02-26T09:15:07.892Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687, upload-time = "2025-02-26T09:15:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147, upload-time = "2025-02-26T09:15:11.185Z" }, +] + [[package]] name = "scikit-learn" version = "1.7.2" @@ -3664,10 +4682,12 @@ name = "scipy" version = "1.16.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] dependencies = [ { name = "numpy", version = "2.3.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')" }, @@ -3764,6 +4784,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/cb/7dc739a484b1a17ccf92a23dfe558ae615c232bd81e78a72049c25d1ff66/selectolax-0.3.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:484274f73839f9a143f4c13ce1b0a0123b5d64be22f967a1dc202a9a78687d67", size = 5727944, upload-time = "2025-04-30T15:16:49.52Z" }, ] +[[package]] +name = "sentence-transformers" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pillow", 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.16.2", 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", 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 = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/84/b30d1b29ff58cfdff423e36a50efd622c8e31d7039b1a0d5e72066620da1/sentence_transformers-4.1.0.tar.gz", hash = "sha256:f125ffd1c727533e0eca5d4567de72f84728de8f7482834de442fd90c2c3d50b", size = 272420, upload-time = "2025-04-15T13:46:13.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/2d/1151b371f28caae565ad384fdc38198f1165571870217aedda230b9d7497/sentence_transformers-4.1.0-py3-none-any.whl", hash = "sha256:382a7f6be1244a100ce40495fb7523dbe8d71b3c10b299f81e6b735092b3b8ca", size = 345695, upload-time = "2025-04-15T13:46:12.44Z" }, +] + [[package]] name = "service-identity" version = "24.2.0" @@ -3874,6 +4914,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'win32' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'win32' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fa/8e8fd93684b04e65816be864bebf0000fe1602e5452d006f9acc5db14ce5/sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7", size = 2112843, upload-time = "2025-03-27T18:49:25.515Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/06992f78a9ce545dfd1fea3dd99262bec5221f6f9d2d2066c3e94662529f/sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758", size = 2104032, upload-time = "2025-03-27T18:49:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/92/ee/57dc77282e8be22d686bd4681825299aa1069bbe090564868ea270ed5214/sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af", size = 3086406, upload-time = "2025-03-27T18:44:25.302Z" }, + { url = "https://files.pythonhosted.org/packages/94/3f/ceb9ab214b2e42d2e74a9209b3a2f2f073504eee16cddd2df81feeb67c2f/sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1", size = 3094652, upload-time = "2025-03-27T18:55:16.174Z" }, + { url = "https://files.pythonhosted.org/packages/00/0a/3401232a5b6d91a2df16c1dc39c6504c54575744c2faafa1e5a50de96621/sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00", size = 3050503, upload-time = "2025-03-27T18:44:28.266Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/ea7171415ab131397f71a2673645c2fe29ebe9a93063d458eb89e42bf051/sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e", size = 3076011, upload-time = "2025-03-27T18:55:17.967Z" }, + { url = "https://files.pythonhosted.org/packages/77/7e/55044a9ec48c3249bb38d5faae93f09579c35e862bb318ebd1ed7a1994a5/sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e", size = 2114025, upload-time = "2025-03-27T18:49:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/77/0f/dcf7bba95f847aec72f638750747b12d37914f71c8cc7c133cf326ab945c/sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011", size = 2104419, upload-time = "2025-03-27T18:49:30.75Z" }, + { url = "https://files.pythonhosted.org/packages/75/70/c86a5c20715e4fe903dde4c2fd44fc7e7a0d5fb52c1b954d98526f65a3ea/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4", size = 3222720, upload-time = "2025-03-27T18:44:29.871Z" }, + { url = "https://files.pythonhosted.org/packages/12/cf/b891a8c1d0c27ce9163361664c2128c7a57de3f35000ea5202eb3a2917b7/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1", size = 3222682, upload-time = "2025-03-27T18:55:20.097Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/7709d8c8266953d945435a96b7f425ae4172a336963756b58e996fbef7f3/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51", size = 3159542, upload-time = "2025-03-27T18:44:31.333Z" }, + { url = "https://files.pythonhosted.org/packages/85/7e/717eaabaf0f80a0132dc2032ea8f745b7a0914451c984821a7c8737fb75a/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a", size = 3179864, upload-time = "2025-03-27T18:55:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/92/06/552c1f92e880b57d8b92ce6619bd569b25cead492389b1d84904b55989d8/sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d", size = 2112620, upload-time = "2025-03-27T18:40:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/a5bc6e76c34cebc071f758161dbe1453de8815ae6e662393910d3be6d70d/sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a", size = 2103004, upload-time = "2025-03-27T18:40:04.204Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fd/0e96c8e6767618ed1a06e4d7a167fe13734c2f8113c4cb704443e6783038/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d", size = 3252440, upload-time = "2025-03-27T18:51:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6a/eb82e45b15a64266a2917a6833b51a334ea3c1991728fd905bfccbf5cf63/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716", size = 3263277, upload-time = "2025-03-27T18:50:28.142Z" }, + { url = "https://files.pythonhosted.org/packages/45/97/ebe41ab4530f50af99e3995ebd4e0204bf1b0dc0930f32250dde19c389fe/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2", size = 3198591, upload-time = "2025-03-27T18:51:27.543Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1c/a569c1b2b2f5ac20ba6846a1321a2bf52e9a4061001f282bf1c5528dcd69/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191", size = 3225199, upload-time = "2025-03-27T18:50:30.069Z" }, + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload-time = "2025-03-27T18:40:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload-time = "2025-03-27T18:40:07.182Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload-time = "2025-03-27T18:51:29.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131, upload-time = "2025-03-27T18:50:31.616Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364, upload-time = "2025-03-27T18:51:31.336Z" }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482, upload-time = "2025-03-27T18:50:33.201Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload-time = "2025-03-27T18:40:43.796Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] + [[package]] name = "sqlparse" version = "0.5.3" @@ -3883,6 +4965,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, ] +[[package]] +name = "sympy" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196, upload-time = "2024-09-18T21:54:25.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483, upload-time = "2024-09-18T21:54:23.097Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + [[package]] name = "termcolor" version = "3.1.0" @@ -3915,6 +5018,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/31/002e0fa5bca67d6a19da8c294273486f6c46cbcc83d6879719a38a181461/tika_client-0.10.0-py3-none-any.whl", hash = "sha256:f5486cc884e4522575662aa295bda761bf9f101ac8d92840155b58ab8b96f6e2", size = 18237, upload-time = "2025-08-04T17:47:28.966Z" }, ] +[[package]] +name = "tiktoken" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f3/50ec5709fad61641e4411eb1b9ac55b99801d71f1993c29853f256c726c9/tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382", size = 1065770, upload-time = "2025-02-14T06:02:01.251Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f8/5a9560a422cf1755b6e0a9a436e14090eeb878d8ec0f80e0cd3d45b78bf4/tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108", size = 1009314, upload-time = "2025-02-14T06:02:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/3ed4cfff8f809cb902900ae686069e029db74567ee10d017cb254df1d598/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd", size = 1143140, upload-time = "2025-02-14T06:02:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/f1/95/cc2c6d79df8f113bdc6c99cdec985a878768120d87d839a34da4bd3ff90a/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de", size = 1197860, upload-time = "2025-02-14T06:02:06.268Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6c/9c1a4cc51573e8867c9381db1814223c09ebb4716779c7f845d48688b9c8/tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990", size = 1259661, upload-time = "2025-02-14T06:02:08.889Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987, upload-time = "2025-02-14T06:02:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155, upload-time = "2025-02-14T06:02:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898, upload-time = "2025-02-14T06:02:16.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535, upload-time = "2025-02-14T06:02:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548, upload-time = "2025-02-14T06:02:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919, upload-time = "2025-02-14T06:02:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877, upload-time = "2025-02-14T06:02:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095, upload-time = "2025-02-14T06:02:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649, upload-time = "2025-02-14T06:02:43Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465, upload-time = "2025-02-14T06:02:45.046Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -3948,6 +5106,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "torch" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux')" }, + { name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/c2/3fb87940fa160d956ee94d644d37b99a24b9c05a4222bf34f94c71880e28/torch-2.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9afea41b11e1a1ab1b258a5c31afbd646d6319042bfe4f231b408034b51128b", size = 99158447, upload-time = "2025-04-23T14:35:10.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/91d1de65573fce563f5284e69d9c56b57289625cffbbb6d533d5d56c36a5/torch-2.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b9960183b6e5b71239a3e6c883d8852c304e691c0b2955f7045e8a6d05b9183", size = 865164221, upload-time = "2025-04-23T14:33:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0b/b2b83f30b8e84a51bf4f96aa3f5f65fdf7c31c591cc519310942339977e2/torch-2.7.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:34e0168ed6de99121612d72224e59b2a58a83dae64999990eada7260c5dd582d", size = 68559462, upload-time = "2025-04-23T14:35:39.889Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/7378d16cc636697f2a94f791cb496939b60fb8580ddbbef22367db2c2274/torch-2.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b7813e904757b125faf1a9a3154e1d50381d539ced34da1992f52440567c156", size = 99159397, upload-time = "2025-04-23T14:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/87fcddd34df9f53880fa1f0c23af7b6b96c935856473faf3914323588c40/torch-2.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd5cfbb4c3bbadd57ad1b27d56a28008f8d8753733411a140fcfb84d7f933a25", size = 865183681, upload-time = "2025-04-23T14:34:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/85b56f7e2abcfa558c5fbf7b11eb02d78a4a63e6aeee2bbae3bb552abea5/torch-2.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0a8d43caa342b9986101ec5feb5bbf1d86570b5caa01e9cb426378311258fdde", size = 68569377, upload-time = "2025-04-23T14:35:20.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5e/ac759f4c0ab7c01feffa777bd68b43d2ac61560a9770eeac074b450f81d4/torch-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:36a6368c7ace41ad1c0f69f18056020b6a5ca47bedaca9a2f3b578f5a104c26c", size = 99013250, upload-time = "2025-04-23T14:35:15.589Z" }, + { url = "https://files.pythonhosted.org/packages/9c/58/2d245b6f1ef61cf11dfc4aceeaacbb40fea706ccebac3f863890c720ab73/torch-2.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15aab3e31c16feb12ae0a88dba3434a458874636f360c567caa6a91f6bfba481", size = 865042157, upload-time = "2025-04-23T14:32:56.011Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8d/b2939e5254be932db1a34b2bd099070c509e8887e0c5a90c498a917e4032/torch-2.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:30b7688a87239a7de83f269333651d8e582afffce6f591fff08c046f7787296e", size = 68574294, upload-time = "2025-04-23T14:34:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/14/24/720ea9a66c29151b315ea6ba6f404650834af57a26b2a04af23ec246b2d5/torch-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:868ccdc11798535b5727509480cd1d86d74220cfdc42842c4617338c1109a205", size = 99015553, upload-time = "2025-04-23T14:34:41.075Z" }, + { url = "https://files.pythonhosted.org/packages/4b/27/285a8cf12bd7cd71f9f211a968516b07dcffed3ef0be585c6e823675ab91/torch-2.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b52347118116cf3dff2ab5a3c3dd97c719eb924ac658ca2a7335652076df708", size = 865046389, upload-time = "2025-04-23T14:32:01.16Z" }, + { url = "https://files.pythonhosted.org/packages/28/fd/74ba6fde80e2b9eef4237fe668ffae302c76f0e4221759949a632ca13afa/torch-2.7.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:edad98dddd82220465b106506bb91ee5ce32bd075cddbcf2b443dfaa2cbd83bf", size = 68856166, upload-time = "2025-04-23T14:34:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b4/8df3f9fe6bdf59e56a0e538592c308d18638eb5f5dc4b08d02abb173c9f0/torch-2.7.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a885fc25afefb6e6eb18a7d1e8bfa01cc153e92271d980a49243b250d5ab6d9", size = 99091348, upload-time = "2025-04-23T14:33:48.975Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f5/0bd30e9da04c3036614aa1b935a9f7e505a9e4f1f731b15e165faf8a4c74/torch-2.7.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:176300ff5bc11a5f5b0784e40bde9e10a35c4ae9609beed96b4aeb46a27f5fae", size = 865104023, upload-time = "2025-04-23T14:30:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/7e6477cf40d48cc0a61fa0d41ee9582b9a316b12772fcac17bc1a40178e7/torch-2.7.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:27f5007bdf45f7bb7af7f11d1828d5c2487e030690afb3d89a651fd7036a390e", size = 68575074, upload-time = "2025-04-23T14:32:38.136Z" }, +] + [[package]] name = "tornado" version = "6.5.2" @@ -3973,6 +5177,43 @@ wheels = [ { 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]] +name = "transformers" +version = "4.51.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", 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 = "numpy", version = "2.3.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 = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, +] + +[[package]] +name = "triton" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/04/d54d3a6d077c646624dc9461b0059e23fd5d30e0dbe67471e3654aec81f9/triton-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fad99beafc860501d7fcc1fb7045d9496cbe2c882b1674640304949165a916e7", size = 156441993, upload-time = "2025-04-09T20:27:25.107Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c5/4874a81131cc9e934d88377fbc9d24319ae1fb540f3333b4e9c696ebc607/triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984", size = 156528461, upload-time = "2025-04-09T20:27:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/ce18470914ab6cfbec9384ee565d23c4d1c55f0548160b1c7b33000b11fd/triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3", size = 156504509, upload-time = "2025-04-09T20:27:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/7d/74/4bf2702b65e93accaa20397b74da46fb7a0356452c1bb94dbabaf0582930/triton-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47bc87ad66fa4ef17968299acacecaab71ce40a238890acc6ad197c3abe2b8f1", size = 156516468, upload-time = "2025-04-09T20:27:48.196Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/f28a696fa750b9b608baa236f8225dd3290e5aff27433b06143adc025961/triton-3.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce4700fc14032af1e049005ae94ba908e71cd6c2df682239aed08e49bc71b742", size = 156580729, upload-time = "2025-04-09T20:27:55.424Z" }, +] + [[package]] name = "twisted" version = "25.5.0" @@ -4183,6 +5424,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-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/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, +] + [[package]] name = "tzdata" version = "2025.2" @@ -4417,6 +5683,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "yarl" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258, upload-time = "2025-04-17T00:45:14.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/ab/66082639f99d7ef647a86b2ff4ca20f8ae13bd68a6237e6e166b8eb92edf/yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", size = 145054, upload-time = "2025-04-17T00:41:27.071Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c2/4e78185c453c3ca02bd11c7907394d0410d26215f9e4b7378648b3522a30/yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", size = 96811, upload-time = "2025-04-17T00:41:30.235Z" }, + { url = "https://files.pythonhosted.org/packages/c7/45/91e31dccdcf5b7232dcace78bd51a1bb2d7b4b96c65eece0078b620587d1/yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", size = 94566, upload-time = "2025-04-17T00:41:32.023Z" }, + { url = "https://files.pythonhosted.org/packages/c8/21/e0aa650bcee881fb804331faa2c0f9a5d6be7609970b2b6e3cdd414e174b/yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", size = 327297, upload-time = "2025-04-17T00:41:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/58f10870f5c17595c5a37da4c6a0b321589b7d7976e10570088d445d0f47/yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", size = 323578, upload-time = "2025-04-17T00:41:36.492Z" }, + { url = "https://files.pythonhosted.org/packages/07/df/2506b1382cc0c4bb0d22a535dc3e7ccd53da9a59b411079013a7904ac35c/yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", size = 343212, upload-time = "2025-04-17T00:41:38.396Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/d1c901d0e2158ad06bb0b9a92473e32d992f98673b93c8a06293e091bab0/yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", size = 337956, upload-time = "2025-04-17T00:41:40.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/10fcf7d86f49b1a11096d6846257485ef32e3d3d322e8a7fdea5b127880c/yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", size = 333889, upload-time = "2025-04-17T00:41:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cd/bae926a25154ba31c5fd15f2aa6e50a545c840e08d85e2e2e0807197946b/yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", size = 322282, upload-time = "2025-04-17T00:41:44.641Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/c3ac3597dfde746c63c637c5422cf3954ebf622a8de7f09892d20a68900d/yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", size = 336270, upload-time = "2025-04-17T00:41:46.812Z" }, + { url = "https://files.pythonhosted.org/packages/dd/42/417fd7b8da5846def29712370ea8916a4be2553de42a2c969815153717be/yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", size = 335500, upload-time = "2025-04-17T00:41:48.896Z" }, + { url = "https://files.pythonhosted.org/packages/37/aa/c2339683f8f05f4be16831b6ad58d04406cf1c7730e48a12f755da9f5ac5/yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", size = 339672, upload-time = "2025-04-17T00:41:50.965Z" }, + { url = "https://files.pythonhosted.org/packages/be/12/ab6c4df95f00d7bc9502bf07a92d5354f11d9d3cb855222a6a8d2bd6e8da/yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", size = 351840, upload-time = "2025-04-17T00:41:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/83/3c/08d58c51bbd3899be3e7e83cd7a691fdcf3b9f78b8699d663ecc2c090ab7/yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", size = 359550, upload-time = "2025-04-17T00:41:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8a/15/de7906c506f85fb476f0edac4bd74569f49e5ffdcf98e246a0313bf593b9/yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", size = 351108, upload-time = "2025-04-17T00:41:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/60/82/a59d8e21b20ffc836775fa7daedac51d16bb8f3010c4fcb495c4496aa922/yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3", size = 145178, upload-time = "2025-04-17T00:42:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/ba/81/315a3f6f95947cfbf37c92d6fbce42a1a6207b6c38e8c2b452499ec7d449/yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", size = 96859, upload-time = "2025-04-17T00:42:06.43Z" }, + { url = "https://files.pythonhosted.org/packages/ad/17/9b64e575583158551b72272a1023cdbd65af54fe13421d856b2850a6ddb7/yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", size = 94647, upload-time = "2025-04-17T00:42:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/2c/29/8f291e7922a58a21349683f6120a85701aeefaa02e9f7c8a2dc24fe3f431/yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", size = 355788, upload-time = "2025-04-17T00:42:09.902Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/b4892c80b805c42c228c6d11e03cafabf81662d371b0853e7f0f513837d5/yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", size = 344613, upload-time = "2025-04-17T00:42:11.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0e/517aa28d3f848589bae9593717b063a544b86ba0a807d943c70f48fcf3bb/yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", size = 370953, upload-time = "2025-04-17T00:42:13.983Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/5bd09d2f1ad6e6f7c2beae9e50db78edd2cca4d194d227b958955573e240/yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", size = 369204, upload-time = "2025-04-17T00:42:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/9c/85/d793a703cf4bd0d4cd04e4b13cc3d44149470f790230430331a0c1f52df5/yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", size = 358108, upload-time = "2025-04-17T00:42:18.622Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/b6c71e13549c1f6048fbc14ce8d930ac5fb8bafe4f1a252e621a24f3f1f9/yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", size = 346610, upload-time = "2025-04-17T00:42:20.9Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1a/d6087d58bdd0d8a2a37bbcdffac9d9721af6ebe50d85304d9f9b57dfd862/yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", size = 365378, upload-time = "2025-04-17T00:42:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/02/84/e25ddff4cbc001dbc4af76f8d41a3e23818212dd1f0a52044cbc60568872/yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", size = 356919, upload-time = "2025-04-17T00:42:25.145Z" }, + { url = "https://files.pythonhosted.org/packages/04/76/898ae362353bf8f64636495d222c8014c8e5267df39b1a9fe1e1572fb7d0/yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", size = 364248, upload-time = "2025-04-17T00:42:27.475Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b0/9d9198d83a622f1c40fdbf7bd13b224a6979f2e1fc2cf50bfb1d8773c495/yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", size = 378418, upload-time = "2025-04-17T00:42:29.333Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ce/1f50c1cc594cf5d3f5bf4a9b616fca68680deaec8ad349d928445ac52eb8/yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", size = 383850, upload-time = "2025-04-17T00:42:31.668Z" }, + { url = "https://files.pythonhosted.org/packages/89/1e/a59253a87b35bfec1a25bb5801fb69943330b67cfd266278eb07e0609012/yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", size = 381218, upload-time = "2025-04-17T00:42:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089, upload-time = "2025-04-17T00:42:39.602Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706, upload-time = "2025-04-17T00:42:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719, upload-time = "2025-04-17T00:42:43.666Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972, upload-time = "2025-04-17T00:42:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639, upload-time = "2025-04-17T00:42:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745, upload-time = "2025-04-17T00:42:49.406Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178, upload-time = "2025-04-17T00:42:51.588Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219, upload-time = "2025-04-17T00:42:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266, upload-time = "2025-04-17T00:42:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873, upload-time = "2025-04-17T00:42:57.895Z" }, + { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524, upload-time = "2025-04-17T00:43:00.094Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370, upload-time = "2025-04-17T00:43:02.242Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297, upload-time = "2025-04-17T00:43:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771, upload-time = "2025-04-17T00:43:06.609Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000, upload-time = "2025-04-17T00:43:09.01Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/514c9bff2900c22a4f10e06297714dbaf98707143b37ff0bcba65a956221/yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", size = 145030, upload-time = "2025-04-17T00:43:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/f88da3fa319b8c9c813389bfb3463e8d777c62654c7168e580a13fadff05/yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", size = 96894, upload-time = "2025-04-17T00:43:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/cd/57/92e83538580a6968b2451d6c89c5579938a7309d4785748e8ad42ddafdce/yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", size = 94457, upload-time = "2025-04-17T00:43:19.431Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ee/7ee43bd4cf82dddd5da97fcaddb6fa541ab81f3ed564c42f146c83ae17ce/yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", size = 343070, upload-time = "2025-04-17T00:43:21.426Z" }, + { url = "https://files.pythonhosted.org/packages/4a/12/b5eccd1109e2097bcc494ba7dc5de156e41cf8309fab437ebb7c2b296ce3/yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", size = 337739, upload-time = "2025-04-17T00:43:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/0eade8e49af9fc2585552f63c76fa59ef469c724cc05b29519b19aa3a6d5/yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", size = 351338, upload-time = "2025-04-17T00:43:25.695Z" }, + { url = "https://files.pythonhosted.org/packages/45/cb/aaaa75d30087b5183c7b8a07b4fb16ae0682dd149a1719b3a28f54061754/yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", size = 353636, upload-time = "2025-04-17T00:43:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/d9cb39ec68a91ba6e66fa86d97003f58570327d6713833edf7ad6ce9dde5/yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", size = 348061, upload-time = "2025-04-17T00:43:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/72/6b/103940aae893d0cc770b4c36ce80e2ed86fcb863d48ea80a752b8bda9303/yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", size = 334150, upload-time = "2025-04-17T00:43:31.742Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/986bd82aa222c3e6b211a69c9081ba46484cffa9fab2a5235e8d18ca7a27/yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", size = 362207, upload-time = "2025-04-17T00:43:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/14/7c/63f5922437b873795d9422cbe7eb2509d4b540c37ae5548a4bb68fd2c546/yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", size = 361277, upload-time = "2025-04-17T00:43:36.202Z" }, + { url = "https://files.pythonhosted.org/packages/81/83/450938cccf732466953406570bdb42c62b5ffb0ac7ac75a1f267773ab5c8/yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", size = 364990, upload-time = "2025-04-17T00:43:38.551Z" }, + { url = "https://files.pythonhosted.org/packages/b4/de/af47d3a47e4a833693b9ec8e87debb20f09d9fdc9139b207b09a3e6cbd5a/yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", size = 374684, upload-time = "2025-04-17T00:43:40.481Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/078bcc2d539f1faffdc7d32cb29a2d7caa65f1a6f7e40795d8485db21851/yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", size = 382599, upload-time = "2025-04-17T00:43:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/74/a9/4fdb1a7899f1fb47fd1371e7ba9e94bff73439ce87099d5dd26d285fffe0/yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", size = 378573, upload-time = "2025-04-17T00:43:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2f/422546794196519152fc2e2f475f0e1d4d094a11995c81a465faf5673ffd/yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", size = 163575, upload-time = "2025-04-17T00:43:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/90/fc/67c64ddab6c0b4a169d03c637fb2d2a212b536e1989dec8e7e2c92211b7f/yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", size = 106121, upload-time = "2025-04-17T00:43:53.506Z" }, + { url = "https://files.pythonhosted.org/packages/6d/00/29366b9eba7b6f6baed7d749f12add209b987c4cfbfa418404dbadc0f97c/yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", size = 103815, upload-time = "2025-04-17T00:43:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/28/f4/a2a4c967c8323c03689383dff73396281ced3b35d0ed140580825c826af7/yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", size = 408231, upload-time = "2025-04-17T00:43:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a1/66f7ffc0915877d726b70cc7a896ac30b6ac5d1d2760613603b022173635/yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", size = 390221, upload-time = "2025-04-17T00:44:00.526Z" }, + { url = "https://files.pythonhosted.org/packages/41/15/cc248f0504610283271615e85bf38bc014224122498c2016d13a3a1b8426/yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", size = 411400, upload-time = "2025-04-17T00:44:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/f0823d7e092bfb97d24fce6c7269d67fcd1aefade97d0a8189c4452e4d5e/yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", size = 411714, upload-time = "2025-04-17T00:44:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/70/be418329eae64b9f1b20ecdaac75d53aef098797d4c2299d82ae6f8e4663/yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", size = 404279, upload-time = "2025-04-17T00:44:07.721Z" }, + { url = "https://files.pythonhosted.org/packages/19/f5/52e02f0075f65b4914eb890eea1ba97e6fd91dd821cc33a623aa707b2f67/yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", size = 384044, upload-time = "2025-04-17T00:44:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/6a/36/b0fa25226b03d3f769c68d46170b3e92b00ab3853d73127273ba22474697/yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", size = 416236, upload-time = "2025-04-17T00:44:11.734Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3a/54c828dd35f6831dfdd5a79e6c6b4302ae2c5feca24232a83cb75132b205/yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", size = 402034, upload-time = "2025-04-17T00:44:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/c7bf5fba488f7e049f9ad69c1b8fdfe3daa2e8916b3d321aa049e361a55a/yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", size = 407943, upload-time = "2025-04-17T00:44:16.052Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/022d2555c1e8fcff08ad7f0f43e4df3aba34f135bff04dd35d5526ce54ab/yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", size = 423058, upload-time = "2025-04-17T00:44:18.547Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f6/0873a05563e5df29ccf35345a6ae0ac9e66588b41fdb7043a65848f03139/yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", size = 423792, upload-time = "2025-04-17T00:44:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/9e/35/43fbbd082708fa42e923f314c24f8277a28483d219e049552e5007a9aaca/yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", size = 422242, upload-time = "2025-04-17T00:44:22.851Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload-time = "2025-04-17T00:45:12.199Z" }, +] + [[package]] name = "zope-interface" version = "8.0.1" @@ -4526,9 +5881,11 @@ name = "zxing-cpp" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] sdist = { url = "https://files.pythonhosted.org/packages/d9/f2/b781bf6119abe665069777e3c0f154752cf924fe8a55fca027243abbc555/zxing_cpp-2.3.0.tar.gz", hash = "sha256:3babedb67a4c15c9de2c2b4c42d70af83a6c85780c1b2d9803ac64c6ae69f14e", size = 1172666, upload-time = "2025-01-01T21:54:05.856Z" } From fb82146c10ccdd647e14f3a4c042a65ee60c564f Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:27:12 +0000 Subject: [PATCH 05/57] Auto translate strings --- src-ui/messages.xlf | 495 +++++++++++++++--------- src/locale/en_US/LC_MESSAGES/django.po | 516 ++++++++++++++----------- 2 files changed, 597 insertions(+), 414 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index a27f4f72e..5cab6203c 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -297,11 +297,11 @@ src/app/components/app-frame/app-frame.component.html - 84 + 87 src/app/components/app-frame/app-frame.component.html - 86 + 89 src/app/components/dashboard/dashboard.component.html @@ -316,11 +316,11 @@ src/app/components/app-frame/app-frame.component.html - 91 + 94 src/app/components/app-frame/app-frame.component.html - 93 + 96 src/app/components/document-list/document-list.component.ts @@ -359,15 +359,15 @@ src/app/components/app-frame/app-frame.component.html - 51 + 54 src/app/components/app-frame/app-frame.component.html - 255 + 258 src/app/components/app-frame/app-frame.component.html - 257 + 260 @@ -385,7 +385,7 @@ src/app/components/document-detail/document-detail.component.html - 119 + 109 @@ -530,18 +530,18 @@ Discard src/app/components/admin/config/config.component.html - 53 + 57 src/app/components/document-detail/document-detail.component.html - 380 + 396 Save src/app/components/admin/config/config.component.html - 56 + 60 src/app/components/admin/settings/settings.component.html @@ -593,7 +593,7 @@ src/app/components/document-detail/document-detail.component.html - 373 + 389 src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html @@ -612,42 +612,42 @@ Error retrieving config src/app/components/admin/config/config.component.ts - 103 + 105 Invalid JSON src/app/components/admin/config/config.component.ts - 129 + 131 Configuration updated src/app/components/admin/config/config.component.ts - 173 + 175 An error occurred updating configuration src/app/components/admin/config/config.component.ts - 178 + 180 File successfully updated src/app/components/admin/config/config.component.ts - 200 + 202 An error occurred uploading file src/app/components/admin/config/config.component.ts - 205 + 207 @@ -658,11 +658,11 @@ src/app/components/app-frame/app-frame.component.html - 290 + 293 src/app/components/app-frame/app-frame.component.html - 293 + 296 @@ -761,7 +761,7 @@ src/app/components/document-detail/document-detail.component.html - 393 + 409 src/app/components/document-list/document-list.component.html @@ -1032,11 +1032,11 @@ src/app/components/app-frame/app-frame.component.html - 215 + 218 src/app/components/app-frame/app-frame.component.html - 217 + 220 src/app/components/manage/saved-views/saved-views.component.html @@ -1234,7 +1234,7 @@ src/app/components/document-detail/document-detail.component.html - 349 + 365 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1601,7 +1601,7 @@ src/app/components/app-frame/app-frame.component.ts - 180 + 182 @@ -1612,11 +1612,11 @@ src/app/components/app-frame/app-frame.component.html - 278 + 281 src/app/components/app-frame/app-frame.component.html - 280 + 283 @@ -2028,11 +2028,11 @@ src/app/components/app-frame/app-frame.component.html - 238 + 241 src/app/components/app-frame/app-frame.component.html - 241 + 244 @@ -2397,11 +2397,11 @@ src/app/components/app-frame/app-frame.component.html - 269 + 272 src/app/components/app-frame/app-frame.component.html - 271 + 274 @@ -2607,11 +2607,11 @@ src/app/components/document-detail/document-detail.component.ts - 1029 + 1098 src/app/components/document-detail/document-detail.component.ts - 1394 + 1463 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -2713,83 +2713,83 @@ Logged in as src/app/components/app-frame/app-frame.component.html - 43 + 46 My Profile src/app/components/app-frame/app-frame.component.html - 47 + 50 Logout src/app/components/app-frame/app-frame.component.html - 54 + 57 Documentation src/app/components/app-frame/app-frame.component.html - 59 - - - src/app/components/app-frame/app-frame.component.html - 299 + 62 src/app/components/app-frame/app-frame.component.html 302 + + src/app/components/app-frame/app-frame.component.html + 305 + Saved views src/app/components/app-frame/app-frame.component.html - 101 + 104 src/app/components/app-frame/app-frame.component.html - 106 + 109 Open documents src/app/components/app-frame/app-frame.component.html - 141 + 144 Close all src/app/components/app-frame/app-frame.component.html - 161 + 164 src/app/components/app-frame/app-frame.component.html - 163 + 166 Manage src/app/components/app-frame/app-frame.component.html - 172 + 175 Correspondents src/app/components/app-frame/app-frame.component.html - 178 + 181 src/app/components/app-frame/app-frame.component.html - 180 + 183 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2800,11 +2800,11 @@ Tags src/app/components/app-frame/app-frame.component.html - 185 + 188 src/app/components/app-frame/app-frame.component.html - 188 + 191 src/app/components/common/input/tags/tags.component.ts @@ -2835,11 +2835,11 @@ Document Types src/app/components/app-frame/app-frame.component.html - 194 + 197 src/app/components/app-frame/app-frame.component.html - 196 + 199 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2850,11 +2850,11 @@ Storage Paths src/app/components/app-frame/app-frame.component.html - 201 + 204 src/app/components/app-frame/app-frame.component.html - 203 + 206 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2865,11 +2865,11 @@ Custom Fields src/app/components/app-frame/app-frame.component.html - 208 + 211 src/app/components/app-frame/app-frame.component.html - 210 + 213 src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -2884,11 +2884,11 @@ Workflows src/app/components/app-frame/app-frame.component.html - 224 + 227 src/app/components/app-frame/app-frame.component.html - 226 + 229 src/app/components/manage/workflows/workflows.component.html @@ -2899,92 +2899,92 @@ Mail src/app/components/app-frame/app-frame.component.html - 231 + 234 src/app/components/app-frame/app-frame.component.html - 234 + 237 Administration src/app/components/app-frame/app-frame.component.html - 249 + 252 Configuration src/app/components/app-frame/app-frame.component.html - 262 + 265 src/app/components/app-frame/app-frame.component.html - 264 + 267 GitHub src/app/components/app-frame/app-frame.component.html - 309 + 312 is available. src/app/components/app-frame/app-frame.component.html - 318,319 + 321,322 Click to view. src/app/components/app-frame/app-frame.component.html - 319 + 322 Paperless-ngx can automatically check for updates src/app/components/app-frame/app-frame.component.html - 323 + 326 How does this work? src/app/components/app-frame/app-frame.component.html - 330,332 + 333,335 Update available src/app/components/app-frame/app-frame.component.html - 343 + 346 Sidebar views updated src/app/components/app-frame/app-frame.component.ts - 264 + 270 Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 267 + 273 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 288 + 294 @@ -3186,6 +3186,20 @@ 20 + + Ask a question about this document... + + src/app/components/chat/chat/chat.component.ts + 37 + + + + Ask a question about a document... + + src/app/components/chat/chat/chat.component.ts + 38 + + Clear @@ -3223,7 +3237,7 @@ src/app/components/document-detail/document-detail.component.ts - 982 + 1051 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -3328,7 +3342,7 @@ src/app/components/document-detail/document-detail.component.ts - 1445 + 1514 @@ -3339,7 +3353,7 @@ src/app/components/document-detail/document-detail.component.ts - 1446 + 1515 @@ -3350,7 +3364,7 @@ src/app/components/document-detail/document-detail.component.ts - 1447 + 1516 @@ -3474,7 +3488,7 @@ src/app/components/document-detail/document-detail.component.html - 113 + 103 src/app/guards/dirty-saved-view.guard.ts @@ -4247,6 +4261,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 264 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 302 + src/app/components/common/toast/toast.component.html 30 @@ -4440,7 +4458,7 @@ src/app/components/document-detail/document-detail.component.html - 315 + 331 @@ -4551,7 +4569,7 @@ src/app/components/document-detail/document-detail.component.html - 98 + 88 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -5626,7 +5644,7 @@ Show password src/app/components/common/input/password/password.component.html - 6 + 12 @@ -5709,6 +5727,13 @@ 55 + + Suggestion: + + src/app/components/common/input/text/text.component.html + 20 + + Read more @@ -5986,7 +6011,7 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html - 284 + 321 src/app/components/manage/mail/mail.component.html @@ -6271,7 +6296,7 @@ src/app/components/document-detail/document-detail.component.html - 94 + 84 @@ -6302,6 +6327,42 @@ 159 + + Suggest + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 8 + + + + Show suggestions + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 17 + + + + No novel suggestions + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 24 + + + + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 30 + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 36 + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 42 + + Environment @@ -6458,6 +6519,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 245 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 293 + Last Updated @@ -6493,6 +6558,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 252 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 300 + WebSocket Connection @@ -6508,6 +6577,13 @@ 261 + + AI Index + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 270 + + Copy Raw Error @@ -6867,7 +6943,7 @@ src/app/components/document-detail/document-detail.component.ts - 1393 + 1462 @@ -6881,28 +6957,28 @@ Send src/app/components/document-detail/document-detail.component.html - 90 + 80 Previous src/app/components/document-detail/document-detail.component.html - 116 + 106 Details src/app/components/document-detail/document-detail.component.html - 129 + 145 Title src/app/components/document-detail/document-detail.component.html - 132 + 148 src/app/components/document-list/document-list.component.html @@ -6925,21 +7001,21 @@ Archive serial number src/app/components/document-detail/document-detail.component.html - 133 + 149 Date created src/app/components/document-detail/document-detail.component.html - 134 + 150 Correspondent src/app/components/document-detail/document-detail.component.html - 136 + 152 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6966,7 +7042,7 @@ Document type src/app/components/document-detail/document-detail.component.html - 138 + 154 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6993,7 +7069,7 @@ Storage path src/app/components/document-detail/document-detail.component.html - 140 + 156 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -7016,7 +7092,7 @@ Default src/app/components/document-detail/document-detail.component.html - 141 + 157 src/app/components/manage/saved-views/saved-views.component.html @@ -7027,14 +7103,14 @@ Content src/app/components/document-detail/document-detail.component.html - 245 + 261 Metadata src/app/components/document-detail/document-detail.component.html - 254 + 270 src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts @@ -7045,175 +7121,175 @@ Date modified src/app/components/document-detail/document-detail.component.html - 261 + 277 Date added src/app/components/document-detail/document-detail.component.html - 265 + 281 Media filename src/app/components/document-detail/document-detail.component.html - 269 + 285 Original filename src/app/components/document-detail/document-detail.component.html - 273 + 289 Original MD5 checksum src/app/components/document-detail/document-detail.component.html - 277 + 293 Original file size src/app/components/document-detail/document-detail.component.html - 281 + 297 Original mime type src/app/components/document-detail/document-detail.component.html - 285 + 301 Archive MD5 checksum src/app/components/document-detail/document-detail.component.html - 290 + 306 Archive file size src/app/components/document-detail/document-detail.component.html - 296 + 312 Original document metadata src/app/components/document-detail/document-detail.component.html - 305 + 321 Archived document metadata src/app/components/document-detail/document-detail.component.html - 308 + 324 Notes src/app/components/document-detail/document-detail.component.html - 327,330 + 343,346 History src/app/components/document-detail/document-detail.component.html - 338 + 354 Save & next src/app/components/document-detail/document-detail.component.html - 375 + 391 Save & close src/app/components/document-detail/document-detail.component.html - 378 + 394 Document loading... src/app/components/document-detail/document-detail.component.html - 388 + 404 Enter Password src/app/components/document-detail/document-detail.component.html - 442 + 458 An error occurred loading content: src/app/components/document-detail/document-detail.component.ts - 417,419 + 430,432 Document changes detected src/app/components/document-detail/document-detail.component.ts - 451 + 464 The version of this document in your browser session appears older than the existing version. src/app/components/document-detail/document-detail.component.ts - 452 + 465 Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document. src/app/components/document-detail/document-detail.component.ts - 453 + 466 Ok src/app/components/document-detail/document-detail.component.ts - 455 + 468 Next document src/app/components/document-detail/document-detail.component.ts - 581 + 594 Previous document src/app/components/document-detail/document-detail.component.ts - 591 + 604 Close document src/app/components/document-detail/document-detail.component.ts - 599 + 612 src/app/services/open-documents.service.ts @@ -7224,67 +7300,67 @@ Save document src/app/components/document-detail/document-detail.component.ts - 606 + 619 Save and close / next src/app/components/document-detail/document-detail.component.ts - 615 + 628 Error retrieving metadata src/app/components/document-detail/document-detail.component.ts - 670 + 683 Error retrieving suggestions. src/app/components/document-detail/document-detail.component.ts - 699 + 731 Document "" saved successfully. src/app/components/document-detail/document-detail.component.ts - 871 + 940 src/app/components/document-detail/document-detail.component.ts - 895 + 964 Error saving document "" src/app/components/document-detail/document-detail.component.ts - 901 + 970 Error saving document src/app/components/document-detail/document-detail.component.ts - 951 + 1020 Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts - 983 + 1052 Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts - 984 + 1053 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -7295,7 +7371,7 @@ Move to trash src/app/components/document-detail/document-detail.component.ts - 986 + 1055 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -7306,14 +7382,14 @@ Error deleting document src/app/components/document-detail/document-detail.component.ts - 1005 + 1074 Reprocess confirm src/app/components/document-detail/document-detail.component.ts - 1025 + 1094 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -7324,102 +7400,102 @@ This operation will permanently recreate the archive file for this document. src/app/components/document-detail/document-detail.component.ts - 1026 + 1095 The archive file will be re-generated with the current settings. src/app/components/document-detail/document-detail.component.ts - 1027 + 1096 Reprocess operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 1037 + 1106 Error executing operation src/app/components/document-detail/document-detail.component.ts - 1048 + 1117 Error downloading document src/app/components/document-detail/document-detail.component.ts - 1097 + 1166 Page Fit src/app/components/document-detail/document-detail.component.ts - 1174 + 1243 PDF edit operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1412 + 1481 Error executing PDF edit operation src/app/components/document-detail/document-detail.component.ts - 1424 + 1493 Please enter the current password before attempting to remove it. src/app/components/document-detail/document-detail.component.ts - 1435 + 1504 Password removal operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1467 + 1536 Error executing password removal operation src/app/components/document-detail/document-detail.component.ts - 1481 + 1550 Print failed. src/app/components/document-detail/document-detail.component.ts - 1518 + 1587 Error loading document for printing. src/app/components/document-detail/document-detail.component.ts - 1530 + 1599 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1595 + 1664 src/app/components/document-detail/document-detail.component.ts - 1599 + 1668 @@ -8224,7 +8300,7 @@ src/app/data/paperless-config.ts - 91 + 104 @@ -9622,196 +9698,259 @@ General Settings src/app/data/paperless-config.ts - 50 + 51 OCR Settings src/app/data/paperless-config.ts - 51 + 52 Barcode Settings src/app/data/paperless-config.ts - 52 + 53 + + + + AI Settings + + src/app/data/paperless-config.ts + 54 Output Type src/app/data/paperless-config.ts - 76 + 89 Language src/app/data/paperless-config.ts - 84 + 97 Mode src/app/data/paperless-config.ts - 98 + 111 Skip Archive File src/app/data/paperless-config.ts - 106 + 119 Image DPI src/app/data/paperless-config.ts - 114 + 127 Clean src/app/data/paperless-config.ts - 121 + 134 Deskew src/app/data/paperless-config.ts - 129 + 142 Rotate Pages src/app/data/paperless-config.ts - 136 + 149 Rotate Pages Threshold src/app/data/paperless-config.ts - 143 + 156 Max Image Pixels src/app/data/paperless-config.ts - 150 + 163 Color Conversion Strategy src/app/data/paperless-config.ts - 157 + 170 OCR Arguments src/app/data/paperless-config.ts - 165 + 178 Application Logo src/app/data/paperless-config.ts - 172 + 185 Application Title src/app/data/paperless-config.ts - 179 + 192 Enable Barcodes src/app/data/paperless-config.ts - 186 + 199 Enable TIFF Support src/app/data/paperless-config.ts - 193 + 206 Barcode String src/app/data/paperless-config.ts - 200 + 213 Retain Split Pages src/app/data/paperless-config.ts - 207 + 220 Enable ASN src/app/data/paperless-config.ts - 214 + 227 ASN Prefix src/app/data/paperless-config.ts - 221 + 234 Upscale src/app/data/paperless-config.ts - 228 + 241 DPI src/app/data/paperless-config.ts - 235 + 248 Max Pages src/app/data/paperless-config.ts - 242 + 255 Enable Tag Detection src/app/data/paperless-config.ts - 249 + 262 Tag Mapping src/app/data/paperless-config.ts - 256 + 269 + + + + AI Enabled + + src/app/data/paperless-config.ts + 276 + + + + Consider privacy implications when enabling AI features, especially if using a remote model. + + src/app/data/paperless-config.ts + 280 + + + + LLM Embedding Backend + + src/app/data/paperless-config.ts + 284 + + + + LLM Embedding Model + + src/app/data/paperless-config.ts + 292 + + + + LLM Backend + + src/app/data/paperless-config.ts + 299 + + + + LLM Model + + src/app/data/paperless-config.ts + 307 + + + + LLM API Key + + src/app/data/paperless-config.ts + 314 + + + + LLM Endpoint + + src/app/data/paperless-config.ts + 321 diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 29c8ccaeb..3c3988307 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-12 21:04+0000\n" +"POT-Creation-Date: 2026-01-13 16:26+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -57,31 +57,31 @@ msgstr "" msgid "Custom field not found" msgstr "" -#: documents/models.py:38 documents/models.py:768 +#: documents/models.py:38 documents/models.py:769 msgid "owner" msgstr "" -#: documents/models.py:55 documents/models.py:983 +#: documents/models.py:55 documents/models.py:984 msgid "None" msgstr "" -#: documents/models.py:56 documents/models.py:984 +#: documents/models.py:56 documents/models.py:985 msgid "Any word" msgstr "" -#: documents/models.py:57 documents/models.py:985 +#: documents/models.py:57 documents/models.py:986 msgid "All words" msgstr "" -#: documents/models.py:58 documents/models.py:986 +#: documents/models.py:58 documents/models.py:987 msgid "Exact match" msgstr "" -#: documents/models.py:59 documents/models.py:987 +#: documents/models.py:59 documents/models.py:988 msgid "Regular expression" msgstr "" -#: documents/models.py:60 documents/models.py:988 +#: documents/models.py:60 documents/models.py:989 msgid "Fuzzy word" msgstr "" @@ -89,20 +89,20 @@ msgstr "" msgid "Automatic" msgstr "" -#: documents/models.py:64 documents/models.py:456 documents/models.py:1526 +#: documents/models.py:64 documents/models.py:456 documents/models.py:1527 #: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" -#: documents/models.py:66 documents/models.py:1052 +#: documents/models.py:66 documents/models.py:1053 msgid "match" msgstr "" -#: documents/models.py:69 documents/models.py:1055 +#: documents/models.py:69 documents/models.py:1056 msgid "matching algorithm" msgstr "" -#: documents/models.py:74 documents/models.py:1060 +#: documents/models.py:74 documents/models.py:1061 msgid "is insensitive" msgstr "" @@ -176,7 +176,7 @@ msgstr "" msgid "title" msgstr "" -#: documents/models.py:194 documents/models.py:682 +#: documents/models.py:194 documents/models.py:683 msgid "content" msgstr "" @@ -214,8 +214,8 @@ msgstr "" msgid "The number of pages of the document." msgstr "" -#: documents/models.py:241 documents/models.py:688 documents/models.py:726 -#: documents/models.py:798 documents/models.py:857 +#: documents/models.py:241 documents/models.py:689 documents/models.py:727 +#: documents/models.py:799 documents/models.py:858 msgid "created" msgstr "" @@ -263,8 +263,8 @@ msgstr "" msgid "The position of this document in your physical document archive." msgstr "" -#: documents/models.py:318 documents/models.py:699 documents/models.py:753 -#: documents/models.py:1569 +#: documents/models.py:318 documents/models.py:700 documents/models.py:754 +#: documents/models.py:1570 msgid "document" msgstr "" @@ -288,11 +288,11 @@ msgstr "" msgid "Title" msgstr "" -#: documents/models.py:443 documents/models.py:1004 +#: documents/models.py:443 documents/models.py:1005 msgid "Created" msgstr "" -#: documents/models.py:444 documents/models.py:1003 +#: documents/models.py:444 documents/models.py:1004 msgid "Added" msgstr "" @@ -604,618 +604,622 @@ msgstr "" msgid "Index Optimize" msgstr "" -#: documents/models.py:605 -msgid "Task ID" +#: documents/models.py:601 +msgid "LLM Index Update" msgstr "" #: documents/models.py:606 +msgid "Task ID" +msgstr "" + +#: documents/models.py:607 msgid "Celery ID for the Task that was run" msgstr "" -#: documents/models.py:611 +#: documents/models.py:612 msgid "Acknowledged" msgstr "" -#: documents/models.py:612 +#: documents/models.py:613 msgid "If the task is acknowledged via the frontend or API" msgstr "" -#: documents/models.py:618 +#: documents/models.py:619 msgid "Task Filename" msgstr "" -#: documents/models.py:619 +#: documents/models.py:620 msgid "Name of the file which the Task was run for" msgstr "" -#: documents/models.py:626 +#: documents/models.py:627 msgid "Task Name" msgstr "" -#: documents/models.py:627 +#: documents/models.py:628 msgid "Name of the task that was run" msgstr "" -#: documents/models.py:634 +#: documents/models.py:635 msgid "Task State" msgstr "" -#: documents/models.py:635 +#: documents/models.py:636 msgid "Current state of the task being run" msgstr "" -#: documents/models.py:641 +#: documents/models.py:642 msgid "Created DateTime" msgstr "" -#: documents/models.py:642 +#: documents/models.py:643 msgid "Datetime field when the task result was created in UTC" msgstr "" -#: documents/models.py:648 +#: documents/models.py:649 msgid "Started DateTime" msgstr "" -#: documents/models.py:649 +#: documents/models.py:650 msgid "Datetime field when the task was started in UTC" msgstr "" -#: documents/models.py:655 +#: documents/models.py:656 msgid "Completed DateTime" msgstr "" -#: documents/models.py:656 +#: documents/models.py:657 msgid "Datetime field when the task was completed in UTC" msgstr "" -#: documents/models.py:662 +#: documents/models.py:663 msgid "Result Data" msgstr "" -#: documents/models.py:664 +#: documents/models.py:665 msgid "The data returned by the task" msgstr "" -#: documents/models.py:672 +#: documents/models.py:673 msgid "Task Type" msgstr "" -#: documents/models.py:673 +#: documents/models.py:674 msgid "The type of task that was run" msgstr "" -#: documents/models.py:684 +#: documents/models.py:685 msgid "Note for the document" msgstr "" -#: documents/models.py:708 +#: documents/models.py:709 msgid "user" msgstr "" -#: documents/models.py:713 +#: documents/models.py:714 msgid "note" msgstr "" -#: documents/models.py:714 +#: documents/models.py:715 msgid "notes" msgstr "" -#: documents/models.py:722 +#: documents/models.py:723 msgid "Archive" msgstr "" -#: documents/models.py:723 +#: documents/models.py:724 msgid "Original" msgstr "" -#: documents/models.py:734 paperless_mail/models.py:75 +#: documents/models.py:735 paperless_mail/models.py:75 msgid "expiration" msgstr "" -#: documents/models.py:741 +#: documents/models.py:742 msgid "slug" msgstr "" -#: documents/models.py:773 +#: documents/models.py:774 msgid "share link" msgstr "" -#: documents/models.py:774 +#: documents/models.py:775 msgid "share links" msgstr "" -#: documents/models.py:786 +#: documents/models.py:787 msgid "String" msgstr "" -#: documents/models.py:787 +#: documents/models.py:788 msgid "URL" msgstr "" -#: documents/models.py:788 +#: documents/models.py:789 msgid "Date" msgstr "" -#: documents/models.py:789 +#: documents/models.py:790 msgid "Boolean" msgstr "" -#: documents/models.py:790 +#: documents/models.py:791 msgid "Integer" msgstr "" -#: documents/models.py:791 +#: documents/models.py:792 msgid "Float" msgstr "" -#: documents/models.py:792 +#: documents/models.py:793 msgid "Monetary" msgstr "" -#: documents/models.py:793 +#: documents/models.py:794 msgid "Document Link" msgstr "" -#: documents/models.py:794 +#: documents/models.py:795 msgid "Select" msgstr "" -#: documents/models.py:795 +#: documents/models.py:796 msgid "Long Text" msgstr "" -#: documents/models.py:807 +#: documents/models.py:808 msgid "data type" msgstr "" -#: documents/models.py:814 +#: documents/models.py:815 msgid "extra data" msgstr "" -#: documents/models.py:818 +#: documents/models.py:819 msgid "Extra data for the custom field, such as select options" msgstr "" -#: documents/models.py:824 +#: documents/models.py:825 msgid "custom field" msgstr "" -#: documents/models.py:825 +#: documents/models.py:826 msgid "custom fields" msgstr "" -#: documents/models.py:925 +#: documents/models.py:926 msgid "custom field instance" msgstr "" -#: documents/models.py:926 +#: documents/models.py:927 msgid "custom field instances" msgstr "" -#: documents/models.py:991 +#: documents/models.py:992 msgid "Consumption Started" msgstr "" -#: documents/models.py:992 +#: documents/models.py:993 msgid "Document Added" msgstr "" -#: documents/models.py:993 +#: documents/models.py:994 msgid "Document Updated" msgstr "" -#: documents/models.py:994 +#: documents/models.py:995 msgid "Scheduled" msgstr "" -#: documents/models.py:997 +#: documents/models.py:998 msgid "Consume Folder" msgstr "" -#: documents/models.py:998 +#: documents/models.py:999 msgid "Api Upload" msgstr "" -#: documents/models.py:999 +#: documents/models.py:1000 msgid "Mail Fetch" msgstr "" -#: documents/models.py:1000 +#: documents/models.py:1001 msgid "Web UI" msgstr "" -#: documents/models.py:1005 +#: documents/models.py:1006 msgid "Modified" msgstr "" -#: documents/models.py:1006 +#: documents/models.py:1007 msgid "Custom Field" msgstr "" -#: documents/models.py:1009 +#: documents/models.py:1010 msgid "Workflow Trigger Type" msgstr "" -#: documents/models.py:1021 +#: documents/models.py:1022 msgid "filter path" msgstr "" -#: documents/models.py:1026 +#: documents/models.py:1027 msgid "" "Only consume documents with a path that matches this if specified. Wildcards " "specified as * are allowed. Case insensitive." msgstr "" -#: documents/models.py:1033 +#: documents/models.py:1034 msgid "filter filename" msgstr "" -#: documents/models.py:1038 paperless_mail/models.py:200 +#: documents/models.py:1039 paperless_mail/models.py:200 msgid "" "Only consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." msgstr "" -#: documents/models.py:1049 +#: documents/models.py:1050 msgid "filter documents from this mail rule" msgstr "" -#: documents/models.py:1065 +#: documents/models.py:1066 msgid "has these tag(s)" msgstr "" -#: documents/models.py:1072 +#: documents/models.py:1073 msgid "has all of these tag(s)" msgstr "" -#: documents/models.py:1079 +#: documents/models.py:1080 msgid "does not have these tag(s)" msgstr "" -#: documents/models.py:1087 +#: documents/models.py:1088 msgid "has this document type" msgstr "" -#: documents/models.py:1094 +#: documents/models.py:1095 msgid "does not have these document type(s)" msgstr "" -#: documents/models.py:1102 +#: documents/models.py:1103 msgid "has this correspondent" msgstr "" -#: documents/models.py:1109 +#: documents/models.py:1110 msgid "does not have these correspondent(s)" msgstr "" -#: documents/models.py:1117 +#: documents/models.py:1118 msgid "has this storage path" msgstr "" -#: documents/models.py:1124 +#: documents/models.py:1125 msgid "does not have these storage path(s)" msgstr "" -#: documents/models.py:1128 +#: documents/models.py:1129 msgid "filter custom field query" msgstr "" -#: documents/models.py:1131 +#: documents/models.py:1132 msgid "JSON-encoded custom field query expression." msgstr "" -#: documents/models.py:1135 +#: documents/models.py:1136 msgid "schedule offset days" msgstr "" -#: documents/models.py:1138 +#: documents/models.py:1139 msgid "The number of days to offset the schedule trigger by." msgstr "" -#: documents/models.py:1143 +#: documents/models.py:1144 msgid "schedule is recurring" msgstr "" -#: documents/models.py:1146 +#: documents/models.py:1147 msgid "If the schedule should be recurring." msgstr "" -#: documents/models.py:1151 +#: documents/models.py:1152 msgid "schedule recurring delay in days" msgstr "" -#: documents/models.py:1155 +#: documents/models.py:1156 msgid "The number of days between recurring schedule triggers." msgstr "" -#: documents/models.py:1160 +#: documents/models.py:1161 msgid "schedule date field" msgstr "" -#: documents/models.py:1165 +#: documents/models.py:1166 msgid "The field to check for a schedule trigger." msgstr "" -#: documents/models.py:1174 +#: documents/models.py:1175 msgid "schedule date custom field" msgstr "" -#: documents/models.py:1178 +#: documents/models.py:1179 msgid "workflow trigger" msgstr "" -#: documents/models.py:1179 +#: documents/models.py:1180 msgid "workflow triggers" msgstr "" -#: documents/models.py:1187 +#: documents/models.py:1188 msgid "email subject" msgstr "" -#: documents/models.py:1191 +#: documents/models.py:1192 msgid "" "The subject of the email, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1197 +#: documents/models.py:1198 msgid "email body" msgstr "" -#: documents/models.py:1200 +#: documents/models.py:1201 msgid "" "The body (message) of the email, can include some placeholders, see " "documentation." msgstr "" -#: documents/models.py:1206 +#: documents/models.py:1207 msgid "emails to" msgstr "" -#: documents/models.py:1209 +#: documents/models.py:1210 msgid "The destination email addresses, comma separated." msgstr "" -#: documents/models.py:1215 +#: documents/models.py:1216 msgid "include document in email" msgstr "" -#: documents/models.py:1226 +#: documents/models.py:1227 msgid "webhook url" msgstr "" -#: documents/models.py:1229 +#: documents/models.py:1230 msgid "The destination URL for the notification." msgstr "" -#: documents/models.py:1234 +#: documents/models.py:1235 msgid "use parameters" msgstr "" -#: documents/models.py:1239 +#: documents/models.py:1240 msgid "send as JSON" msgstr "" -#: documents/models.py:1243 +#: documents/models.py:1244 msgid "webhook parameters" msgstr "" -#: documents/models.py:1246 +#: documents/models.py:1247 msgid "The parameters to send with the webhook URL if body not used." msgstr "" -#: documents/models.py:1250 +#: documents/models.py:1251 msgid "webhook body" msgstr "" -#: documents/models.py:1253 +#: documents/models.py:1254 msgid "The body to send with the webhook URL if parameters not used." msgstr "" -#: documents/models.py:1257 +#: documents/models.py:1258 msgid "webhook headers" msgstr "" -#: documents/models.py:1260 +#: documents/models.py:1261 msgid "The headers to send with the webhook URL." msgstr "" -#: documents/models.py:1265 +#: documents/models.py:1266 msgid "include document in webhook" msgstr "" -#: documents/models.py:1276 +#: documents/models.py:1277 msgid "Assignment" msgstr "" -#: documents/models.py:1280 +#: documents/models.py:1281 msgid "Removal" msgstr "" -#: documents/models.py:1284 documents/templates/account/password_reset.html:15 +#: documents/models.py:1285 documents/templates/account/password_reset.html:15 msgid "Email" msgstr "" -#: documents/models.py:1288 +#: documents/models.py:1289 msgid "Webhook" msgstr "" -#: documents/models.py:1292 +#: documents/models.py:1293 msgid "Workflow Action Type" msgstr "" -#: documents/models.py:1298 +#: documents/models.py:1299 msgid "assign title" msgstr "" -#: documents/models.py:1302 +#: documents/models.py:1303 msgid "Assign a document title, must be a Jinja2 template, see documentation." msgstr "" -#: documents/models.py:1310 paperless_mail/models.py:274 +#: documents/models.py:1311 paperless_mail/models.py:274 msgid "assign this tag" msgstr "" -#: documents/models.py:1319 paperless_mail/models.py:282 +#: documents/models.py:1320 paperless_mail/models.py:282 msgid "assign this document type" msgstr "" -#: documents/models.py:1328 paperless_mail/models.py:296 +#: documents/models.py:1329 paperless_mail/models.py:296 msgid "assign this correspondent" msgstr "" -#: documents/models.py:1337 +#: documents/models.py:1338 msgid "assign this storage path" msgstr "" -#: documents/models.py:1346 +#: documents/models.py:1347 msgid "assign this owner" msgstr "" -#: documents/models.py:1353 +#: documents/models.py:1354 msgid "grant view permissions to these users" msgstr "" -#: documents/models.py:1360 +#: documents/models.py:1361 msgid "grant view permissions to these groups" msgstr "" -#: documents/models.py:1367 +#: documents/models.py:1368 msgid "grant change permissions to these users" msgstr "" -#: documents/models.py:1374 +#: documents/models.py:1375 msgid "grant change permissions to these groups" msgstr "" -#: documents/models.py:1381 +#: documents/models.py:1382 msgid "assign these custom fields" msgstr "" -#: documents/models.py:1385 +#: documents/models.py:1386 msgid "custom field values" msgstr "" -#: documents/models.py:1389 +#: documents/models.py:1390 msgid "Optional values to assign to the custom fields." msgstr "" -#: documents/models.py:1398 +#: documents/models.py:1399 msgid "remove these tag(s)" msgstr "" -#: documents/models.py:1403 +#: documents/models.py:1404 msgid "remove all tags" msgstr "" -#: documents/models.py:1410 +#: documents/models.py:1411 msgid "remove these document type(s)" msgstr "" -#: documents/models.py:1415 +#: documents/models.py:1416 msgid "remove all document types" msgstr "" -#: documents/models.py:1422 +#: documents/models.py:1423 msgid "remove these correspondent(s)" msgstr "" -#: documents/models.py:1427 +#: documents/models.py:1428 msgid "remove all correspondents" msgstr "" -#: documents/models.py:1434 +#: documents/models.py:1435 msgid "remove these storage path(s)" msgstr "" -#: documents/models.py:1439 +#: documents/models.py:1440 msgid "remove all storage paths" msgstr "" -#: documents/models.py:1446 +#: documents/models.py:1447 msgid "remove these owner(s)" msgstr "" -#: documents/models.py:1451 +#: documents/models.py:1452 msgid "remove all owners" msgstr "" -#: documents/models.py:1458 +#: documents/models.py:1459 msgid "remove view permissions for these users" msgstr "" -#: documents/models.py:1465 +#: documents/models.py:1466 msgid "remove view permissions for these groups" msgstr "" -#: documents/models.py:1472 +#: documents/models.py:1473 msgid "remove change permissions for these users" msgstr "" -#: documents/models.py:1479 +#: documents/models.py:1480 msgid "remove change permissions for these groups" msgstr "" -#: documents/models.py:1484 +#: documents/models.py:1485 msgid "remove all permissions" msgstr "" -#: documents/models.py:1491 +#: documents/models.py:1492 msgid "remove these custom fields" msgstr "" -#: documents/models.py:1496 +#: documents/models.py:1497 msgid "remove all custom fields" msgstr "" -#: documents/models.py:1505 +#: documents/models.py:1506 msgid "email" msgstr "" -#: documents/models.py:1514 +#: documents/models.py:1515 msgid "webhook" msgstr "" -#: documents/models.py:1518 +#: documents/models.py:1519 msgid "workflow action" msgstr "" -#: documents/models.py:1519 +#: documents/models.py:1520 msgid "workflow actions" msgstr "" -#: documents/models.py:1528 paperless_mail/models.py:145 +#: documents/models.py:1529 paperless_mail/models.py:145 msgid "order" msgstr "" -#: documents/models.py:1534 +#: documents/models.py:1535 msgid "triggers" msgstr "" -#: documents/models.py:1541 +#: documents/models.py:1542 msgid "actions" msgstr "" -#: documents/models.py:1544 paperless_mail/models.py:154 +#: documents/models.py:1545 paperless_mail/models.py:154 msgid "enabled" msgstr "" -#: documents/models.py:1555 +#: documents/models.py:1556 msgid "workflow" msgstr "" -#: documents/models.py:1559 +#: documents/models.py:1560 msgid "workflow trigger type" msgstr "" -#: documents/models.py:1573 +#: documents/models.py:1574 msgid "date run" msgstr "" -#: documents/models.py:1579 +#: documents/models.py:1580 msgid "workflow run" msgstr "" -#: documents/models.py:1580 +#: documents/models.py:1581 msgid "workflow runs" msgstr "" @@ -1594,263 +1598,303 @@ msgstr "" msgid "CMYK" msgstr "" -#: paperless/models.py:83 +#: paperless/models.py:78 paperless/models.py:87 +msgid "OpenAI" +msgstr "" + +#: paperless/models.py:79 +msgid "Huggingface" +msgstr "" + +#: paperless/models.py:88 +msgid "Ollama" +msgstr "" + +#: paperless/models.py:97 msgid "Sets the output PDF type" msgstr "" -#: paperless/models.py:95 +#: paperless/models.py:109 msgid "Do OCR from page 1 to this value" msgstr "" -#: paperless/models.py:101 +#: paperless/models.py:115 msgid "Do OCR using these languages" msgstr "" -#: paperless/models.py:108 +#: paperless/models.py:122 msgid "Sets the OCR mode" msgstr "" -#: paperless/models.py:116 +#: paperless/models.py:130 msgid "Controls the generation of an archive file" msgstr "" -#: paperless/models.py:124 +#: paperless/models.py:138 msgid "Sets image DPI fallback value" msgstr "" -#: paperless/models.py:131 +#: paperless/models.py:145 msgid "Controls the unpaper cleaning" msgstr "" -#: paperless/models.py:138 +#: paperless/models.py:152 msgid "Enables deskew" msgstr "" -#: paperless/models.py:141 +#: paperless/models.py:155 msgid "Enables page rotation" msgstr "" -#: paperless/models.py:146 +#: paperless/models.py:160 msgid "Sets the threshold for rotation of pages" msgstr "" -#: paperless/models.py:152 +#: paperless/models.py:166 msgid "Sets the maximum image size for decompression" msgstr "" -#: paperless/models.py:158 +#: paperless/models.py:172 msgid "Sets the Ghostscript color conversion strategy" msgstr "" -#: paperless/models.py:166 +#: paperless/models.py:180 msgid "Adds additional user arguments for OCRMyPDF" msgstr "" -#: paperless/models.py:175 +#: paperless/models.py:189 msgid "Application title" msgstr "" -#: paperless/models.py:182 +#: paperless/models.py:196 msgid "Application logo" msgstr "" -#: paperless/models.py:197 +#: paperless/models.py:211 msgid "Enables barcode scanning" msgstr "" -#: paperless/models.py:203 +#: paperless/models.py:217 msgid "Enables barcode TIFF support" msgstr "" -#: paperless/models.py:209 +#: paperless/models.py:223 msgid "Sets the barcode string" msgstr "" -#: paperless/models.py:217 +#: paperless/models.py:231 msgid "Retains split pages" msgstr "" -#: paperless/models.py:223 +#: paperless/models.py:237 msgid "Enables ASN barcode" msgstr "" -#: paperless/models.py:229 +#: paperless/models.py:243 msgid "Sets the ASN barcode prefix" msgstr "" -#: paperless/models.py:237 +#: paperless/models.py:251 msgid "Sets the barcode upscale factor" msgstr "" -#: paperless/models.py:244 +#: paperless/models.py:258 msgid "Sets the barcode DPI" msgstr "" -#: paperless/models.py:251 +#: paperless/models.py:265 msgid "Sets the maximum pages for barcode" msgstr "" -#: paperless/models.py:258 +#: paperless/models.py:272 msgid "Enables tag barcode" msgstr "" -#: paperless/models.py:264 +#: paperless/models.py:278 msgid "Sets the tag barcode mapping" msgstr "" -#: paperless/models.py:269 +#: paperless/models.py:287 +msgid "Enables AI features" +msgstr "" + +#: paperless/models.py:293 +msgid "Sets the LLM embedding backend" +msgstr "" + +#: paperless/models.py:301 +msgid "Sets the LLM embedding model" +msgstr "" + +#: paperless/models.py:308 +msgid "Sets the LLM backend" +msgstr "" + +#: paperless/models.py:316 +msgid "Sets the LLM model" +msgstr "" + +#: paperless/models.py:323 +msgid "Sets the LLM API key" +msgstr "" + +#: paperless/models.py:330 +msgid "Sets the LLM endpoint, optional" +msgstr "" + +#: paperless/models.py:337 msgid "paperless application settings" msgstr "" -#: paperless/settings.py:768 +#: paperless/settings.py:800 msgid "English (US)" msgstr "" -#: paperless/settings.py:769 +#: paperless/settings.py:801 msgid "Arabic" msgstr "" -#: paperless/settings.py:770 +#: paperless/settings.py:802 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:771 +#: paperless/settings.py:803 msgid "Belarusian" msgstr "" -#: paperless/settings.py:772 +#: paperless/settings.py:804 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:773 +#: paperless/settings.py:805 msgid "Catalan" msgstr "" -#: paperless/settings.py:774 +#: paperless/settings.py:806 msgid "Czech" msgstr "" -#: paperless/settings.py:775 +#: paperless/settings.py:807 msgid "Danish" msgstr "" -#: paperless/settings.py:776 +#: paperless/settings.py:808 msgid "German" msgstr "" -#: paperless/settings.py:777 +#: paperless/settings.py:809 msgid "Greek" msgstr "" -#: paperless/settings.py:778 +#: paperless/settings.py:810 msgid "English (GB)" msgstr "" -#: paperless/settings.py:779 +#: paperless/settings.py:811 msgid "Spanish" msgstr "" -#: paperless/settings.py:780 +#: paperless/settings.py:812 msgid "Persian" msgstr "" -#: paperless/settings.py:781 +#: paperless/settings.py:813 msgid "Finnish" msgstr "" -#: paperless/settings.py:782 +#: paperless/settings.py:814 msgid "French" msgstr "" -#: paperless/settings.py:783 +#: paperless/settings.py:815 msgid "Hungarian" msgstr "" -#: paperless/settings.py:784 +#: paperless/settings.py:816 msgid "Indonesian" msgstr "" -#: paperless/settings.py:785 +#: paperless/settings.py:817 msgid "Italian" msgstr "" -#: paperless/settings.py:786 +#: paperless/settings.py:818 msgid "Japanese" msgstr "" -#: paperless/settings.py:787 +#: paperless/settings.py:819 msgid "Korean" msgstr "" -#: paperless/settings.py:788 +#: paperless/settings.py:820 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:789 +#: paperless/settings.py:821 msgid "Norwegian" msgstr "" -#: paperless/settings.py:790 +#: paperless/settings.py:822 msgid "Dutch" msgstr "" -#: paperless/settings.py:791 +#: paperless/settings.py:823 msgid "Polish" msgstr "" -#: paperless/settings.py:792 +#: paperless/settings.py:824 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:793 +#: paperless/settings.py:825 msgid "Portuguese" msgstr "" -#: paperless/settings.py:794 +#: paperless/settings.py:826 msgid "Romanian" msgstr "" -#: paperless/settings.py:795 +#: paperless/settings.py:827 msgid "Russian" msgstr "" -#: paperless/settings.py:796 +#: paperless/settings.py:828 msgid "Slovak" msgstr "" -#: paperless/settings.py:797 +#: paperless/settings.py:829 msgid "Slovenian" msgstr "" -#: paperless/settings.py:798 +#: paperless/settings.py:830 msgid "Serbian" msgstr "" -#: paperless/settings.py:799 +#: paperless/settings.py:831 msgid "Swedish" msgstr "" -#: paperless/settings.py:800 +#: paperless/settings.py:832 msgid "Turkish" msgstr "" -#: paperless/settings.py:801 +#: paperless/settings.py:833 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:802 +#: paperless/settings.py:834 msgid "Vietnamese" msgstr "" -#: paperless/settings.py:803 +#: paperless/settings.py:835 msgid "Chinese Simplified" msgstr "" -#: paperless/settings.py:804 +#: paperless/settings.py:836 msgid "Chinese Traditional" msgstr "" -#: paperless/urls.py:370 +#: paperless/urls.py:376 msgid "Paperless-ngx administration" msgstr "" From 7c457466b76d7a4abeca521043de69d3c1f4eb11 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:55:52 -0800 Subject: [PATCH 06/57] Security: prevent path traversal in storage paths --- src/documents/signals/handlers.py | 24 +++++++++++++++++++++--- src/documents/templating/filepath.py | 17 +++++++++++++++++ src/documents/tests/test_api_objects.py | 24 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 5f2c8b4b2..4ec00258a 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -418,7 +418,15 @@ def update_filename_and_move_files( return instance = instance.document - def validate_move(instance, old_path: Path, new_path: Path): + def validate_move(instance, old_path: Path, new_path: Path, root: Path): + if not new_path.is_relative_to(root): + msg = ( + f"Document {instance!s}: Refusing to move file outside root {root}: " + f"{new_path}." + ) + logger.warning(msg) + raise CannotMoveFilesException(msg) + if not old_path.is_file(): # Can't do anything if the old file does not exist anymore. msg = f"Document {instance!s}: File {old_path} doesn't exist." @@ -507,12 +515,22 @@ def update_filename_and_move_files( return if move_original: - validate_move(instance, old_source_path, instance.source_path) + validate_move( + instance, + old_source_path, + instance.source_path, + settings.ORIGINALS_DIR, + ) create_source_path_directory(instance.source_path) shutil.move(old_source_path, instance.source_path) if move_archive: - validate_move(instance, old_archive_path, instance.archive_path) + validate_move( + instance, + old_archive_path, + instance.archive_path, + settings.ARCHIVE_DIR, + ) create_source_path_directory(instance.archive_path) shutil.move(old_archive_path, instance.archive_path) diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 7d76e7f31..805cefbdb 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -262,6 +262,17 @@ def get_custom_fields_context( return field_data +def _is_safe_relative_path(value: str) -> bool: + if value == "": + return True + + path = PurePath(value) + if path.is_absolute() or path.drive: + return False + + return ".." not in path.parts + + def validate_filepath_template_and_render( template_string: str, document: Document | None = None, @@ -309,6 +320,12 @@ def validate_filepath_template_and_render( ) rendered_template = template.render(context) + if not _is_safe_relative_path(rendered_template): + logger.warning( + "Template rendered an unsafe path (absolute or containing traversal).", + ) + return None + # We're good! return rendered_template except UndefinedError: diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index 014dd3c2a..0eb99f023 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -219,6 +219,30 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(StoragePath.objects.count(), 1) + def test_api_create_storage_path_rejects_traversal(self): + """ + GIVEN: + - API request to create a storage paths + - Storage path attempts directory traversal + WHEN: + - API is called + THEN: + - Correct HTTP 400 response + - No storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Traversal path", + "path": "../../../../../tmp/proof", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(StoragePath.objects.count(), 1) + def test_api_storage_path_placeholders(self): """ GIVEN: From 11ec676909cc349b3f866cab1d10f6f77035a512 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:42:45 -0800 Subject: [PATCH 07/57] Fix: propagate metadata override created value (#11659) --- src/documents/data_models.py | 3 ++- src/documents/tests/test_bulk_edit.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/documents/data_models.py b/src/documents/data_models.py index 7f98a1f05..2623a6138 100644 --- a/src/documents/data_models.py +++ b/src/documents/data_models.py @@ -22,7 +22,7 @@ class DocumentMetadataOverrides: document_type_id: int | None = None tag_ids: list[int] | None = None storage_path_id: int | None = None - created: datetime.datetime | None = None + created: datetime.date | None = None asn: int | None = None owner_id: int | None = None view_users: list[int] | None = None @@ -100,6 +100,7 @@ class DocumentMetadataOverrides: overrides.storage_path_id = doc.storage_path.id if doc.storage_path else None overrides.owner_id = doc.owner.id if doc.owner else None overrides.tag_ids = list(doc.tags.values_list("id", flat=True)) + overrides.created = doc.created overrides.view_users = list( get_users_with_perms( diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index e1379386f..62ce48de4 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -581,7 +581,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): - Consume file should be called """ doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id] - metadata_document_id = self.doc1.id + metadata_document_id = self.doc2.id user = User.objects.create(username="test_user") result = bulk_edit.merge( @@ -606,7 +606,9 @@ class TestPDFActions(DirectoriesMixin, TestCase): # With metadata_document_id overrides result = bulk_edit.merge(doc_ids, metadata_document_id=metadata_document_id) consume_file_args, _ = mock_consume_file.call_args - self.assertEqual(consume_file_args[1].title, "A (merged)") + self.assertEqual(consume_file_args[1].title, "B (merged)") + self.assertEqual(consume_file_args[1].created, self.doc2.created) + self.assertTrue(consume_file_args[1].skip_asn) self.assertEqual(result, "OK") From 00bb92e3e17fe93a6a42dfa10b79cd3a0fd62779 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 28 Dec 2025 16:05:21 -0800 Subject: [PATCH 08/57] Fix: support ordering by storage path name (#11661) --- src/documents/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/documents/views.py b/src/documents/views.py index ba265926c..d5910497f 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -708,6 +708,7 @@ class DocumentViewSet( "title", "correspondent__name", "document_type__name", + "storage_path__name", "created", "modified", "added", From cb091665e2cb60982b8e673a61484eb09eebd736 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 29 Dec 2025 06:48:31 -0800 Subject: [PATCH 09/57] Fix: validate cf integer values within PostgreSQL range (#11666) --- src/documents/serialisers.py | 9 ++++++ src/documents/tests/test_api_documents.py | 38 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5c90c6f1c..5c71de9a9 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -18,6 +18,8 @@ from django.core.exceptions import ValidationError from django.core.validators import DecimalValidator from django.core.validators import EmailValidator from django.core.validators import MaxLengthValidator +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator from django.core.validators import RegexValidator from django.core.validators import integer_validator from django.db.models import Count @@ -875,6 +877,13 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): uri_validator(data["value"]) elif field.data_type == CustomField.FieldDataType.INT: integer_validator(data["value"]) + try: + value_int = int(data["value"]) + except (TypeError, ValueError): + raise serializers.ValidationError("Enter a valid integer.") + # Keep values within the PostgreSQL integer range + MinValueValidator(-2147483648)(value_int) + MaxValueValidator(2147483647)(value_int) elif ( field.data_type == CustomField.FieldDataType.MONETARY and data["value"] != "" diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 87190c23b..f40ef157f 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -1664,6 +1664,44 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.consume_file_mock.assert_not_called() + def test_patch_document_integer_custom_field_out_of_range(self): + """ + GIVEN: + - An integer custom field + - A document + WHEN: + - Patching the document with an integer value exceeding PostgreSQL's range + THEN: + - HTTP 400 is returned (validation catches the overflow) + - No custom field instance is created + """ + cf_int = CustomField.objects.create( + name="intfield", + data_type=CustomField.FieldDataType.INT, + ) + doc = Document.objects.create( + title="Doc", + checksum="123", + mime_type="application/pdf", + ) + + response = self.client.patch( + f"/api/documents/{doc.pk}/", + { + "custom_fields": [ + { + "field": cf_int.pk, + "value": 2**31, # overflow for PostgreSQL integer fields + }, + ], + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("custom_fields", response.data) + self.assertEqual(CustomFieldInstance.objects.count(), 0) + def test_upload_with_webui_source(self): """ GIVEN: A document with a source file From d4e60e13bf1e2bd27ed5ab1313dc927eda940f18 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:49:24 -0800 Subject: [PATCH 10/57] Fixhancement: add error handling and retry when opening index (#11731) --- src/documents/index.py | 34 +++++++-- src/documents/tests/test_index.py | 118 ++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/documents/index.py b/src/documents/index.py index 6b994ac8c..ea26ea926 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -10,6 +10,7 @@ from datetime import time from datetime import timedelta from datetime import timezone from shutil import rmtree +from time import sleep from typing import TYPE_CHECKING from typing import Literal @@ -32,6 +33,7 @@ from whoosh.highlight import HtmlFormatter from whoosh.idsets import BitSet from whoosh.idsets import DocIdSet from whoosh.index import FileIndex +from whoosh.index import LockError from whoosh.index import create_in from whoosh.index import exists_in from whoosh.index import open_dir @@ -97,11 +99,33 @@ def get_schema() -> Schema: def open_index(*, recreate=False) -> FileIndex: - try: - if exists_in(settings.INDEX_DIR) and not recreate: - return open_dir(settings.INDEX_DIR, schema=get_schema()) - except Exception: - logger.exception("Error while opening the index, recreating.") + transient_exceptions = (FileNotFoundError, LockError) + max_retries = 3 + retry_delay = 0.1 + + for attempt in range(max_retries + 1): + try: + if exists_in(settings.INDEX_DIR) and not recreate: + return open_dir(settings.INDEX_DIR, schema=get_schema()) + break + except transient_exceptions as exc: + is_last_attempt = attempt == max_retries or recreate + if is_last_attempt: + logger.exception( + "Error while opening the index after retries, recreating.", + ) + break + + logger.warning( + "Transient error while opening the index (attempt %s/%s): %s. Retrying.", + attempt + 1, + max_retries + 1, + exc, + ) + sleep(retry_delay) + except Exception: + logger.exception("Error while opening the index, recreating.") + break # create_in doesn't handle corrupted indexes very well, remove the directory entirely first if settings.INDEX_DIR.is_dir(): diff --git a/src/documents/tests/test_index.py b/src/documents/tests/test_index.py index f216feedb..3167bb762 100644 --- a/src/documents/tests/test_index.py +++ b/src/documents/tests/test_index.py @@ -1,6 +1,7 @@ from datetime import datetime from unittest import mock +from django.conf import settings from django.contrib.auth.models import User from django.test import SimpleTestCase from django.test import TestCase @@ -251,3 +252,120 @@ class TestRewriteNaturalDateKeywords(SimpleTestCase): result = self._rewrite_with_now("added:today", fixed_now) # Should convert to UTC properly self.assertIn("added:[20250719", result) + + +class TestIndexResilience(DirectoriesMixin, SimpleTestCase): + def _assert_recreate_called(self, mock_create_in): + mock_create_in.assert_called_once() + path_arg, schema_arg = mock_create_in.call_args.args + self.assertEqual(path_arg, settings.INDEX_DIR) + self.assertEqual(schema_arg.__class__.__name__, "Schema") + + def test_transient_missing_segment_does_not_force_recreate(self): + """ + GIVEN: + - Index directory exists + WHEN: + - open_index is called + - Opening the index raises FileNotFoundError once due to a + transient missing segment + THEN: + - Index is opened successfully on retry + - Index is not recreated + """ + file_marker = settings.INDEX_DIR / "file_marker.txt" + file_marker.write_text("keep") + expected_index = object() + + with ( + mock.patch("documents.index.exists_in", return_value=True), + mock.patch( + "documents.index.open_dir", + side_effect=[FileNotFoundError("missing"), expected_index], + ) as mock_open_dir, + mock.patch( + "documents.index.create_in", + ) as mock_create_in, + mock.patch( + "documents.index.rmtree", + ) as mock_rmtree, + ): + ix = index.open_index() + + self.assertIs(ix, expected_index) + self.assertGreaterEqual(mock_open_dir.call_count, 2) + mock_rmtree.assert_not_called() + mock_create_in.assert_not_called() + self.assertEqual(file_marker.read_text(), "keep") + + def test_transient_errors_exhaust_retries_and_recreate(self): + """ + GIVEN: + - Index directory exists + WHEN: + - open_index is called + - Opening the index raises FileNotFoundError multiple times due to + transient missing segments + THEN: + - Index is recreated after retries are exhausted + """ + recreated_index = object() + + with ( + self.assertLogs("paperless.index", level="ERROR") as cm, + mock.patch("documents.index.exists_in", return_value=True), + mock.patch( + "documents.index.open_dir", + side_effect=FileNotFoundError("missing"), + ) as mock_open_dir, + mock.patch("documents.index.rmtree") as mock_rmtree, + mock.patch( + "documents.index.create_in", + return_value=recreated_index, + ) as mock_create_in, + ): + ix = index.open_index() + + self.assertIs(ix, recreated_index) + self.assertEqual(mock_open_dir.call_count, 4) + mock_rmtree.assert_called_once_with(settings.INDEX_DIR) + self._assert_recreate_called(mock_create_in) + self.assertIn( + "Error while opening the index after retries, recreating.", + cm.output[0], + ) + + def test_non_transient_error_recreates_index(self): + """ + GIVEN: + - Index directory exists + WHEN: + - open_index is called + - Opening the index raises a "non-transient" error + THEN: + - Index is recreated + """ + recreated_index = object() + + with ( + self.assertLogs("paperless.index", level="ERROR") as cm, + mock.patch("documents.index.exists_in", return_value=True), + mock.patch( + "documents.index.open_dir", + side_effect=RuntimeError("boom"), + ), + mock.patch("documents.index.rmtree") as mock_rmtree, + mock.patch( + "documents.index.create_in", + return_value=recreated_index, + ) as mock_create_in, + ): + ix = index.open_index() + + self.assertIs(ix, recreated_index) + mock_rmtree.assert_called_once_with(settings.INDEX_DIR) + self._assert_recreate_called(mock_create_in) + self.assertIn( + "Error while opening the index, recreating.", + cm.output[0], + ) From e816269db546ef2f6445fb25cec63284f5dd3147 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:52:53 -0800 Subject: [PATCH 11/57] Fix: recurring workflow to respect latest run time (#11735) --- src/documents/tasks.py | 2 +- src/documents/tests/test_workflows.py | 62 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 606f278db..6c415ad69 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -493,7 +493,7 @@ def check_scheduled_workflows(): trigger.schedule_is_recurring and workflow_runs.exists() and ( - workflow_runs.last().run_at + workflow_runs.first().run_at > now - datetime.timedelta( days=trigger.schedule_recurring_interval_days, diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 249183b6e..deb40a165 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -2094,6 +2094,68 @@ class TestWorkflows( doc.refresh_from_db() self.assertIsNone(doc.owner) + def test_workflow_scheduled_recurring_respects_latest_run(self): + """ + GIVEN: + - Scheduled workflow marked as recurring with a 1-day interval + - Document that matches the trigger + - Two prior runs exist: one 2 days ago and one 1 hour ago + WHEN: + - Scheduled workflows are checked again + THEN: + - Workflow does not run because the most recent run is inside the interval + """ + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, + schedule_date_field=WorkflowTrigger.ScheduleDateField.CREATED, + schedule_is_recurring=True, + schedule_recurring_interval_days=1, + ) + action = WorkflowAction.objects.create( + assign_title="Doc assign owner", + assign_owner=self.user2, + ) + w = Workflow.objects.create( + name="Workflow 1", + order=0, + ) + w.triggers.add(trigger) + w.actions.add(action) + w.save() + + doc = Document.objects.create( + title="sample test", + correspondent=self.c, + original_filename="sample.pdf", + created=timezone.now().date() - timedelta(days=3), + ) + + WorkflowRun.objects.create( + workflow=w, + document=doc, + type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, + run_at=timezone.now() - timedelta(days=2), + ) + WorkflowRun.objects.create( + workflow=w, + document=doc, + type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, + run_at=timezone.now() - timedelta(hours=1), + ) + + tasks.check_scheduled_workflows() + + doc.refresh_from_db() + self.assertIsNone(doc.owner) + self.assertEqual( + WorkflowRun.objects.filter( + workflow=w, + document=doc, + type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, + ).count(), + 2, + ) + def test_workflow_scheduled_trigger_negative_offset_customfield(self): """ GIVEN: From 6f4497185e5d65f4b2ebcb60a3aa363401b6b828 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:01:27 -0800 Subject: [PATCH 12/57] Fix merge conflict --- src/documents/tests/test_bulk_edit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index 62ce48de4..b2cb89b8b 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -608,7 +608,6 @@ class TestPDFActions(DirectoriesMixin, TestCase): consume_file_args, _ = mock_consume_file.call_args self.assertEqual(consume_file_args[1].title, "B (merged)") self.assertEqual(consume_file_args[1].created, self.doc2.created) - self.assertTrue(consume_file_args[1].skip_asn) self.assertEqual(result, "OK") From 3618c50b62cc6afd42251eaf6f020585a2d7cd11 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:38:00 -0800 Subject: [PATCH 13/57] Bump version to 2.20.4 --- pyproject.toml | 2 +- src-ui/package.json | 2 +- src-ui/src/environments/environment.prod.ts | 2 +- src/paperless/version.py | 2 +- uv.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f5c484ae4..64df97c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.4" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index dcfe3ed63..9690e86c0 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.3", + "version": "2.20.4", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index c8bb844e9..d27ab9966 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.3', + version: '2.20.4', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/paperless/version.py b/src/paperless/version.py index c0c6439d4..0ce227357 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 3) +__version__: Final[tuple[int, int, int]] = (2, 20, 4) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/uv.lock b/uv.lock index d1cf11ee2..fccc00ada 100644 --- a/uv.lock +++ b/uv.lock @@ -2115,7 +2115,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.4" source = { virtual = "." } dependencies = [ { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, From 71ecdc528e6ee4d25e1f3843bea72a68a848959b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:51:37 -0800 Subject: [PATCH 14/57] Documentation: Add v2.20.4 changelog (#11772) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/changelog.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 189c74ce1..29f955256 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,44 @@ # Changelog +## paperless-ngx 2.20.4 + +### Security + +- Resolve [GHSA-28cf-xvcf-hw6m](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-28cf-xvcf-hw6m) + +### Bug Fixes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) + +### All App Changes + +
    +5 changes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) +
    + ## paperless-ngx 2.20.3 +### Security + +- Resolve [GHSA-7cq3-mhxq-w946](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7cq3-mhxq-w946) + ## paperless-ngx 2.20.2 +### Security + +- Resolve [GHSA-6653-vcx4-69mc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-6653-vcx4-69mc) +- Resolve [GHSA-24x5-wp64-9fcc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-24x5-wp64-9fcc) + ### Features / Enhancements - Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537)) From 6cf8abc5d31233ef1ec56bc72e9a3783c08fcf58 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:25:36 -0800 Subject: [PATCH 15/57] Chore: attempt to resolve Codecov patch coverage issues (#11773) --- .codecov.yml | 56 +++++++++++++++++++++++++------ .github/workflows/ci-backend.yml | 4 +-- .github/workflows/ci-frontend.yml | 4 +-- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index b4037d507..e6b4d0347 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,7 @@ +# https://docs.codecov.com/docs/codecovyml-reference#codecov codecov: require_ci_to_pass: true - # https://docs.codecov.com/docs/components +# https://docs.codecov.com/docs/components component_management: individual_components: - component_id: backend @@ -9,35 +10,70 @@ component_management: - component_id: frontend paths: - src-ui/** +# https://docs.codecov.com/docs/flags#step-2-flag-management-in-yaml +# https://docs.codecov.com/docs/carryforward-flags flags: - backend: + # Backend Python versions + backend-python-3.10: paths: - src/** carryforward: true - frontend: + backend-python-3.11: + paths: + - src/** + carryforward: true + backend-python-3.12: + paths: + - src/** + carryforward: true + # Frontend (shards merge into single flag) + frontend-node-24.x: paths: - src-ui/** carryforward: true -# https://docs.codecov.com/docs/pull-request-comments comment: layout: "header, diff, components, flags, files" - # https://docs.codecov.com/docs/javascript-bundle-analysis require_bundle_changes: true bundle_change_threshold: "50Kb" coverage: + # https://docs.codecov.com/docs/commit-status status: project: - default: + backend: + flags: + - backend-python-3.10 + - backend-python-3.11 + - backend-python-3.12 + paths: + - src/** # https://docs.codecov.com/docs/commit-status#threshold threshold: 1% + removed_code_behavior: adjust_base + frontend: + flags: + - frontend-node-24.x + paths: + - src-ui/** + threshold: 1% + removed_code_behavior: adjust_base patch: - default: - # For the changed lines only, target 100% covered, but - # allow as low as 75% + backend: + flags: + - backend-python-3.10 + - backend-python-3.11 + - backend-python-3.12 + paths: + - src/** + target: 100% + threshold: 25% + frontend: + flags: + - frontend-node-24.x + paths: + - src-ui/** target: 100% threshold: 25% # https://docs.codecov.com/docs/javascript-bundle-analysis bundle_analysis: - # Fail if the bundle size increases by more than 1MB warning_threshold: "1MB" status: true diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 25d19f73a..98c10396c 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -88,13 +88,13 @@ jobs: if: always() uses: codecov/codecov-action@v5 with: - flags: backend,backend-python-${{ matrix.python-version }} + flags: backend-python-${{ matrix.python-version }} files: junit.xml report_type: test_results - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: - flags: backend,backend-python-${{ matrix.python-version }} + flags: backend-python-${{ matrix.python-version }} files: coverage.xml report_type: coverage - name: Stop containers diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 907d36abf..d800fe827 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -109,13 +109,13 @@ jobs: if: always() uses: codecov/codecov-action@v5 with: - flags: frontend,frontend-node-${{ matrix.node-version }} + flags: frontend-node-${{ matrix.node-version }} directory: src-ui/ report_type: test_results - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: - flags: frontend,frontend-node-${{ matrix.node-version }} + flags: frontend-node-${{ matrix.node-version }} directory: src-ui/coverage/ e2e-tests: name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})" From eeb5639990042e45e66c3b8cc711ecaf24f928b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:02:22 -0800 Subject: [PATCH 16/57] Chore(deps): Bump azure-core from 1.33.0 to 1.38.0 (#11776) Bumps [azure-core](https://github.com/Azure/azure-sdk-for-python) from 1.33.0 to 1.38.0. - [Release notes](https://github.com/Azure/azure-sdk-for-python/releases) - [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-core_1.33.0...azure-core_1.38.0) --- updated-dependencies: - dependency-name: azure-core dependency-version: 1.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/uv.lock b/uv.lock index 596bebc75..055dff8a4 100644 --- a/uv.lock +++ b/uv.lock @@ -210,23 +210,22 @@ dependencies = [ { name = "isodate", 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/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 }, + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, ] [[package]] name = "azure-core" -version = "1.33.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "six", 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/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, ] [[package]] @@ -1849,9 +1848,9 @@ wheels = [ name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] From 948c664dcfe7c8959880bef7e560395de9238190 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:55:03 -0800 Subject: [PATCH 17/57] Correct get_tool_calls_from_response signature --- src/paperless_ai/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paperless_ai/client.py b/src/paperless_ai/client.py index 57eedaa75..2b66bfd9b 100644 --- a/src/paperless_ai/client.py +++ b/src/paperless_ai/client.py @@ -52,7 +52,7 @@ class AIClient: ) tool_calls = self.llm.get_tool_calls_from_response( result, - error_on_no_tool_calls=True, + error_on_no_tool_call=True, ) logger.debug("LLM query result: %s", tool_calls) parsed = DocumentClassifierSchema(**tool_calls[0].tool_kwargs) From 94a5af66eb4babc219d0283c523b8662f0b0d81e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:36:01 -0800 Subject: [PATCH 18/57] Fix default llama3.1 --- docs/configuration.md | 2 +- src/paperless_ai/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6a3624d36..b7b24d313 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1873,7 +1873,7 @@ using the OpenAI API. This setting is required to be set to use the AI features. #### [`PAPERLESS_AI_LLM_MODEL=`](#PAPERLESS_AI_LLM_MODEL) {#PAPERLESS_AI_LLM_MODEL} : The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the -current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3" for Ollama. +current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3.1" for Ollama. Defaults to None. diff --git a/src/paperless_ai/client.py b/src/paperless_ai/client.py index 2b66bfd9b..1f52c56c7 100644 --- a/src/paperless_ai/client.py +++ b/src/paperless_ai/client.py @@ -23,7 +23,7 @@ class AIClient: def get_llm(self) -> Ollama | OpenAI: if self.settings.llm_backend == "ollama": return Ollama( - model=self.settings.llm_model or "llama3", + model=self.settings.llm_model or "llama3.1", base_url=self.settings.llm_endpoint or "http://localhost:11434", request_timeout=120, ) From 1b415590670816dd5b971c713b4c00876397dc88 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:33:19 -0800 Subject: [PATCH 19/57] Chore: Reduce amd64 Docker image size by using CPU-only PyTorch wheels (#11779) --------- Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com> --- Dockerfile | 6 +- pyproject.toml | 10 ++ uv.lock | 245 ++++++++++++------------------------------------- 3 files changed, 72 insertions(+), 189 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5262ea124..419b19b3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -196,7 +196,11 @@ RUN set -eux \ && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ && echo "Installing Python requirements" \ && uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \ - && uv pip install --no-cache --system --no-python-downloads --python-preference system --requirements requirements.txt \ + && uv pip install --no-cache --system --no-python-downloads --python-preference system \ + --index https://pypi.org/simple \ + --index https://download.pytorch.org/whl/cpu \ + --index-strategy unsafe-best-match \ + --requirements requirements.txt \ && echo "Installing NLTK data" \ && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \ && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \ diff --git a/pyproject.toml b/pyproject.toml index a030ad840..e23a589e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ dependencies = [ "sentence-transformers>=4.1", "setproctitle~=1.3.4", "tika-client~=0.10.0", + "torch~=2.7.0", "tqdm~=4.67.1", "watchdog~=6.0", "whitenoise~=6.9", @@ -169,6 +170,15 @@ zxing-cpp = [ { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" }, ] +torch = [ + { index = "pytorch-cpu" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + [tool.ruff] target-version = "py310" line-length = 88 diff --git a/uv.lock b/uv.lock index 055dff8a4..bb559b10e 100644 --- a/uv.lock +++ b/uv.lock @@ -2746,139 +2746,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, ] -[[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" }, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" }, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" }, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" }, - { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" }, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, -] - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" }, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" }, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" }, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" }, -] - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, - { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" }, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" }, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" }, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" }, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, -] - [[package]] name = "oauthlib" version = "3.3.1" @@ -3022,6 +2889,8 @@ dependencies = [ { name = "sentence-transformers", 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 = "torch", version = "2.7.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.7.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 = "watchdog", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3176,6 +3045,7 @@ requires-dist = [ { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, { name = "tika-client", specifier = "~=0.10.0" }, + { name = "torch", specifier = "~=2.7.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "tqdm", specifier = "~=4.67.1" }, { name = "watchdog", specifier = "~=6.0" }, { name = "whitenoise", specifier = "~=6.9" }, @@ -4793,7 +4663,8 @@ dependencies = [ { 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.16.2", 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", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch", version = "2.7.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.7.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 = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -5107,48 +4978,61 @@ wheels = [ [[package]] name = "torch" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } +version = "2.7.1" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] dependencies = [ - { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux')" }, - { name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", marker = "sys_platform == 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/46/c2/3fb87940fa160d956ee94d644d37b99a24b9c05a4222bf34f94c71880e28/torch-2.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9afea41b11e1a1ab1b258a5c31afbd646d6319042bfe4f231b408034b51128b", size = 99158447, upload-time = "2025-04-23T14:35:10.557Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/91d1de65573fce563f5284e69d9c56b57289625cffbbb6d533d5d56c36a5/torch-2.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b9960183b6e5b71239a3e6c883d8852c304e691c0b2955f7045e8a6d05b9183", size = 865164221, upload-time = "2025-04-23T14:33:27.864Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0b/b2b83f30b8e84a51bf4f96aa3f5f65fdf7c31c591cc519310942339977e2/torch-2.7.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:34e0168ed6de99121612d72224e59b2a58a83dae64999990eada7260c5dd582d", size = 68559462, upload-time = "2025-04-23T14:35:39.889Z" }, - { url = "https://files.pythonhosted.org/packages/40/da/7378d16cc636697f2a94f791cb496939b60fb8580ddbbef22367db2c2274/torch-2.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b7813e904757b125faf1a9a3154e1d50381d539ced34da1992f52440567c156", size = 99159397, upload-time = "2025-04-23T14:35:35.304Z" }, - { url = "https://files.pythonhosted.org/packages/0e/6b/87fcddd34df9f53880fa1f0c23af7b6b96c935856473faf3914323588c40/torch-2.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd5cfbb4c3bbadd57ad1b27d56a28008f8d8753733411a140fcfb84d7f933a25", size = 865183681, upload-time = "2025-04-23T14:34:21.802Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3f/85b56f7e2abcfa558c5fbf7b11eb02d78a4a63e6aeee2bbae3bb552abea5/torch-2.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0a8d43caa342b9986101ec5feb5bbf1d86570b5caa01e9cb426378311258fdde", size = 68569377, upload-time = "2025-04-23T14:35:20.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5e/ac759f4c0ab7c01feffa777bd68b43d2ac61560a9770eeac074b450f81d4/torch-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:36a6368c7ace41ad1c0f69f18056020b6a5ca47bedaca9a2f3b578f5a104c26c", size = 99013250, upload-time = "2025-04-23T14:35:15.589Z" }, - { url = "https://files.pythonhosted.org/packages/9c/58/2d245b6f1ef61cf11dfc4aceeaacbb40fea706ccebac3f863890c720ab73/torch-2.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15aab3e31c16feb12ae0a88dba3434a458874636f360c567caa6a91f6bfba481", size = 865042157, upload-time = "2025-04-23T14:32:56.011Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8d/b2939e5254be932db1a34b2bd099070c509e8887e0c5a90c498a917e4032/torch-2.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:30b7688a87239a7de83f269333651d8e582afffce6f591fff08c046f7787296e", size = 68574294, upload-time = "2025-04-23T14:34:47.098Z" }, - { url = "https://files.pythonhosted.org/packages/14/24/720ea9a66c29151b315ea6ba6f404650834af57a26b2a04af23ec246b2d5/torch-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:868ccdc11798535b5727509480cd1d86d74220cfdc42842c4617338c1109a205", size = 99015553, upload-time = "2025-04-23T14:34:41.075Z" }, - { url = "https://files.pythonhosted.org/packages/4b/27/285a8cf12bd7cd71f9f211a968516b07dcffed3ef0be585c6e823675ab91/torch-2.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b52347118116cf3dff2ab5a3c3dd97c719eb924ac658ca2a7335652076df708", size = 865046389, upload-time = "2025-04-23T14:32:01.16Z" }, - { url = "https://files.pythonhosted.org/packages/28/fd/74ba6fde80e2b9eef4237fe668ffae302c76f0e4221759949a632ca13afa/torch-2.7.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:edad98dddd82220465b106506bb91ee5ce32bd075cddbcf2b443dfaa2cbd83bf", size = 68856166, upload-time = "2025-04-23T14:34:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b4/8df3f9fe6bdf59e56a0e538592c308d18638eb5f5dc4b08d02abb173c9f0/torch-2.7.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a885fc25afefb6e6eb18a7d1e8bfa01cc153e92271d980a49243b250d5ab6d9", size = 99091348, upload-time = "2025-04-23T14:33:48.975Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f5/0bd30e9da04c3036614aa1b935a9f7e505a9e4f1f731b15e165faf8a4c74/torch-2.7.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:176300ff5bc11a5f5b0784e40bde9e10a35c4ae9609beed96b4aeb46a27f5fae", size = 865104023, upload-time = "2025-04-23T14:30:40.537Z" }, - { url = "https://files.pythonhosted.org/packages/90/48/7e6477cf40d48cc0a61fa0d41ee9582b9a316b12772fcac17bc1a40178e7/torch-2.7.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:27f5007bdf45f7bb7af7f11d1828d5c2487e030690afb3d89a651fd7036a390e", size = 68575074, upload-time = "2025-04-23T14:32:38.136Z" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:f8c3bee261b0c8e090f6347490dc6ee2aebfd661eb0f3f6aeae06d992d8ed56f" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:68a352c7f435abb5cb47e2c032dcd1012772ae2bacb6fc8b83b0c1b11874ab3a" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7b4f8b2b83bd08f7d399025a9a7b323bdbb53d20566f1e0d584689bb92d82f9a" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:95af97e7b2cecdc89edc0558962a51921bf9c61538597dbec6b7cc48d31e2e13" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7ecd868a086468e1bcf74b91db425c1c2951a9cfcd0592c4c73377b7e42485ae" }, +] + +[[package]] +name = "torch" +version = "2.7.1+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c0df17cee97653d09a4e84488a33d21217f9b24208583c55cf28f0045aab0766" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f04a373a3f643821f721da9898ef77dce73b5b6bfc64486f0976f7fb5f90e83" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fe6045b8f426bf2d0426e4fe009f1667a954ec2aeb82f1bd0bf60c6d7a85445" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a1684793e352f03fa14f78857e55d65de4ada8405ded1da2bf4f452179c4b779" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf2db5adf77b433844f080887ade049c4705ddf9fe1a32023ff84ff735aa5ad" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8f8b3cfc53010a4b4a3c7ecb88c212e9decc4f5eeb6af75c3c803937d2d60947" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:eb17646792ac4374ffc87e42369f45d21eff17c790868963b90483ef0b6db4ef" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84ea1f6a1d15663037d01b121d6e33bb9da3c90af8e069e5072c30f413455a57" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:56136a2aca6707df3c8811e46ea2d379eaafd18e656e2fd51e8e4d0ca995651b" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:355614185a2aea7155f9c88a20bfd49de5f3063866f3cf9b2f21b6e9e59e31e0" }, ] [[package]] @@ -5198,21 +5082,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, ] -[[package]] -name = "triton" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools", marker = "(python_full_version != '3.12.*' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/04/d54d3a6d077c646624dc9461b0059e23fd5d30e0dbe67471e3654aec81f9/triton-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fad99beafc860501d7fcc1fb7045d9496cbe2c882b1674640304949165a916e7", size = 156441993, upload-time = "2025-04-09T20:27:25.107Z" }, - { url = "https://files.pythonhosted.org/packages/3c/c5/4874a81131cc9e934d88377fbc9d24319ae1fb540f3333b4e9c696ebc607/triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984", size = 156528461, upload-time = "2025-04-09T20:27:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/ce18470914ab6cfbec9384ee565d23c4d1c55f0548160b1c7b33000b11fd/triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3", size = 156504509, upload-time = "2025-04-09T20:27:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/7d/74/4bf2702b65e93accaa20397b74da46fb7a0356452c1bb94dbabaf0582930/triton-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47bc87ad66fa4ef17968299acacecaab71ce40a238890acc6ad197c3abe2b8f1", size = 156516468, upload-time = "2025-04-09T20:27:48.196Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/f28a696fa750b9b608baa236f8225dd3290e5aff27433b06143adc025961/triton-3.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce4700fc14032af1e049005ae94ba908e71cd6c2df682239aed08e49bc71b742", size = 156580729, upload-time = "2025-04-09T20:27:55.424Z" }, -] - [[package]] name = "twisted" version = "25.5.0" From 055ce9172c663e08b6435d982f718d6dd517a041 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:49:21 -0800 Subject: [PATCH 20/57] Fix: use explicit order field for workflow actions (#11781) --- .../workflow-edit-dialog.component.spec.ts | 4 +-- .../workflow-edit-dialog.component.ts | 5 ---- .../migrations/1076_workflowaction_order.py | 28 +++++++++++++++++++ src/documents/models.py | 2 ++ src/documents/serialisers.py | 13 ++++++++- src/documents/signals/handlers.py | 2 +- src/documents/workflows/utils.py | 20 ++++++++++--- 7 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 src/documents/migrations/1076_workflowaction_order.py diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts index aa52592b1..fafc9e876 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts @@ -252,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => { expect(component.object.actions.length).toEqual(2) }) - it('should update order and remove ids from actions on drag n drop', () => { + it('should update order on drag n drop', () => { const action1 = workflow.actions[0] const action2 = workflow.actions[1] component.object = workflow @@ -261,8 +261,6 @@ describe('WorkflowEditDialogComponent', () => { WorkflowAction[] >) expect(component.object.actions).toEqual([action2, action1]) - expect(action1.id).toBeNull() - expect(action2.id).toBeNull() }) it('should not include auto matching in algorithms', () => { diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index f6d9e60f5..74221e3f0 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -1283,11 +1283,6 @@ export class WorkflowEditDialogComponent const actionField = this.actionFields.at(event.previousIndex) this.actionFields.removeAt(event.previousIndex) this.actionFields.insert(event.currentIndex, actionField) - // removing id will effectively re-create the actions in this order - this.object.actions.forEach((a) => (a.id = null)) - this.actionFields.controls.forEach((c) => - c.get('id').setValue(null, { emitEvent: false }) - ) } save(): void { diff --git a/src/documents/migrations/1076_workflowaction_order.py b/src/documents/migrations/1076_workflowaction_order.py new file mode 100644 index 000000000..5c9f7ff52 --- /dev/null +++ b/src/documents/migrations/1076_workflowaction_order.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-01-14 16:53 + +from django.db import migrations +from django.db import models +from django.db.models import F + + +def populate_action_order(apps, schema_editor): + WorkflowAction = apps.get_model("documents", "WorkflowAction") + WorkflowAction.objects.all().update(order=F("id")) + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1075_alter_paperlesstask_task_name"), + ] + + operations = [ + migrations.AddField( + model_name="workflowaction", + name="order", + field=models.PositiveIntegerField(default=0, verbose_name="order"), + ), + migrations.RunPython( + populate_action_order, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 0bf3d48dd..372fafaf2 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1295,6 +1295,8 @@ class WorkflowAction(models.Model): default=WorkflowActionType.ASSIGNMENT, ) + order = models.PositiveIntegerField(_("order"), default=0) + assign_title = models.TextField( _("assign title"), null=True, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index b780db815..a265b036b 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -2577,7 +2577,8 @@ class WorkflowSerializer(serializers.ModelSerializer): set_triggers.append(trigger_instance) if actions is not None and actions is not serializers.empty: - for action in actions: + for index, action in enumerate(actions): + action["order"] = index assign_tags = action.pop("assign_tags", None) assign_view_users = action.pop("assign_view_users", None) assign_view_groups = action.pop("assign_view_groups", None) @@ -2704,6 +2705,16 @@ class WorkflowSerializer(serializers.ModelSerializer): return instance + def to_representation(self, instance): + data = super().to_representation(instance) + actions = instance.actions.order_by("order", "pk") + data["actions"] = WorkflowActionSerializer( + actions, + many=True, + context=self.context, + ).data + return data + class TrashSerializer(SerializerWithPerms): documents = serializers.ListField( diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index bff68780b..cfd2f185b 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -781,7 +781,7 @@ def run_workflows( if matching.document_matches_workflow(document, workflow, trigger_type): action: WorkflowAction - for action in workflow.actions.all(): + for action in workflow.actions.order_by("order", "pk"): message = f"Applying {action} from {workflow}" if not use_overrides: logger.info(message, extra={"group": logging_group}) diff --git a/src/documents/workflows/utils.py b/src/documents/workflows/utils.py index 553622252..0a644b0eb 100644 --- a/src/documents/workflows/utils.py +++ b/src/documents/workflows/utils.py @@ -20,9 +20,6 @@ def get_workflows_for_trigger( wrap it in a list; otherwise fetch enabled workflows for the trigger with the prefetches used by the runner. """ - if workflow_to_run is not None: - return [workflow_to_run] - annotated_actions = ( WorkflowAction.objects.select_related( "assign_correspondent", @@ -105,10 +102,25 @@ def get_workflows_for_trigger( ) ) + action_prefetch = Prefetch( + "actions", + queryset=annotated_actions.order_by("order", "pk"), + ) + + if workflow_to_run is not None: + return ( + Workflow.objects.filter(pk=workflow_to_run.pk) + .prefetch_related( + action_prefetch, + "triggers", + ) + .distinct() + ) + return ( Workflow.objects.filter(enabled=True, triggers__type=trigger_type) .prefetch_related( - Prefetch("actions", queryset=annotated_actions), + action_prefetch, "triggers", ) .order_by("order") From 11cc2f82896c4eb5445135ff5054a600471f6c9c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 23:02:39 +0000 Subject: [PATCH 21/57] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 305 +++++++++++++------------ 1 file changed, 153 insertions(+), 152 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 3c3988307..4843c5ccc 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-13 16:26+0000\n" +"POT-Creation-Date: 2026-01-15 23:01+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -89,7 +89,7 @@ msgstr "" msgid "Automatic" msgstr "" -#: documents/models.py:64 documents/models.py:456 documents/models.py:1527 +#: documents/models.py:64 documents/models.py:456 documents/models.py:1529 #: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" @@ -264,7 +264,7 @@ msgid "The position of this document in your physical document archive." msgstr "" #: documents/models.py:318 documents/models.py:700 documents/models.py:754 -#: documents/models.py:1570 +#: documents/models.py:1572 msgid "document" msgstr "" @@ -1047,179 +1047,180 @@ msgstr "" msgid "Workflow Action Type" msgstr "" -#: documents/models.py:1299 -msgid "assign title" -msgstr "" - -#: documents/models.py:1303 -msgid "Assign a document title, must be a Jinja2 template, see documentation." -msgstr "" - -#: documents/models.py:1311 paperless_mail/models.py:274 -msgid "assign this tag" -msgstr "" - -#: documents/models.py:1320 paperless_mail/models.py:282 -msgid "assign this document type" -msgstr "" - -#: documents/models.py:1329 paperless_mail/models.py:296 -msgid "assign this correspondent" -msgstr "" - -#: documents/models.py:1338 -msgid "assign this storage path" -msgstr "" - -#: documents/models.py:1347 -msgid "assign this owner" -msgstr "" - -#: documents/models.py:1354 -msgid "grant view permissions to these users" -msgstr "" - -#: documents/models.py:1361 -msgid "grant view permissions to these groups" -msgstr "" - -#: documents/models.py:1368 -msgid "grant change permissions to these users" -msgstr "" - -#: documents/models.py:1375 -msgid "grant change permissions to these groups" -msgstr "" - -#: documents/models.py:1382 -msgid "assign these custom fields" -msgstr "" - -#: documents/models.py:1386 -msgid "custom field values" -msgstr "" - -#: documents/models.py:1390 -msgid "Optional values to assign to the custom fields." -msgstr "" - -#: documents/models.py:1399 -msgid "remove these tag(s)" -msgstr "" - -#: documents/models.py:1404 -msgid "remove all tags" -msgstr "" - -#: documents/models.py:1411 -msgid "remove these document type(s)" -msgstr "" - -#: documents/models.py:1416 -msgid "remove all document types" -msgstr "" - -#: documents/models.py:1423 -msgid "remove these correspondent(s)" -msgstr "" - -#: documents/models.py:1428 -msgid "remove all correspondents" -msgstr "" - -#: documents/models.py:1435 -msgid "remove these storage path(s)" -msgstr "" - -#: documents/models.py:1440 -msgid "remove all storage paths" -msgstr "" - -#: documents/models.py:1447 -msgid "remove these owner(s)" -msgstr "" - -#: documents/models.py:1452 -msgid "remove all owners" -msgstr "" - -#: documents/models.py:1459 -msgid "remove view permissions for these users" -msgstr "" - -#: documents/models.py:1466 -msgid "remove view permissions for these groups" -msgstr "" - -#: documents/models.py:1473 -msgid "remove change permissions for these users" -msgstr "" - -#: documents/models.py:1480 -msgid "remove change permissions for these groups" -msgstr "" - -#: documents/models.py:1485 -msgid "remove all permissions" -msgstr "" - -#: documents/models.py:1492 -msgid "remove these custom fields" -msgstr "" - -#: documents/models.py:1497 -msgid "remove all custom fields" -msgstr "" - -#: documents/models.py:1506 -msgid "email" -msgstr "" - -#: documents/models.py:1515 -msgid "webhook" -msgstr "" - -#: documents/models.py:1519 -msgid "workflow action" -msgstr "" - -#: documents/models.py:1520 -msgid "workflow actions" -msgstr "" - -#: documents/models.py:1529 paperless_mail/models.py:145 +#: documents/models.py:1298 documents/models.py:1531 +#: paperless_mail/models.py:145 msgid "order" msgstr "" -#: documents/models.py:1535 +#: documents/models.py:1301 +msgid "assign title" +msgstr "" + +#: documents/models.py:1305 +msgid "Assign a document title, must be a Jinja2 template, see documentation." +msgstr "" + +#: documents/models.py:1313 paperless_mail/models.py:274 +msgid "assign this tag" +msgstr "" + +#: documents/models.py:1322 paperless_mail/models.py:282 +msgid "assign this document type" +msgstr "" + +#: documents/models.py:1331 paperless_mail/models.py:296 +msgid "assign this correspondent" +msgstr "" + +#: documents/models.py:1340 +msgid "assign this storage path" +msgstr "" + +#: documents/models.py:1349 +msgid "assign this owner" +msgstr "" + +#: documents/models.py:1356 +msgid "grant view permissions to these users" +msgstr "" + +#: documents/models.py:1363 +msgid "grant view permissions to these groups" +msgstr "" + +#: documents/models.py:1370 +msgid "grant change permissions to these users" +msgstr "" + +#: documents/models.py:1377 +msgid "grant change permissions to these groups" +msgstr "" + +#: documents/models.py:1384 +msgid "assign these custom fields" +msgstr "" + +#: documents/models.py:1388 +msgid "custom field values" +msgstr "" + +#: documents/models.py:1392 +msgid "Optional values to assign to the custom fields." +msgstr "" + +#: documents/models.py:1401 +msgid "remove these tag(s)" +msgstr "" + +#: documents/models.py:1406 +msgid "remove all tags" +msgstr "" + +#: documents/models.py:1413 +msgid "remove these document type(s)" +msgstr "" + +#: documents/models.py:1418 +msgid "remove all document types" +msgstr "" + +#: documents/models.py:1425 +msgid "remove these correspondent(s)" +msgstr "" + +#: documents/models.py:1430 +msgid "remove all correspondents" +msgstr "" + +#: documents/models.py:1437 +msgid "remove these storage path(s)" +msgstr "" + +#: documents/models.py:1442 +msgid "remove all storage paths" +msgstr "" + +#: documents/models.py:1449 +msgid "remove these owner(s)" +msgstr "" + +#: documents/models.py:1454 +msgid "remove all owners" +msgstr "" + +#: documents/models.py:1461 +msgid "remove view permissions for these users" +msgstr "" + +#: documents/models.py:1468 +msgid "remove view permissions for these groups" +msgstr "" + +#: documents/models.py:1475 +msgid "remove change permissions for these users" +msgstr "" + +#: documents/models.py:1482 +msgid "remove change permissions for these groups" +msgstr "" + +#: documents/models.py:1487 +msgid "remove all permissions" +msgstr "" + +#: documents/models.py:1494 +msgid "remove these custom fields" +msgstr "" + +#: documents/models.py:1499 +msgid "remove all custom fields" +msgstr "" + +#: documents/models.py:1508 +msgid "email" +msgstr "" + +#: documents/models.py:1517 +msgid "webhook" +msgstr "" + +#: documents/models.py:1521 +msgid "workflow action" +msgstr "" + +#: documents/models.py:1522 +msgid "workflow actions" +msgstr "" + +#: documents/models.py:1537 msgid "triggers" msgstr "" -#: documents/models.py:1542 +#: documents/models.py:1544 msgid "actions" msgstr "" -#: documents/models.py:1545 paperless_mail/models.py:154 +#: documents/models.py:1547 paperless_mail/models.py:154 msgid "enabled" msgstr "" -#: documents/models.py:1556 +#: documents/models.py:1558 msgid "workflow" msgstr "" -#: documents/models.py:1560 +#: documents/models.py:1562 msgid "workflow trigger type" msgstr "" -#: documents/models.py:1574 +#: documents/models.py:1576 msgid "date run" msgstr "" -#: documents/models.py:1580 +#: documents/models.py:1582 msgid "workflow run" msgstr "" -#: documents/models.py:1581 +#: documents/models.py:1583 msgid "workflow runs" msgstr "" From 825e9ca14cb7168ddadff7f116ce58904802b4d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:12:12 -0800 Subject: [PATCH 22/57] Chore(deps): Bump virtualenv from 20.34.0 to 20.36.1 (#11774) Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.34.0 to 20.36.1. - [Release notes](https://github.com/pypa/virtualenv/releases) - [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/virtualenv/compare/20.34.0...20.36.1) --- updated-dependencies: - dependency-name: virtualenv dependency-version: 20.36.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index bb559b10e..3f14fd772 100644 --- a/uv.lock +++ b/uv.lock @@ -1244,11 +1244,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -5419,7 +5419,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.34.0" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -5427,9 +5427,9 @@ dependencies = [ { 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/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +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/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, + { 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]] From 0689c8ad3aaf2648ef097a5f15cb486698cccc68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:15:52 -0800 Subject: [PATCH 23/57] Chore(deps): Bump urllib3 from 2.5.0 to 2.6.3 (#11792) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.3. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.3) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.6.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 3f14fd772..f0d6c768b 100644 --- a/uv.lock +++ b/uv.lock @@ -5346,11 +5346,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { 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]] From c6697cd82b771813f43c07a505441ddc017ecc78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:26:01 -0800 Subject: [PATCH 24/57] Chore(deps): Bump aiohttp from 3.11.18 to 3.13.3 (#11789) --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.13.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 157 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 61 deletions(-) diff --git a/uv.lock b/uv.lock index f0d6c768b..49dd5512b 100644 --- a/uv.lock +++ b/uv.lock @@ -27,7 +27,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.18" +version = "3.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -39,76 +39,111 @@ dependencies = [ { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/c3/e5f64af7e97a02f547020e6ff861595766bb5ecb37c7492fac9fe3c14f6c/aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4", size = 711703, upload-time = "2025-04-21T09:40:25.487Z" }, - { url = "https://files.pythonhosted.org/packages/5f/2f/53c26e96efa5fd01ebcfe1fefdfb7811f482bb21f4fa103d85eca4dcf888/aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6", size = 471348, upload-time = "2025-04-21T09:40:27.569Z" }, - { url = "https://files.pythonhosted.org/packages/80/47/dcc248464c9b101532ee7d254a46f6ed2c1fd3f4f0f794cf1f2358c0d45b/aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609", size = 457611, upload-time = "2025-04-21T09:40:28.978Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ca/67d816ef075e8ac834b5f1f6b18e8db7d170f7aebaf76f1be462ea10cab0/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55", size = 1591976, upload-time = "2025-04-21T09:40:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/0c120287aa51c744438d99e9aae9f8c55ca5b9911c42706966c91c9d68d6/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f", size = 1632819, upload-time = "2025-04-21T09:40:32.731Z" }, - { url = "https://files.pythonhosted.org/packages/54/a3/3923c9040cd4927dfee1aa017513701e35adcfc35d10729909688ecaa465/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94", size = 1666567, upload-time = "2025-04-21T09:40:34.901Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ab/40dacb15c0c58f7f17686ea67bc186e9f207341691bdb777d1d5ff4671d5/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1", size = 1594959, upload-time = "2025-04-21T09:40:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/d40c2b7c4a5483f9a16ef0adffce279ced3cc44522e84b6ba9e906be5168/aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415", size = 1538516, upload-time = "2025-04-21T09:40:38.263Z" }, - { url = "https://files.pythonhosted.org/packages/cf/10/e0bf3a03524faac45a710daa034e6f1878b24a1fef9c968ac8eb786ae657/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7", size = 1529037, upload-time = "2025-04-21T09:40:40.349Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d6/5ff5282e00e4eb59c857844984cbc5628f933e2320792e19f93aff518f52/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb", size = 1546813, upload-time = "2025-04-21T09:40:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/de/96/f1014f84101f9b9ad2d8acf3cc501426475f7f0cc62308ae5253e2fac9a7/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d", size = 1523852, upload-time = "2025-04-21T09:40:44.164Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/ec772c6838dd6bae3229065af671891496ac1834b252f305cee8152584b2/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421", size = 1603766, upload-time = "2025-04-21T09:40:46.203Z" }, - { url = "https://files.pythonhosted.org/packages/84/38/31f85459c9402d409c1499284fc37a96f69afadce3cfac6a1b5ab048cbf1/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643", size = 1620647, upload-time = "2025-04-21T09:40:48.168Z" }, - { url = "https://files.pythonhosted.org/packages/31/2f/54aba0040764dd3d362fb37bd6aae9b3034fcae0b27f51b8a34864e48209/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868", size = 1559260, upload-time = "2025-04-21T09:40:50.219Z" }, - { url = "https://files.pythonhosted.org/packages/2f/10/fd9ee4f9e042818c3c2390054c08ccd34556a3cb209d83285616434cf93e/aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9", size = 712088, upload-time = "2025-04-21T09:40:55.776Z" }, - { url = "https://files.pythonhosted.org/packages/22/eb/6a77f055ca56f7aae2cd2a5607a3c9e7b9554f1497a069dcfcb52bfc9540/aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b", size = 471450, upload-time = "2025-04-21T09:40:57.301Z" }, - { url = "https://files.pythonhosted.org/packages/78/dc/5f3c0d27c91abf0bb5d103e9c9b0ff059f60cf6031a5f06f456c90731f42/aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66", size = 457836, upload-time = "2025-04-21T09:40:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/49/7b/55b65af9ef48b9b811c91ff8b5b9de9650c71147f10523e278d297750bc8/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756", size = 1690978, upload-time = "2025-04-21T09:41:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5a/3f8938c4f68ae400152b42742653477fc625d6bfe02e764f3521321c8442/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717", size = 1745307, upload-time = "2025-04-21T09:41:02.89Z" }, - { url = "https://files.pythonhosted.org/packages/b4/42/89b694a293333ef6f771c62da022163bcf44fb03d4824372d88e3dc12530/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4", size = 1780692, upload-time = "2025-04-21T09:41:04.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ce/1a75384e01dd1bf546898b6062b1b5f7a59b6692ef802e4dd6db64fed264/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f", size = 1676934, upload-time = "2025-04-21T09:41:06.728Z" }, - { url = "https://files.pythonhosted.org/packages/a5/31/442483276e6c368ab5169797d9873b5875213cbcf7e74b95ad1c5003098a/aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361", size = 1621190, upload-time = "2025-04-21T09:41:08.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/83/90274bf12c079457966008a58831a99675265b6a34b505243e004b408934/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1", size = 1658947, upload-time = "2025-04-21T09:41:11.054Z" }, - { url = "https://files.pythonhosted.org/packages/91/c1/da9cee47a0350b78fdc93670ebe7ad74103011d7778ab4c382ca4883098d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421", size = 1654443, upload-time = "2025-04-21T09:41:13.213Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f2/73cbe18dc25d624f79a09448adfc4972f82ed6088759ddcf783cd201956c/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e", size = 1644169, upload-time = "2025-04-21T09:41:14.827Z" }, - { url = "https://files.pythonhosted.org/packages/5b/32/970b0a196c4dccb1b0cfa5b4dc3b20f63d76f1c608f41001a84b2fd23c3d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d", size = 1728532, upload-time = "2025-04-21T09:41:17.168Z" }, - { url = "https://files.pythonhosted.org/packages/0b/50/b1dc810a41918d2ea9574e74125eb053063bc5e14aba2d98966f7d734da0/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f", size = 1750310, upload-time = "2025-04-21T09:41:19.353Z" }, - { url = "https://files.pythonhosted.org/packages/95/24/39271f5990b35ff32179cc95537e92499d3791ae82af7dcf562be785cd15/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd", size = 1691580, upload-time = "2025-04-21T09:41:21.868Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" }, - { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" }, - { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" }, - { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" }, - { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" }, - { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" }, - { url = "https://files.pythonhosted.org/packages/0a/18/be8b5dd6b9cf1b2172301dbed28e8e5e878ee687c21947a6c81d6ceaa15d/aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", size = 699833, upload-time = "2025-04-21T09:42:00.298Z" }, - { url = "https://files.pythonhosted.org/packages/0d/84/ecdc68e293110e6f6f6d7b57786a77555a85f70edd2b180fb1fafaff361a/aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", size = 462774, upload-time = "2025-04-21T09:42:02.015Z" }, - { url = "https://files.pythonhosted.org/packages/d7/85/f07718cca55884dad83cc2433746384d267ee970e91f0dcc75c6d5544079/aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", size = 454429, upload-time = "2025-04-21T09:42:03.728Z" }, - { url = "https://files.pythonhosted.org/packages/82/02/7f669c3d4d39810db8842c4e572ce4fe3b3a9b82945fdd64affea4c6947e/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", size = 1670283, upload-time = "2025-04-21T09:42:06.053Z" }, - { url = "https://files.pythonhosted.org/packages/ec/79/b82a12f67009b377b6c07a26bdd1b81dab7409fc2902d669dbfa79e5ac02/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", size = 1717231, upload-time = "2025-04-21T09:42:07.953Z" }, - { url = "https://files.pythonhosted.org/packages/a6/38/d5a1f28c3904a840642b9a12c286ff41fc66dfa28b87e204b1f242dbd5e6/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", size = 1769621, upload-time = "2025-04-21T09:42:09.855Z" }, - { url = "https://files.pythonhosted.org/packages/53/2d/deb3749ba293e716b5714dda06e257f123c5b8679072346b1eb28b766a0b/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", size = 1678667, upload-time = "2025-04-21T09:42:11.741Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a8/04b6e11683a54e104b984bd19a9790eb1ae5f50968b601bb202d0406f0ff/aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", size = 1601592, upload-time = "2025-04-21T09:42:14.137Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c33305ae8370b789423623f0e073d09ac775cd9c831ac0f11338b81c16e0/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", size = 1621679, upload-time = "2025-04-21T09:42:16.056Z" }, - { url = "https://files.pythonhosted.org/packages/56/45/8e9a27fff0538173d47ba60362823358f7a5f1653c6c30c613469f94150e/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", size = 1656878, upload-time = "2025-04-21T09:42:18.368Z" }, - { url = "https://files.pythonhosted.org/packages/84/5b/8c5378f10d7a5a46b10cb9161a3aac3eeae6dba54ec0f627fc4ddc4f2e72/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", size = 1620509, upload-time = "2025-04-21T09:42:20.141Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2f/99dee7bd91c62c5ff0aa3c55f4ae7e1bc99c6affef780d7777c60c5b3735/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", size = 1680263, upload-time = "2025-04-21T09:42:21.993Z" }, - { url = "https://files.pythonhosted.org/packages/03/0a/378745e4ff88acb83e2d5c884a4fe993a6e9f04600a4560ce0e9b19936e3/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", size = 1715014, upload-time = "2025-04-21T09:42:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0b/b5524b3bb4b01e91bc4323aad0c2fcaebdf2f1b4d2eb22743948ba364958/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", size = 1666614, upload-time = "2025-04-21T09:42:25.764Z" }, + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, ] [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] From ad78c436c0884c0ff40248de3493aa072bfef65b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:14:59 -0800 Subject: [PATCH 25/57] Chore(deps): Bump uv from 0.9.3 to 0.9.6 (#11795) Bumps [uv](https://github.com/astral-sh/uv) from 0.9.3 to 0.9.6. - [Release notes](https://github.com/astral-sh/uv/releases) - [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/uv/compare/0.9.3...0.9.6) --- updated-dependencies: - dependency-name: uv dependency-version: 0.9.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/uv.lock b/uv.lock index 49dd5512b..e0919e2a7 100644 --- a/uv.lock +++ b/uv.lock @@ -5390,25 +5390,25 @@ wheels = [ [[package]] name = "uv" -version = "0.9.3" +version = "0.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dc/4a0e01bcb38c756130c8118a8561d4bf0a0bb685b70ad11e8f40a0cbfa10/uv-0.9.3.tar.gz", hash = "sha256:a290a1a8783bf04ca2d4a63d5d72191b255dfa4cc3426a9c9b5af4da49a7b5af", size = 3699151, upload-time = "2025-10-15T15:20:15.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/51/c56ac81b4bd642d78365741eef118140459e2a94b9385ef973826b1b5306/uv-0.9.6.tar.gz", hash = "sha256:547fd27ab5da7cd1a833288a36858852451d416a056825f162ecf2af5be6f8b8", size = 3704033, upload-time = "2025-10-29T19:40:46.35Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ad/194e550062e4b3b9a74cb06401dc0afd83490af8e2ec0f414737868d0262/uv-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7b1b79dd435ade1de97c6f0b8b90811a6ccf1bd0bdd70f4d034a93696cf0d0a3", size = 20584531, upload-time = "2025-10-15T15:19:14.26Z" }, - { url = "https://files.pythonhosted.org/packages/d0/1a/8e68d0020c29f6f329a265773c23b0c01e002794ea884b8bdbd594c7ea97/uv-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:596a982c5a061d58412824a2ebe2960b52db23f1b1658083ba9c0e7ae390308a", size = 19577639, upload-time = "2025-10-15T15:19:18.668Z" }, - { url = "https://files.pythonhosted.org/packages/16/25/6df8be6cd549200e80d19374579689fda39b18735afde841345284fb113d/uv-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:741e80c4230e1b9a5d0869aca2fb082b3832b251ef61537bc9278364b8e74df2", size = 18210073, upload-time = "2025-10-15T15:19:22.16Z" }, - { url = "https://files.pythonhosted.org/packages/07/19/bb8aa38b4441e03c742e71a31779f91b42d9db255ede66f80cdfdb672618/uv-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:406ab1a8b313b4b3cf67ad747fb8713a0c0cf3d3daf11942b5a4e49f60882339", size = 20022427, upload-time = "2025-10-15T15:19:25.453Z" }, - { url = "https://files.pythonhosted.org/packages/40/15/f190004dd855b443cfc1cc36edb1765e6cd0b6b340a50bb8015531dfff2e/uv-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73dbd91581a82e53bb4352243d7bc491cf78ac3ebb951d95bb8b7964e5ee0659", size = 20150307, upload-time = "2025-10-15T15:19:28.99Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/553e90bc2b881f168de9cd57f9e0b0464304a12aee289e71b54c42559e1a/uv-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:970ac8428678b92eddb990dc132d75e893234bb1b809e87b90a4acd96bb054e4", size = 21152942, upload-time = "2025-10-15T15:19:32.461Z" }, - { url = "https://files.pythonhosted.org/packages/30/fb/768647a31622c2c1da7a9394eaab937e2e7ca0e8c983ca3d1918ec623620/uv-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:32694e64d6e4ea44b647866c4240659f3964b0317e98f539b73915dbcca7d973", size = 22632018, upload-time = "2025-10-15T15:19:36.091Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/66d660414aed123686bf9a2a3ea167967b847b97c08cacd13d6b2b6d1267/uv-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36df7eb562b103e3263a03df1b04cee91ee52af88d005d07ee494137c7a5782a", size = 22241856, upload-time = "2025-10-15T15:19:39.662Z" }, - { url = "https://files.pythonhosted.org/packages/0d/99/af8b0cd2c958e8cb9c20e6e2d417de9476338a2b155643492a8ee2baf077/uv-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:117c5921bcfdac04b88211ee830c6c7e412eaf93a34aa3ad4bb3230bc61646aa", size = 21391699, upload-time = "2025-10-15T15:19:42.933Z" }, - { url = "https://files.pythonhosted.org/packages/82/45/488417c6c0127c00bcdfac3556ae2ea0597df8245fe5f9bcfda35ebdbe85/uv-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ae4bbc7d555ba1738da08c64b55f21ab0ea0ff85636708cebaf460d98a440d", size = 21318117, upload-time = "2025-10-15T15:19:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/508c20f8dbdd2342cc4821ab6f41e29a9b36e2a469dfb5cbbd042e15218c/uv-0.9.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e75ce14c9375e7e99422d5383fb415e8f0eab9ebdcdfba45756749dee0c42b2", size = 20132999, upload-time = "2025-10-15T15:19:49.578Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fc/ea673d1c68915ea53f1ab7e134b330a2351c543f06e9d0009b4f27cc3057/uv-0.9.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:71faefa9805ccf3f2db645ae27c9e719e47aaa8781e43dfa3760d993aadecb8c", size = 21223810, upload-time = "2025-10-15T15:19:52.711Z" }, - { url = "https://files.pythonhosted.org/packages/97/1f/af8ced7f6c8f6af887c52369088058ecae92ff21819e385531023f9ec923/uv-0.9.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8844103e0b4074821fb2814abf30af59d66f33b6ca1bb2276dd37d4e5997c292", size = 20156823, upload-time = "2025-10-15T15:19:56.552Z" }, - { url = "https://files.pythonhosted.org/packages/05/2d/e1d8f74ec9d95daf57f3c53083c98a2145ee895a4f8502c61c9013c9bf5a/uv-0.9.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:214bb2fb4d87a55e2ba2bc038a8b646a24ec66980528d2ed1e6e7d0612d246e1", size = 20564971, upload-time = "2025-10-15T15:20:00.012Z" }, - { url = "https://files.pythonhosted.org/packages/bc/04/4aaf90e031f0735795407a208c9528f85b0b27b63409abe4ee3bee0d4527/uv-0.9.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ccf4cd2e1907fb011764f6f4bc0e514c500e8d300288f04a4680400d5aa205ec", size = 21506573, upload-time = "2025-10-15T15:20:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/c0747c9b307a91618e483b0cd78ba076578df70359f08c9096f36b0dae93/uv-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:b2f934737c93f88c906b6a47bcc083170210fe5d66565e80a7c139599e5cbf2f", size = 20632765, upload-time = "2025-10-29T19:39:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d8/eba09583108476b9c21f4e09427553af7c5516a21ac01a18c63c357bcd72/uv-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a7c6067919d87208c4a6092033c3bc9799cb8be1c8bc6ef419a1f6d42a755329", size = 19666984, upload-time = "2025-10-29T19:39:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a3/8d7da102542995ed8b16ae6079ae853221e17a5eec1fff442e6eacf5760c/uv-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:95a62c1f668272555ad0c446bf44a9924dee06054b831d04c162e0bad736dc28", size = 18335059, upload-time = "2025-10-29T19:40:05.138Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0b/597719ad347610588954730f1124761184a6b71cf5aa1600f0a992939ef4/uv-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0169a85d3ba5ef1c37089d64ff26de573439ca84ecf549276a2eee42d7f833f2", size = 20144462, upload-time = "2025-10-29T19:40:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/31/cf/3f87025168bb377994ea468fc8757d5e01062b3888ec23eddd9b6d119135/uv-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ba311b3ca49d246f36d444d3ee81571619ef95e5f509eb694a81defcbed262", size = 20251834, upload-time = "2025-10-29T19:40:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/55/a0/a4027a220756a88dbc8bb7a6896fffc0e70af9b9ab030d644ab8baba3793/uv-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e89c964f614fa3f0481060cac709d6da50feac553e1e11227d6c4c81c87af7c", size = 21172738, upload-time = "2025-10-29T19:40:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f6/d9fd69247c8c3039c6818ceb20652d18322a874e10f6def3f05599ed8d07/uv-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ea67369918af24ea7e01991dfc8b8988d1b0b7c49cb39d9e5bc0c409930a0a3f", size = 22756338, upload-time = "2025-10-29T19:40:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f6/6a0b4f43675c48138d62a6ddb5aebed67a1c283bee3758e5258a75f000ed/uv-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90122a76e6441b8c580fc9faf06bd8c4dbe276cb1c185ad91eceb2afa78e492a", size = 22334132, upload-time = "2025-10-29T19:40:18.862Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3f/a17d6e26a795a2e7d6023bae9c5af7da3118eebc23053ec7c0cbbb603638/uv-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86e05782f9b75d39ab1c0af98bf11e87e646a36a61d425021d5b284073e56315", size = 21487365, upload-time = "2025-10-29T19:40:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/9cbafd47012a68b39902ff022bd1c7051384fcc23392b2d813d0f418e61f/uv-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2c2b2b093330e603d838fec26941ab6f62e8d62a012f9fa0d5ed88da39d907", size = 21384698, upload-time = "2025-10-29T19:40:24.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/525978cfa7c27ca2616ca0d214460861a8046085c032a0de6c5bedddcf6c/uv-0.9.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e700b2098f9d365061c572d0729b4e8bc71c6468d83dfaae2537cd66e3cb1b98", size = 20255252, upload-time = "2025-10-29T19:40:26.757Z" }, + { url = "https://files.pythonhosted.org/packages/10/6f/89040302486b83e2085579ffebe3078dc92b15f42406f986d9e690e47f1b/uv-0.9.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6403176b55388cf94fb8737e73b26ee2a7b1805a9139da5afa951210986d4fcd", size = 21308498, upload-time = "2025-10-29T19:40:29.273Z" }, + { url = "https://files.pythonhosted.org/packages/39/6e/5a3e879f7399c36c97d0b893c2dd5e91b76315c41793f13f86ff2091191a/uv-0.9.6-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:62e3f057a9ae5e5003a7cd56b617e940f519f6dabcbb22d36cdd0149df25d409", size = 20230221, upload-time = "2025-10-29T19:40:32.161Z" }, + { url = "https://files.pythonhosted.org/packages/7a/66/5bdabfd7afc6b429d8be7d6dc6446709f657621384960ec8b85e0088a3d9/uv-0.9.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:538716ec97f8d899baa7e1c427f4411525459c0ef72ea9b3625ce9610c9976e6", size = 20625876, upload-time = "2025-10-29T19:40:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/5d/34/257747253ad446fd155e39f0c30afda4597b3b9e28f44a9de5dee76a6509/uv-0.9.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b31377ebf2d0499afc5abe3fe1abded5ca843f3a1161b432fe26eb0ce15bab8e", size = 21597889, upload-time = "2025-10-29T19:40:36.963Z" }, ] [[package]] From 8b58718ffff4e197c78a3ffe6510584a4120cf4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:25:09 +0000 Subject: [PATCH 26/57] Chore(deps): Bump marshmallow from 3.26.1 to 3.26.2 (#11790) Bumps [marshmallow](https://github.com/marshmallow-code/marshmallow) from 3.26.1 to 3.26.2. - [Changelog](https://github.com/marshmallow-code/marshmallow/blob/dev/CHANGELOG.rst) - [Commits](https://github.com/marshmallow-code/marshmallow/compare/3.26.1...3.26.2) --- updated-dependencies: - dependency-name: marshmallow dependency-version: 3.26.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index e0919e2a7..2faa59d29 100644 --- a/uv.lock +++ b/uv.lock @@ -2315,14 +2315,14 @@ wheels = [ [[package]] name = "marshmallow" -version = "3.26.1" +version = "3.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, ] [[package]] From 939b2f7553fef2052d260e00728c748709d26a82 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:35:49 -0800 Subject: [PATCH 27/57] Chore: Fixes Docker image pushing for every PR we get (#11777) --- .github/workflows/ci-docker.yml | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 7ecdb055c..2fd465fdd 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -35,7 +35,7 @@ jobs: contents: read packages: write outputs: - can-push: ${{ steps.check-push.outputs.can-push }} + should-push: ${{ steps.check-push.outputs.should-push }} push-external: ${{ steps.check-push.outputs.push-external }} repository: ${{ steps.repo.outputs.name }} ref-name: ${{ steps.ref.outputs.name }} @@ -59,16 +59,28 @@ jobs: env: REF_NAME: ${{ steps.ref.outputs.name }} run: | - # can-push: Can we push to GHCR? - # True for: pushes, or PRs from the same repo (not forks) - can_push=${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} - echo "can-push=${can_push}" - echo "can-push=${can_push}" >> $GITHUB_OUTPUT + # should-push: Should we push to GHCR? + # True for: + # 1. Pushes (tags/dev/beta) - filtered via the workflow triggers + # 2. Internal PRs where the branch name starts with 'feature-' - filtered here when a PR is synced + + should_push="false" + + if [[ "${{ github.event_name }}" == "push" ]]; then + should_push="true" + elif [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then + if [[ "${REF_NAME}" == feature-* || "${REF_NAME}" == fix-* ]]; then + should_push="true" + fi + fi + + echo "should-push=${should_push}" + echo "should-push=${should_push}" >> $GITHUB_OUTPUT # push-external: Should we also push to Docker Hub and Quay.io? # Only for main repo on dev/beta branches or version tags push_external="false" - if [[ "${can_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then + if [[ "${should_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then case "${REF_NAME}" in dev|beta) push_external="true" @@ -125,20 +137,20 @@ jobs: labels: ${{ steps.docker-meta.outputs.labels }} build-args: | PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.can-push }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.should-push }} cache-from: | type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }} type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }} - cache-to: ${{ steps.check-push.outputs.can-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }} + cache-to: ${{ steps.check-push.outputs.should-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }} - name: Export digest - if: steps.check-push.outputs.can-push == 'true' + if: steps.check-push.outputs.should-push == 'true' run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" echo "digest=${digest}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - if: steps.check-push.outputs.can-push == 'true' + if: steps.check-push.outputs.should-push == 'true' uses: actions/upload-artifact@v6.0.0 with: name: digests-${{ matrix.arch }} @@ -149,7 +161,7 @@ jobs: name: Merge and Push Manifest runs-on: ubuntu-24.04 needs: build-arch - if: needs.build-arch.outputs.can-push == 'true' + if: needs.build-arch.outputs.should-push == 'true' permissions: contents: read packages: write From 742c1367732f303f1d2735782f6ae4f122c2ae2c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:49:21 -0800 Subject: [PATCH 28/57] Fix: use explicit order field for workflow actions (#11781) --- .../workflow-edit-dialog.component.spec.ts | 4 +-- .../workflow-edit-dialog.component.ts | 5 ---- .../migrations/1076_workflowaction_order.py | 28 +++++++++++++++++++ src/documents/models.py | 2 ++ src/documents/serialisers.py | 13 ++++++++- src/documents/signals/handlers.py | 2 +- src/documents/workflows/utils.py | 20 ++++++++++--- 7 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 src/documents/migrations/1076_workflowaction_order.py diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts index aa52592b1..fafc9e876 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts @@ -252,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => { expect(component.object.actions.length).toEqual(2) }) - it('should update order and remove ids from actions on drag n drop', () => { + it('should update order on drag n drop', () => { const action1 = workflow.actions[0] const action2 = workflow.actions[1] component.object = workflow @@ -261,8 +261,6 @@ describe('WorkflowEditDialogComponent', () => { WorkflowAction[] >) expect(component.object.actions).toEqual([action2, action1]) - expect(action1.id).toBeNull() - expect(action2.id).toBeNull() }) it('should not include auto matching in algorithms', () => { diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index f6d9e60f5..74221e3f0 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -1283,11 +1283,6 @@ export class WorkflowEditDialogComponent const actionField = this.actionFields.at(event.previousIndex) this.actionFields.removeAt(event.previousIndex) this.actionFields.insert(event.currentIndex, actionField) - // removing id will effectively re-create the actions in this order - this.object.actions.forEach((a) => (a.id = null)) - this.actionFields.controls.forEach((c) => - c.get('id').setValue(null, { emitEvent: false }) - ) } save(): void { diff --git a/src/documents/migrations/1076_workflowaction_order.py b/src/documents/migrations/1076_workflowaction_order.py new file mode 100644 index 000000000..5c9f7ff52 --- /dev/null +++ b/src/documents/migrations/1076_workflowaction_order.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-01-14 16:53 + +from django.db import migrations +from django.db import models +from django.db.models import F + + +def populate_action_order(apps, schema_editor): + WorkflowAction = apps.get_model("documents", "WorkflowAction") + WorkflowAction.objects.all().update(order=F("id")) + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1075_alter_paperlesstask_task_name"), + ] + + operations = [ + migrations.AddField( + model_name="workflowaction", + name="order", + field=models.PositiveIntegerField(default=0, verbose_name="order"), + ), + migrations.RunPython( + populate_action_order, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 12dab2b6d..c7c082a7b 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1294,6 +1294,8 @@ class WorkflowAction(models.Model): default=WorkflowActionType.ASSIGNMENT, ) + order = models.PositiveIntegerField(_("order"), default=0) + assign_title = models.TextField( _("assign title"), null=True, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5c71de9a9..0648aa0b3 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -2562,7 +2562,8 @@ class WorkflowSerializer(serializers.ModelSerializer): set_triggers.append(trigger_instance) if actions is not None and actions is not serializers.empty: - for action in actions: + for index, action in enumerate(actions): + action["order"] = index assign_tags = action.pop("assign_tags", None) assign_view_users = action.pop("assign_view_users", None) assign_view_groups = action.pop("assign_view_groups", None) @@ -2689,6 +2690,16 @@ class WorkflowSerializer(serializers.ModelSerializer): return instance + def to_representation(self, instance): + data = super().to_representation(instance) + actions = instance.actions.order_by("order", "pk") + data["actions"] = WorkflowActionSerializer( + actions, + many=True, + context=self.context, + ).data + return data + class TrashSerializer(SerializerWithPerms): documents = serializers.ListField( diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 4ec00258a..d6edb523a 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -769,7 +769,7 @@ def run_workflows( if matching.document_matches_workflow(document, workflow, trigger_type): action: WorkflowAction - for action in workflow.actions.all(): + for action in workflow.actions.order_by("order", "pk"): message = f"Applying {action} from {workflow}" if not use_overrides: logger.info(message, extra={"group": logging_group}) diff --git a/src/documents/workflows/utils.py b/src/documents/workflows/utils.py index 553622252..0a644b0eb 100644 --- a/src/documents/workflows/utils.py +++ b/src/documents/workflows/utils.py @@ -20,9 +20,6 @@ def get_workflows_for_trigger( wrap it in a list; otherwise fetch enabled workflows for the trigger with the prefetches used by the runner. """ - if workflow_to_run is not None: - return [workflow_to_run] - annotated_actions = ( WorkflowAction.objects.select_related( "assign_correspondent", @@ -105,10 +102,25 @@ def get_workflows_for_trigger( ) ) + action_prefetch = Prefetch( + "actions", + queryset=annotated_actions.order_by("order", "pk"), + ) + + if workflow_to_run is not None: + return ( + Workflow.objects.filter(pk=workflow_to_run.pk) + .prefetch_related( + action_prefetch, + "triggers", + ) + .distinct() + ) + return ( Workflow.objects.filter(enabled=True, triggers__type=trigger_type) .prefetch_related( - Prefetch("actions", queryset=annotated_actions), + action_prefetch, "triggers", ) .order_by("order") From 2f1cd31e31cd890360fa9a54c8d88a26c5d19591 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:42:54 -0800 Subject: [PATCH 29/57] Adds the release-drafter commitish filtering to perhaps generate the release notes better --- .github/release-drafter.yml | 1 + .github/workflows/ci.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 89c8a96ea..2b8169f24 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -44,6 +44,7 @@ include-labels: - 'notable' exclude-labels: - 'skip-changelog' +filter-by-commitish: true category-template: '### $TITLE' change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))' change-title-escapes: '\<*_&#@' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8eb6c832..6abd71dfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -617,6 +617,7 @@ jobs: version: ${{ steps.get_version.outputs.version }} prerelease: ${{ steps.get_version.outputs.prerelease }} publish: true # ensures release is not marked as draft + committish: ${{ github.sha }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload release archive From a9548afb42e33fb1e3623efd0654502bd72f8156 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:31:47 +0000 Subject: [PATCH 30/57] Chore(deps): Bump the ai-group (#11798) * Chore(deps): Bump llama-index-core from 0.12.33.post1 to 0.13.0 Bumps [llama-index-core](https://github.com/run-llama/llama_index) from 0.12.33.post1 to 0.13.0. - [Release notes](https://github.com/run-llama/llama_index/releases) - [Changelog](https://github.com/run-llama/llama_index/blob/main/CHANGELOG.md) - [Commits](https://github.com/run-llama/llama_index/commits/v0.13.0) --- updated-dependencies: - dependency-name: llama-index-core dependency-version: 0.13.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Update llama-index to latest versions * Fix embedding mock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- pyproject.toml | 12 +- src/paperless_ai/tests/test_chat.py | 10 +- uv.lock | 272 +++++++++++++++++----------- 3 files changed, 179 insertions(+), 115 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e23a589e8..f99b9d72f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,12 +53,12 @@ dependencies = [ "inotifyrecursive~=0.3", "jinja2~=3.1.5", "langdetect~=1.0.9", - "llama-index-core>=0.12.33.post1", - "llama-index-embeddings-huggingface>=0.5.3", - "llama-index-embeddings-openai>=0.3.1", - "llama-index-llms-ollama>=0.5.4", - "llama-index-llms-openai>=0.3.38", - "llama-index-vector-stores-faiss>=0.3", + "llama-index-core>=0.14.12", + "llama-index-embeddings-huggingface>=0.6.1", + "llama-index-embeddings-openai>=0.5.1", + "llama-index-llms-ollama>=0.9.1", + "llama-index-llms-openai>=0.6.13", + "llama-index-vector-stores-faiss>=0.5.2", "nltk~=3.9.1", "ocrmypdf~=16.12.0", "openai>=1.76", diff --git a/src/paperless_ai/tests/test_chat.py b/src/paperless_ai/tests/test_chat.py index c91488cf1..688d78058 100644 --- a/src/paperless_ai/tests/test_chat.py +++ b/src/paperless_ai/tests/test_chat.py @@ -11,14 +11,12 @@ from paperless_ai.chat import stream_chat_with_documents @pytest.fixture(autouse=True) def patch_embed_model(): from llama_index.core import settings as llama_settings + from llama_index.core.embeddings.mock_embed_model import MockEmbedding - mock_embed_model = MagicMock() - mock_embed_model._get_text_embedding_batch.return_value = [ - [0.1] * 1536, - ] # 1 vector per input - llama_settings.Settings._embed_model = mock_embed_model + # Use a real BaseEmbedding subclass to satisfy llama-index 0.14 validation + llama_settings.Settings.embed_model = MockEmbedding(embed_dim=1536) yield - llama_settings.Settings._embed_model = None + llama_settings.Settings.embed_model = None @pytest.fixture(autouse=True) diff --git a/uv.lock b/uv.lock index 2faa59d29..7413d745c 100644 --- a/uv.lock +++ b/uv.lock @@ -146,6 +146,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "amqp" version = "5.3.1" @@ -288,7 +297,7 @@ wheels = [ [[package]] name = "banks" -version = "2.1.2" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -297,9 +306,9 @@ dependencies = [ { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/34/2b6697f02ffb68bee50e5fd37d6c64432244d3245603fd62950169dfed7e/banks-2.1.2.tar.gz", hash = "sha256:a0651db9d14b57fa2e115e78f68dbb1b36fe226ad6eef96192542908b1d20c1f", size = 173332, upload-time = "2025-04-20T07:09:21.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/f8/25ef24814f77f3fd7f0fd3bd1ef3749e38a9dbd23502fbb53034de49900c/banks-2.2.0.tar.gz", hash = "sha256:d1446280ce6e00301e3e952dd754fd8cee23ff277d29ed160994a84d0d7ffe62", size = 179052, upload-time = "2025-07-18T16:28:26.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4a/7fdca29d1db62f5f5c3446bf8f668beacdb0b5a8aff4247574ddfddc6bcd/banks-2.1.2-py3-none-any.whl", hash = "sha256:7fba451069f6bea376483b8136a0f29cb1e6883133626d00e077e20a3d102c0e", size = 28064, upload-time = "2025-04-20T07:09:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/f9168956276934162ec8d48232f9920f2985ee45aa7602e3c6b4bc203613/banks-2.2.0-py3-none-any.whl", hash = "sha256:963cd5c85a587b122abde4f4064078def35c50c688c1b9d36f43c92503854e7d", size = 29244, upload-time = "2025-07-18T16:28:27.835Z" }, ] [[package]] @@ -2017,10 +2026,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2 [[package]] name = "llama-index-core" -version = "0.12.33.post1" +version = "0.14.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "aiosqlite", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "banks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "dataclasses-json", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2028,15 +2038,18 @@ dependencies = [ { name = "filetype", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-workflows", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "nest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "nltk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "numpy", version = "2.2.6", 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 = "numpy", version = "2.3.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 = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sqlalchemy", extra = ["asyncio"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2045,74 +2058,101 @@ dependencies = [ { name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/51/e99358e80b0d80777c84081159d351f51feaa6c7d7054486bbbb49f6c9c0/llama_index_core-0.12.33.post1.tar.gz", hash = "sha256:d257f6f594dfd9cf6435af02761a3d21f1427df5347f0e5e9fffe4024db6a724", size = 7282200, upload-time = "2025-04-23T18:48:42.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/d3/9d65f3c631a41fbb0dac47c52adad0fdbbaee3456518a97d558d8c754788/llama_index_core-0.14.12.tar.gz", hash = "sha256:6917e5865c6c789046dca001ebeea5a7f80e1ba83ac646dc793aaa041e8feb12", size = 11584083, upload-time = "2025-12-30T01:06:24.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/01/6fcf557a72ad25734327515db506744f8f8ba95846a0f7e055c8fa95a54d/llama_index_core-0.12.33.post1-py3-none-any.whl", hash = "sha256:2c4a316a1ae9ec86c817d44961d1058691632acb3a7021e6af56fcfb8735fd3d", size = 7650733, upload-time = "2025-04-23T18:48:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c7/a39d65aeb7b1df234719e918a27b9122eeee81744e2e39f60b12a57a3e09/llama_index_core-0.14.12-py3-none-any.whl", hash = "sha256:a3a7e3a084b01700458874dd635ba40d00af2680daa47f072e357e8f0d172872", size = 11927498, upload-time = "2025-12-30T01:06:27.459Z" }, ] [[package]] name = "llama-index-embeddings-huggingface" -version = "0.5.3" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub", extra = ["inference"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/4b/35c4f1623fc33b60a91490c0849b8bdb6c704f18cade400ee5baaf064c0d/llama_index_embeddings_huggingface-0.5.3.tar.gz", hash = "sha256:3fecb363cc0d05890689aefc2d1bda2f02431857e0456fdaf2e8c4960f9daeaf", size = 7947, upload-time = "2025-04-08T21:48:42.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/a0/77beca4ed28af68db6ab9c647b3fa75fae905d33ace96e91010cc9b96027/llama_index_embeddings_huggingface-0.6.1.tar.gz", hash = "sha256:3b21ffeda22f8221ed55778bb3daed71664ab07b341f1dd2f408963bd20355b9", size = 8694, upload-time = "2025-09-08T20:25:27.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/21/8c201efcd3c69b4ccb7a876ccd80464525d2c1ba49bec9a99f91604055d9/llama_index_embeddings_huggingface-0.5.3-py3-none-any.whl", hash = "sha256:f181d6490ebb29f0e7ada93b53fdd9acf5808387cdd69b9f1b499a2380d38758", size = 8951, upload-time = "2025-04-08T21:48:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b0/4d327cb2f2e039606feb9345b4de975090b9659c4ee84b00443bb149a80d/llama_index_embeddings_huggingface-0.6.1-py3-none-any.whl", hash = "sha256:b63990cf71ee7a36c51f36657133fcf76130e9bf5dcf9eb5a73a5087106d6881", size = 8903, upload-time = "2025-09-08T20:25:27.038Z" }, ] [[package]] name = "llama-index-embeddings-openai" -version = "0.3.1" +version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-core", 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/a1/02/a2604ef3a167131fdd701888f45f16c8efa6d523d02efe8c4e640238f4ea/llama_index_embeddings_openai-0.3.1.tar.gz", hash = "sha256:1368aad3ce24cbaed23d5ad251343cef1eb7b4a06d6563d6606d59cb347fef20", size = 5492, upload-time = "2024-11-27T16:04:17.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/36/90336d054a5061a3f5bc17ac2c18ef63d9d84c55c14d557de484e811ea4d/llama_index_embeddings_openai-0.5.1.tar.gz", hash = "sha256:1c89867a48b0d0daa3d2d44f5e76b394b2b2ef9935932daf921b9e77939ccda8", size = 7020, upload-time = "2025-09-08T20:17:44.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/45/ca55b91c4ac1b6251d4099fa44121a6c012129822906cadcc27b8cfb33a4/llama_index_embeddings_openai-0.3.1-py3-none-any.whl", hash = "sha256:f15a3d13da9b6b21b8bd51d337197879a453d1605e625a1c6d45e741756c0290", size = 6177, upload-time = "2024-11-27T16:04:15.981Z" }, + { url = "https://files.pythonhosted.org/packages/23/4a/8ab11026cf8deff8f555aa73919be0bac48332683111e5fc4290f352dc50/llama_index_embeddings_openai-0.5.1-py3-none-any.whl", hash = "sha256:a2fcda3398bbd987b5ce3f02367caee8e84a56b930fdf43cc1d059aa9fd20ca5", size = 7011, upload-time = "2025-09-08T20:17:44.015Z" }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/b9/a7a74de6d8aacf4be329329495983d78d96b1a6e69b6d9fcf4a233febd4b/llama_index_instrumentation-0.4.2.tar.gz", hash = "sha256:dc4957b64da0922060690e85a6be9698ac08e34e0f69e90b01364ddec4f3de7f", size = 46146, upload-time = "2025-10-13T20:44:48.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/54/df8063b0441242e250e03d1e31ebde5dffbe24e1af32b025cb1a4544150c/llama_index_instrumentation-0.4.2-py3-none-any.whl", hash = "sha256:b4989500e6454059ab3f3c4a193575d47ab1fadb730c2e8f2b962649ae88b70b", size = 15411, upload-time = "2025-10-13T20:44:47.685Z" }, ] [[package]] name = "llama-index-llms-ollama" -version = "0.5.4" +version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/05/bd6a1006d93566a65cb5ef8318bca8695a9a7cfc6064c1bd218aae3a5ded/llama_index_llms_ollama-0.5.4.tar.gz", hash = "sha256:e5e7e7a4e65478c762906d08f594647d8148ffdc32ae908d56b73c0df8ea04f2", size = 8218, upload-time = "2025-03-27T01:05:04.409Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/42/866e43f86ebe6c8ae0c3a2a473642f1438d0f7e006188a998c3ef4e5e357/llama_index_llms_ollama-0.9.1.tar.gz", hash = "sha256:d5885ed65ae2e2bc74ba9e3ffd3a5bcd7c5341ef0670e3d9fe200880fc19f9a6", size = 9077, upload-time = "2025-12-19T03:24:46.466Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/1d/f000f50525dff87e6a78d7d7623720fdfc14b95a04b25e2bc4e936af2907/llama_index_llms_ollama-0.5.4-py3-none-any.whl", hash = "sha256:203688429a99454ae261877dd48f68f65374edf92ba25796a6a8a2346fd005f0", size = 7790, upload-time = "2025-03-27T01:05:03.611Z" }, + { url = "https://files.pythonhosted.org/packages/be/cd/aa2499dc38c1cc513cab56f9a74e5526d1a97717f35a62295fda0d363bf6/llama_index_llms_ollama-0.9.1-py3-none-any.whl", hash = "sha256:da6469be951605841f7e33ce1bbb5b72c65de0f084b250e7f78aacac84379d5f", size = 8721, upload-time = "2025-12-19T03:24:47.438Z" }, ] [[package]] name = "llama-index-llms-openai" -version = "0.3.38" +version = "0.6.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-core", 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/4d/bd/b0ceae2d5d697feb5d18a7402214cdad30bc20d8cbe1619e9e6355361ca5/llama_index_llms_openai-0.3.38.tar.gz", hash = "sha256:bcd1d5212bf7c948301958719a1df361be62b37b5620732e4c9ce804bc078b77", size = 22738, upload-time = "2025-04-21T21:52:08.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/f9/8ee42db8ce58e72846c119ba2061facbf3475f648506990a61ccf0d5d643/llama_index_llms_openai-0.6.13.tar.gz", hash = "sha256:e3b7422bc72276e00a980d826477d0b14d5bf743ba69c4a4f0bdee0f5225d450", size = 25784, upload-time = "2026-01-13T11:42:12.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/e1/1c185e22ca1fd1ac813d225be046c4223dbe2fdf64d90a6e86608e6d17ad/llama_index_llms_openai-0.3.38-py3-none-any.whl", hash = "sha256:d724b809d5e81e15cd1c3def65f023c4c74f2a097e542e5c002793ffbaa33a96", size = 23839, upload-time = "2025-04-21T21:52:06.99Z" }, + { url = "https://files.pythonhosted.org/packages/70/42/ab78f9c472d99552e4a1227a097d017b2f8ec8814cfec1e9813c036cd37d/llama_index_llms_openai-0.6.13-py3-none-any.whl", hash = "sha256:f0f8665381eb8e553de187a492da999830817733357364f151a3d4f3e3db746f", size = 26787, upload-time = "2026-01-13T11:42:13.812Z" }, ] [[package]] name = "llama-index-vector-stores-faiss" -version = "0.3.0" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ec/6890f61bbdd5afa52c4b133e145d06f22121334f7d615a1f6b8879beb35b/llama_index_vector_stores_faiss-0.3.0.tar.gz", hash = "sha256:c9df99dd00fe7058606ef4fce113535fa30b73edd650136be87c9b5b240df3f9", size = 3454, upload-time = "2024-11-17T22:55:00.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/5f/c4ae340f178f202cf09dcc24dd0953a41d9ab24bc33e1f7220544ba86e41/llama_index_vector_stores_faiss-0.5.2.tar.gz", hash = "sha256:924504765e68b1f84ec602feb2d9516be6a6c12fad5e133f19cc5da3b23f4282", size = 5910, upload-time = "2025-12-17T21:01:13.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/16/d3f512f47d40a27ee1c75ce86faa91004de9c20202ca376fe35bc92ba413/llama_index_vector_stores_faiss-0.3.0-py3-none-any.whl", hash = "sha256:2148163dba1222c855bd367a7b796bc35d46dc2e77d57bafd321ba14aac00177", size = 3868, upload-time = "2024-11-17T22:54:59.249Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c1/c8317250c2a83d1d439814d1a7f41fa34a23c224b3099da898f08a249859/llama_index_vector_stores_faiss-0.5.2-py3-none-any.whl", hash = "sha256:72a3a03d9f25c70bbcc8c61aa860cd1db69f2a8070606ecc3266d767b71ff2a2", size = 7605, upload-time = "2025-12-17T21:01:12.429Z" }, +] + +[[package]] +name = "llama-index-workflows" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", 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/d1/ee/8c58554942933f33752ccb86451ea0a15493808eb934f4899e4d2c43a408/llama_index_workflows-2.12.2.tar.gz", hash = "sha256:37e05cd3483c64f410176fe614db8c84b6f42fc32cdadb3cc8ac8de18f01a97b", size = 79771, upload-time = "2026-01-15T20:30:24.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2a/4188d3cb65c539fcac3a467f77d7ac32d6136d0802d0b2ba113d51adfbf6/llama_index_workflows-2.12.2-py3-none-any.whl", hash = "sha256:888baf7e557f7fbe10d442f354bdc3415390757f0ac9268d32f89401128ae508", size = 102617, upload-time = "2026-01-15T20:30:25.42Z" }, ] [[package]] @@ -2812,20 +2852,20 @@ wheels = [ [[package]] name = "ollama" -version = "0.4.8" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/64/709dc99030f8f46ec552f0a7da73bbdcc2da58666abfec4742ccdb2e800e/ollama-0.4.8.tar.gz", hash = "sha256:1121439d49b96fa8339842965d0616eba5deb9f8c790786cdf4c0b3df4833802", size = 12972, upload-time = "2025-04-16T21:55:14.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/3f/164de150e983b3a16e8bf3d4355625e51a357e7b3b1deebe9cc1f7cb9af8/ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c", size = 13325, upload-time = "2025-04-16T21:55:12.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, ] [[package]] name = "openai" -version = "1.76.0" +version = "1.109.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2837,9 +2877,9 @@ dependencies = [ { name = "tqdm", 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/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660, upload-time = "2025-04-23T16:33:53.266Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201, upload-time = "2025-04-23T16:33:51.12Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, ] [[package]] @@ -3050,12 +3090,12 @@ requires-dist = [ { name = "inotifyrecursive", specifier = "~=0.3" }, { name = "jinja2", specifier = "~=3.1.5" }, { name = "langdetect", specifier = "~=1.0.9" }, - { name = "llama-index-core", specifier = ">=0.12.33.post1" }, - { name = "llama-index-embeddings-huggingface", specifier = ">=0.5.3" }, - { name = "llama-index-embeddings-openai", specifier = ">=0.3.1" }, - { name = "llama-index-llms-ollama", specifier = ">=0.5.4" }, - { name = "llama-index-llms-openai", specifier = ">=0.3.38" }, - { name = "llama-index-vector-stores-faiss", specifier = ">=0.3" }, + { name = "llama-index-core", specifier = ">=0.14.12" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.6.1" }, + { name = "llama-index-embeddings-openai", specifier = ">=0.5.1" }, + { name = "llama-index-llms-ollama", specifier = ">=0.9.1" }, + { name = "llama-index-llms-openai", specifier = ">=0.6.13" }, + { name = "llama-index-vector-stores-faiss", specifier = ">=0.5.2" }, { name = "mysqlclient", marker = "extra == 'mariadb'", specifier = "~=2.2.7" }, { name = "nltk", specifier = "~=3.9.1" }, { name = "ocrmypdf", specifier = "~=16.12.0" }, @@ -3626,7 +3666,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.3" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3634,82 +3674,108 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload-time = "2025-04-08T13:27:06.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload-time = "2025-04-08T13:27:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload-time = "2025-04-02T09:49:41.8Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021, upload-time = "2025-04-02T09:46:45.065Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742, upload-time = "2025-04-02T09:46:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414, upload-time = "2025-04-02T09:46:48.263Z" }, - { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848, upload-time = "2025-04-02T09:46:49.441Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055, upload-time = "2025-04-02T09:46:50.602Z" }, - { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806, upload-time = "2025-04-02T09:46:52.116Z" }, - { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777, upload-time = "2025-04-02T09:46:53.675Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803, upload-time = "2025-04-02T09:46:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755, upload-time = "2025-04-02T09:46:56.956Z" }, - { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358, upload-time = "2025-04-02T09:46:58.445Z" }, - { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916, upload-time = "2025-04-02T09:46:59.726Z" }, - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224, upload-time = "2025-04-02T09:47:04.199Z" }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845, upload-time = "2025-04-02T09:47:05.686Z" }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029, upload-time = "2025-04-02T09:47:07.042Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784, upload-time = "2025-04-02T09:47:08.63Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075, upload-time = "2025-04-02T09:47:10.267Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849, upload-time = "2025-04-02T09:47:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794, upload-time = "2025-04-02T09:47:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237, upload-time = "2025-04-02T09:47:14.355Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351, upload-time = "2025-04-02T09:47:15.676Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914, upload-time = "2025-04-02T09:47:17Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385, upload-time = "2025-04-02T09:47:18.631Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640, upload-time = "2025-04-02T09:47:25.394Z" }, - { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649, upload-time = "2025-04-02T09:47:27.417Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472, upload-time = "2025-04-02T09:47:29.006Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509, upload-time = "2025-04-02T09:47:33.464Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702, upload-time = "2025-04-02T09:47:34.812Z" }, - { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428, upload-time = "2025-04-02T09:47:37.315Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753, upload-time = "2025-04-02T09:47:39.013Z" }, - { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849, upload-time = "2025-04-02T09:47:40.427Z" }, - { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541, upload-time = "2025-04-02T09:47:42.01Z" }, - { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225, upload-time = "2025-04-02T09:47:43.425Z" }, - { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373, upload-time = "2025-04-02T09:47:44.979Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload-time = "2025-04-02T09:47:51.648Z" }, - { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload-time = "2025-04-02T09:47:53.149Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload-time = "2025-04-02T09:47:55.006Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload-time = "2025-04-02T09:47:56.532Z" }, - { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload-time = "2025-04-02T09:47:58.088Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload-time = "2025-04-02T09:47:59.591Z" }, - { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload-time = "2025-04-02T09:48:01.397Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload-time = "2025-04-02T09:48:03.056Z" }, - { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload-time = "2025-04-02T09:48:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload-time = "2025-04-02T09:48:06.226Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload-time = "2025-04-02T09:48:08.114Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload-time = "2025-04-02T09:48:14.553Z" }, - { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload-time = "2025-04-02T09:48:16.222Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659, upload-time = "2025-04-02T09:48:45.342Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294, upload-time = "2025-04-02T09:48:47.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771, upload-time = "2025-04-02T09:48:49.468Z" }, - { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558, upload-time = "2025-04-02T09:48:51.409Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038, upload-time = "2025-04-02T09:48:53.702Z" }, - { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315, upload-time = "2025-04-02T09:48:55.555Z" }, - { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063, upload-time = "2025-04-02T09:48:57.479Z" }, - { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631, upload-time = "2025-04-02T09:48:59.581Z" }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858, upload-time = "2025-04-02T09:49:03.419Z" }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745, upload-time = "2025-04-02T09:49:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188, upload-time = "2025-04-02T09:49:07.352Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479, upload-time = "2025-04-02T09:49:09.304Z" }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415, upload-time = "2025-04-02T09:49:11.25Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623, upload-time = "2025-04-02T09:49:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175, upload-time = "2025-04-02T09:49:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674, upload-time = "2025-04-02T09:49:17.61Z" }, + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, ] [[package]] @@ -5342,14 +5408,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] From c471c201ee5f165ad78d677942a2c820acbc0c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:11:04 -0800 Subject: [PATCH 31/57] Chore(deps): Bump django from 5.2.7 to 5.2.9 (#11794) Bumps [django](https://github.com/django/django) from 5.2.7 to 5.2.9. - [Commits](https://github.com/django/django/compare/5.2.7...5.2.9) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.9 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 7413d745c..470a163e7 100644 --- a/uv.lock +++ b/uv.lock @@ -900,15 +900,15 @@ wheels = [ [[package]] name = "django" -version = "5.2.7" +version = "5.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", 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/b1/96/bd84e2bb997994de8bcda47ae4560991084e86536541d7214393880f01a8/django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd", size = 10865812, upload-time = "2025-10-01T14:22:12.081Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/1c/188ce85ee380f714b704283013434976df8d3a2df8e735221a02605b6794/django-5.2.9.tar.gz", hash = "sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495", size = 10848762, upload-time = "2025-12-02T14:01:08.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ef/81f3372b5dd35d8d354321155d1a38894b2b766f576d0abffac4d8ae78d9/django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b", size = 8307145, upload-time = "2025-10-01T14:22:49.476Z" }, + { url = "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a", size = 8290652, upload-time = "2025-12-02T14:01:03.485Z" }, ] [[package]] From 4a7f9fa98432e71743bf236c5eff4d325040a28d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:25:52 +0000 Subject: [PATCH 32/57] Chore(deps): Bump transformers from 4.51.3 to 4.53.0 (#11797) Bumps [transformers](https://github.com/huggingface/transformers) from 4.51.3 to 4.53.0. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.51.3...v4.53.0) --- updated-dependencies: - dependency-name: transformers dependency-version: 4.53.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 470a163e7..9f33e7ff8 100644 --- a/uv.lock +++ b/uv.lock @@ -5163,7 +5163,7 @@ wheels = [ [[package]] name = "transformers" -version = "4.51.3" +version = "4.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -5178,9 +5178,9 @@ dependencies = [ { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/40/f2d2c3bcf5c6135027cab0fd7db52f6149a1c23acc4e45f914c43d362386/transformers-4.53.0.tar.gz", hash = "sha256:f89520011b4a73066fdc7aabfa158317c3934a22e3cd652d7ffbc512c4063841", size = 9177265, upload-time = "2025-06-26T16:10:54.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0c/68d03a38f6ab2ba2b2829eb11b334610dd236e7926787f7656001b68e1f2/transformers-4.53.0-py3-none-any.whl", hash = "sha256:7d8039ff032c01a2d7f8a8fe0066620367003275f023815a966e62203f9f5dd7", size = 10821970, upload-time = "2025-06-26T16:10:51.505Z" }, ] [[package]] From 155d69b2119eec0fcd30524d6dba6062a1c9dbb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:16:34 -0800 Subject: [PATCH 33/57] Chore(deps): Bump brotli from 1.1.0 to 1.2.0 (#11796) Bumps [brotli](https://github.com/google/brotli) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/google/brotli/releases) - [Changelog](https://github.com/google/brotli/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/brotli/compare/go/cbrotli/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: brotli dependency-version: 1.2.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 98 +++++++++++++++++++++++++-------------------------------- 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/uv.lock b/uv.lock index 9f33e7ff8..b3885be19 100644 --- a/uv.lock +++ b/uv.lock @@ -334,64 +334,50 @@ wheels = [ [[package]] name = "brotli" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload-time = "2023-09-07T14:03:16.894Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload-time = "2023-09-07T14:03:18.917Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload-time = "2023-09-07T14:03:20.398Z" }, - { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload-time = "2023-09-07T14:03:21.914Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload-time = "2023-09-07T14:03:24Z" }, - { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload-time = "2023-09-07T14:03:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload-time = "2023-09-07T14:03:27.849Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload-time = "2023-09-07T14:03:29.92Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload-time = "2023-09-07T14:03:32.035Z" }, - { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload-time = "2023-09-07T14:03:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload-time = "2024-10-18T12:32:09.016Z" }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload-time = "2024-10-18T12:32:11.134Z" }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload-time = "2024-10-18T12:32:12.813Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload-time = "2024-10-18T12:32:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload-time = "2023-09-07T14:03:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload-time = "2023-09-07T14:03:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload-time = "2023-09-07T14:03:46.594Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload-time = "2023-09-07T14:03:48.204Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload-time = "2023-09-07T14:03:50.348Z" }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload-time = "2023-09-07T14:03:52.395Z" }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload-time = "2023-09-07T14:03:53.96Z" }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload-time = "2024-10-18T12:32:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload-time = "2024-10-18T12:32:18.459Z" }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload-time = "2024-10-18T12:32:20.192Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload-time = "2024-10-18T12:32:21.774Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, ] [[package]] From d447a9fb3253d282d7b698bef0224dfb7de2e88a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:43:43 -0800 Subject: [PATCH 34/57] docker(deps): Bump astral-sh/uv (#11762) Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.9.15-python3.12-trixie-slim to 0.9.24-python3.12-trixie-slim. - [Release notes](https://github.com/astral-sh/uv/releases) - [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/uv/compare/0.9.15...0.9.24) --- updated-dependencies: - dependency-name: astral-sh/uv dependency-version: 0.9.24-python3.12-trixie-slim dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 419b19b3d..ba0592509 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN set -eux \ # Purpose: Installs s6-overlay and rootfs # Comments: # - Don't leave anything extra in here either -FROM ghcr.io/astral-sh/uv:0.9.15-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 From 56fddf1e5803bcd73f7f40c5f78a54d9a18c6247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:03:04 -0800 Subject: [PATCH 35/57] Chore(deps): Bump torch from 2.7.1 to 2.8.0 (#11800) Bumps [torch](https://github.com/pytorch/pytorch) from 2.7.1 to 2.8.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.7.1...v2.8.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.8.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 48 ++++++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f99b9d72f..6b2d5359d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dependencies = [ "sentence-transformers>=4.1", "setproctitle~=1.3.4", "tika-client~=0.10.0", - "torch~=2.7.0", + "torch~=2.8.0", "tqdm~=4.67.1", "watchdog~=6.0", "whitenoise~=6.9", diff --git a/uv.lock b/uv.lock index b3885be19..898d431a3 100644 --- a/uv.lock +++ b/uv.lock @@ -2950,8 +2950,8 @@ dependencies = [ { name = "sentence-transformers", 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 = "torch", version = "2.7.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.7.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.8.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "watchdog", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3106,7 +3106,7 @@ requires-dist = [ { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, { name = "tika-client", specifier = "~=0.10.0" }, - { name = "torch", specifier = "~=2.7.0", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "tqdm", specifier = "~=4.67.1" }, { name = "watchdog", specifier = "~=6.0" }, { name = "whitenoise", specifier = "~=6.9" }, @@ -4750,8 +4750,8 @@ dependencies = [ { 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.16.2", 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.7.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.7.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.8.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "tqdm", 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'" }, @@ -5065,7 +5065,7 @@ wheels = [ [[package]] name = "torch" -version = "2.7.1" +version = "2.8.0" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -5082,16 +5082,16 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:f8c3bee261b0c8e090f6347490dc6ee2aebfd661eb0f3f6aeae06d992d8ed56f" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:68a352c7f435abb5cb47e2c032dcd1012772ae2bacb6fc8b83b0c1b11874ab3a" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7b4f8b2b83bd08f7d399025a9a7b323bdbb53d20566f1e0d584689bb92d82f9a" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:95af97e7b2cecdc89edc0558962a51921bf9c61538597dbec6b7cc48d31e2e13" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7ecd868a086468e1bcf74b91db425c1c2951a9cfcd0592c4c73377b7e42485ae" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a467b49fe893a6a6cce89e3aee556edfdc64a722d7195fdfdd75cec9dea13779" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" }, ] [[package]] name = "torch" -version = "2.7.1+cpu" +version = "2.8.0+cpu" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -5110,16 +5110,20 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c0df17cee97653d09a4e84488a33d21217f9b24208583c55cf28f0045aab0766" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f04a373a3f643821f721da9898ef77dce73b5b6bfc64486f0976f7fb5f90e83" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fe6045b8f426bf2d0426e4fe009f1667a954ec2aeb82f1bd0bf60c6d7a85445" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a1684793e352f03fa14f78857e55d65de4ada8405ded1da2bf4f452179c4b779" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf2db5adf77b433844f080887ade049c4705ddf9fe1a32023ff84ff735aa5ad" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8f8b3cfc53010a4b4a3c7ecb88c212e9decc4f5eeb6af75c3c803937d2d60947" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:eb17646792ac4374ffc87e42369f45d21eff17c790868963b90483ef0b6db4ef" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84ea1f6a1d15663037d01b121d6e33bb9da3c90af8e069e5072c30f413455a57" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:56136a2aca6707df3c8811e46ea2d379eaafd18e656e2fd51e8e4d0ca995651b" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:355614185a2aea7155f9c88a20bfd49de5f3063866f3cf9b2f21b6e9e59e31e0" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-linux_s390x.whl", hash = "sha256:5d255d259fbc65439b671580e40fdb8faea4644761b64fed90d6904ffe71bbc1" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b2149858b8340aeeb1f3056e0bff5b82b96e43b596fe49a9dba3184522261213" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:16d75fa4e96ea28a785dfd66083ca55eb1058b6d6c5413f01656ca965ee2077e" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" }, ] [[package]] From 3ea5e0513770002db7821a508be6b1f8bc204bd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:06:20 -0800 Subject: [PATCH 36/57] Chore(deps): Bump pyasn1 from 0.6.1 to 0.6.2 (#11801) Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/pyasn1/pyasn1/releases) - [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst) - [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: pyasn1 dependency-version: 0.6.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 898d431a3..76505fa56 100644 --- a/uv.lock +++ b/uv.lock @@ -3622,11 +3622,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] From e9f7993ba5da7b8f2496f859895fa62ca5011192 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:40:42 -0800 Subject: [PATCH 37/57] Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates (#11799) * Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates Bumps the utilities-minor group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [django-auditlog](https://github.com/jazzband/django-auditlog) | `3.3.0` | `3.4.1` | | [drf-spectacular](https://github.com/tfranzel/drf-spectacular) | `0.28.0` | `0.29.0` | | [faiss-cpu](https://github.com/kyamagu/faiss-wheels) | `1.10.0` | `1.13.2` | | [gotenberg-client](https://github.com/stumpylog/gotenberg-client) | `0.12.0` | `0.13.1` | | [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | `16.12.0` | `16.13.0` | | [torch](https://github.com/pytorch/pytorch) | `2.7.1` | `2.9.1` | | [psycopg-pool](https://github.com/psycopg/psycopg) | `3.2.7` | `3.3.0` | | [pre-commit](https://github.com/pre-commit/pre-commit) | `4.4.0` | `4.5.1` | | [celery-types](https://github.com/sbdchd/celery-types) | `0.23.0` | `0.24.0` | | [mypy](https://github.com/python/mypy) | `1.18.2` | `1.19.1` | Updates `django-auditlog` from 3.3.0 to 3.4.1 - [Release notes](https://github.com/jazzband/django-auditlog/releases) - [Changelog](https://github.com/jazzband/django-auditlog/blob/master/CHANGELOG.md) - [Commits](https://github.com/jazzband/django-auditlog/compare/v3.3.0...v3.4.1) Updates `drf-spectacular` from 0.28.0 to 0.29.0 - [Release notes](https://github.com/tfranzel/drf-spectacular/releases) - [Changelog](https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tfranzel/drf-spectacular/compare/0.28.0...0.29.0) Updates `faiss-cpu` from 1.10.0 to 1.13.2 - [Release notes](https://github.com/kyamagu/faiss-wheels/releases) - [Commits](https://github.com/kyamagu/faiss-wheels/compare/v1.10.0...v1.13.2) Updates `gotenberg-client` from 0.12.0 to 0.13.1 - [Release notes](https://github.com/stumpylog/gotenberg-client/releases) - [Changelog](https://github.com/stumpylog/gotenberg-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/stumpylog/gotenberg-client/compare/0.12.0...0.13.1) Updates `ocrmypdf` from 16.12.0 to 16.13.0 - [Release notes](https://github.com/ocrmypdf/OCRmyPDF/releases) - [Changelog](https://github.com/ocrmypdf/OCRmyPDF/blob/main/docs/release_notes.md) - [Commits](https://github.com/ocrmypdf/OCRmyPDF/compare/v16.12.0...v16.13.0) Updates `torch` from 2.7.1 to 2.9.1 - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.7.1...v2.9.1) Updates `psycopg-pool` from 3.2.7 to 3.3.0 - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.2.7...3.3.0) Updates `pre-commit` from 4.4.0 to 4.5.1 - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.4.0...v4.5.1) Updates `celery-types` from 0.23.0 to 0.24.0 - [Commits](https://github.com/sbdchd/celery-types/commits) Updates `mypy` from 1.18.2 to 1.19.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.18.2...v1.19.1) --- updated-dependencies: - dependency-name: django-auditlog dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: drf-spectacular dependency-version: 0.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: faiss-cpu dependency-version: 1.13.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: gotenberg-client dependency-version: 0.13.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: ocrmypdf dependency-version: 16.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: torch dependency-version: 2.9.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: psycopg-pool dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: pre-commit dependency-version: 4.5.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: celery-types dependency-version: 0.24.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: utilities-minor - dependency-name: mypy dependency-version: 1.19.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: utilities-minor ... Signed-off-by: dependabot[bot] * Apply suggestion from @shamoon --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- pyproject.toml | 12 +-- uv.lock | 264 +++++++++++++++++++++++++++++-------------------- 2 files changed, 162 insertions(+), 114 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6b2d5359d..0eb63f8aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # Only patch versions are guaranteed to not introduce breaking changes. "django~=5.2.5", "django-allauth[mfa,socialaccount]~=65.12.1", - "django-auditlog~=3.3.0", + "django-auditlog~=3.4.1", "django-cachalot~=2.8.0", "django-celery-results~=2.6.0", "django-compression-middleware~=0.5.0", @@ -47,7 +47,7 @@ dependencies = [ "faiss-cpu>=1.10", "filelock~=3.20.0", "flower~=2.0.1", - "gotenberg-client~=0.12.0", + "gotenberg-client~=0.13.1", "httpx-oauth~=0.16", "imap-tools~=1.11.0", "inotifyrecursive~=0.3", @@ -60,7 +60,7 @@ dependencies = [ "llama-index-llms-openai>=0.6.13", "llama-index-vector-stores-faiss>=0.5.2", "nltk~=3.9.1", - "ocrmypdf~=16.12.0", + "ocrmypdf~=16.13.0", "openai>=1.76", "pathvalidate~=3.3.1", "pdf2image~=1.17.0", @@ -77,7 +77,7 @@ dependencies = [ "sentence-transformers>=4.1", "setproctitle~=1.3.4", "tika-client~=0.10.0", - "torch~=2.8.0", + "torch~=2.9.1", "tqdm~=4.67.1", "watchdog~=6.0", "whitenoise~=6.9", @@ -92,7 +92,7 @@ optional-dependencies.postgres = [ "psycopg[c,pool]==3.2.12", # Direct dependency for proper resolution of the pre-built wheels "psycopg-c==3.2.12", - "psycopg-pool==3.2.7", + "psycopg-pool==3.3", ] optional-dependencies.webserver = [ "granian[uvloop]~=2.5.1", @@ -127,7 +127,7 @@ testing = [ ] lint = [ - "pre-commit~=4.4.0", + "pre-commit~=4.5.1", "pre-commit-uv~=4.2.0", "ruff~=0.14.0", ] diff --git a/uv.lock b/uv.lock index 76505fa56..2f94a7276 100644 --- a/uv.lock +++ b/uv.lock @@ -415,14 +415,14 @@ redis = [ [[package]] name = "celery-types" -version = "0.23.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/d1/0823e71c281e4ad0044e278cf1577d1a68e05f2809424bf94e1614925c5d/celery_types-0.23.0.tar.gz", hash = "sha256:402ed0555aea3cd5e1e6248f4632e4f18eec8edb2435173f9e6dc08449fa101e", size = 31479, upload-time = "2025-03-03T23:56:51.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/25/2276a1f00f8ab9fc88128c939333933a24db7df1d75aa57ecc27b7dd3a22/celery_types-0.24.0.tar.gz", hash = "sha256:c93fbcd0b04a9e9c2f55d5540aca4aa1ea4cc06a870c0c8dee5062fdd59663fe", size = 33148, upload-time = "2025-12-23T17:16:30.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/8b/92bb54dd74d145221c3854aa245c84f4dc04cc9366147496182cec8e88e3/celery_types-0.23.0-py3-none-any.whl", hash = "sha256:0cc495b8d7729891b7e070d0ec8d4906d2373209656a6e8b8276fe1ed306af9a", size = 50189, upload-time = "2025-03-03T23:56:50.458Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/3252cba5f5c9a65a3f52a69734d8e51e023db8981022b503e8183cf0225e/celery_types-0.24.0-py3-none-any.whl", hash = "sha256:a21e04681e68719a208335e556a79909da4be9c5e0d6d2fd0dd4c5615954b3fd", size = 60473, upload-time = "2025-12-23T17:16:29.89Z" }, ] [[package]] @@ -920,15 +920,15 @@ socialaccount = [ [[package]] name = "django-auditlog" -version = "3.3.0" +version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/d8/ddd1c653ffb7ed1984596420982e32a0b163a0be316721a801a54dcbf016/django_auditlog-3.3.0.tar.gz", hash = "sha256:01331a0e7bb1a8ff7573311b486c88f3d0c431c388f5a1e4a9b6b26911dd79b8", size = 85941, upload-time = "2025-10-02T17:16:27.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/e5/2beb2b256775c4fc041ed60cb44f5d77acb6cde307f01567dcf2756721a7/django_auditlog-3.4.1.tar.gz", hash = "sha256:ad07b9db452d5fa8303822cccd78cd3fcb2c2863aeb6abe039ec45739b4d7e33", size = 91611, upload-time = "2025-12-18T08:56:35.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/bc/6e1b503d1755ab09cff6480cb088def073f1303165ab59b1a09247a2e756/django_auditlog-3.3.0-py3-none-any.whl", hash = "sha256:ab0f0f556a7107ac01c8fa87137bdfbb2b6f0debf70f7753169d9a40673d2636", size = 39676, upload-time = "2025-10-02T17:15:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/3239143dddb34a3f64582c43f56587a80c9ac136cbd34fc215077f83beb9/django_auditlog-3.4.1-py3-none-any.whl", hash = "sha256:29958ecacfee00144127214f3ccef3f0c203c3659bcb6dd404a0f3d5551a10a5", size = 49541, upload-time = "2025-12-18T08:56:23.483Z" }, ] [[package]] @@ -1061,7 +1061,7 @@ wheels = [ [[package]] name = "django-stubs" -version = "5.2.5" +version = "5.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -1070,9 +1070,9 @@ dependencies = [ { name = "types-pyyaml", 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/e1/8e/286150f593481c33f54d14efb58d72178f159d57d31043529d38bbc98e2f/django_stubs-5.2.5.tar.gz", hash = "sha256:fc78384e28d8c5292d60983075a5934f644f7c304c25ae2793fc57aa66d5018b", size = 247794, upload-time = "2025-09-12T19:29:49.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/75/97626224fd8f1787bb6f7f06944efcfddd5da7764bf741cf7f59d102f4a0/django_stubs-5.2.8.tar.gz", hash = "sha256:9bba597c9a8ed8c025cae4696803d5c8be1cf55bfc7648a084cbf864187e2f8b", size = 257709, upload-time = "2025-12-01T08:13:09.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/02/cdbf7652ef2c9a7a1fed7c279484b7f3806f15b1bb34aec9fef8e8cfacbf/django_stubs-5.2.5-py3-none-any.whl", hash = "sha256:223c1a3324cd4873b7629dec6e9adbe224a94508284c1926b25fddff7a92252b", size = 490196, upload-time = "2025-09-12T19:29:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3f/7c9543ad5ade5ce1d33d187a3abd82164570314ebee72c6206ab5c044ebf/django_stubs-5.2.8-py3-none-any.whl", hash = "sha256:a3c63119fd7062ac63d58869698d07c9e5ec0561295c4e700317c54e8d26716c", size = 508136, upload-time = "2025-12-01T08:13:07.963Z" }, ] [package.optional-dependencies] @@ -1082,15 +1082,15 @@ compatible-mypy = [ [[package]] name = "django-stubs-ext" -version = "5.2.5" +version = "5.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", 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/16/94/c9b8f4c47084a0fa666da9066c36771098101932688adf2c17a40fab79c2/django_stubs_ext-5.2.5.tar.gz", hash = "sha256:ecc628df29d36cede638567c4e33ff485dd7a99f1552ad0cece8c60e9c3a8872", size = 6489, upload-time = "2025-09-12T19:29:06.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/a2/d67f4a5200ff7626b104eddceaf529761cba4ed318a73ffdb0677551be73/django_stubs_ext-5.2.8.tar.gz", hash = "sha256:b39938c46d7a547cd84e4a6378dbe51a3dd64d70300459087229e5fee27e5c6b", size = 6487, upload-time = "2025-12-01T08:12:37.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/fe/a85a105fddffadb4a8d50e500caeee87d836b679d51a19d52dfa0cc6c660/django_stubs_ext-5.2.5-py3-none-any.whl", hash = "sha256:9b4b8ac9d32f7e6c304fd05477f8688fae6ed57f6a0f9f4d074f9e55b5a3da14", size = 9310, upload-time = "2025-09-12T19:29:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/da/2d/cb0151b780c3730cf0f2c0fcb1b065a5e88f877cf7a9217483c375353af1/django_stubs_ext-5.2.8-py3-none-any.whl", hash = "sha256:1dd5470c9675591362c78a157a3cf8aec45d0e7a7f0cf32f227a1363e54e0652", size = 9949, upload-time = "2025-12-01T08:12:36.397Z" }, ] [[package]] @@ -1130,18 +1130,17 @@ wheels = [ [[package]] name = "djangorestframework-stubs" -version = "3.16.4" +version = "3.16.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-requests", 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/77/33/79c0dd2a02ead2702a1b3d25579b56ffdc1bc6d1271c31a0979ce9ad10fa/djangorestframework_stubs-3.16.4.tar.gz", hash = "sha256:f43136bfbef568dd0e10848427c01bd9ef759dd328195949f6f7f9a2292a34f6", size = 31960, upload-time = "2025-09-29T20:11:20.57Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/50/889b1121dc0831aa9f6ece8409d41a5f4667da2a963172516841f343fd35/djangorestframework_stubs-3.16.7.tar.gz", hash = "sha256:e53bc346e9950ebdd1bb2bbc19d7e5c8b7acc894e381df55da69248f47ab78ff", size = 32296, upload-time = "2026-01-13T11:42:48.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/45/63fac5c34313986acc005529fdba638822bae973f2b61a0542a6c8494847/djangorestframework_stubs-3.16.4-py3-none-any.whl", hash = "sha256:3b27353fa797876f55da87eceafe4c2f265a93924fa7763d257e509d865df1b2", size = 56503, upload-time = "2025-09-29T20:11:19.317Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/7c969728d66388e22fdaba94e1a9c56490954e2f12f598416e380a53b26d/djangorestframework_stubs-3.16.7-py3-none-any.whl", hash = "sha256:70f80050144875f80ce8ac823ff8628f6e3eb7336495394bb9803251721d9358", size = 56522, upload-time = "2026-01-13T11:42:46.118Z" }, ] [package.optional-dependencies] @@ -1152,7 +1151,7 @@ compatible-mypy = [ [[package]] name = "drf-spectacular" -version = "0.28.0" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -1162,9 +1161,9 @@ dependencies = [ { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "uritemplate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/b9/741056455aed00fa51a1df41fad5ad27c8e0d433b6bf490d4e60e2808bc6/drf_spectacular-0.28.0.tar.gz", hash = "sha256:2c778a47a40ab2f5078a7c42e82baba07397bb35b074ae4680721b2805943061", size = 237849, upload-time = "2024-11-30T08:49:02.355Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/66/c2929871393b1515c3767a670ff7d980a6882964a31a4ca2680b30d7212a/drf_spectacular-0.28.0-py3-none-any.whl", hash = "sha256:856e7edf1056e49a4245e87a61e8da4baff46c83dbc25be1da2df77f354c7cb4", size = 103928, upload-time = "2024-11-30T08:48:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" }, ] [[package]] @@ -1222,7 +1221,7 @@ wheels = [ [[package]] name = "faiss-cpu" -version = "1.10.0" +version = "1.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", 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')" }, @@ -1230,22 +1229,12 @@ dependencies = [ { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/56/87eb506d8634f08fc7c63d1ca5631aeec7d6b9afbfabedf2cb7a2a804b13/faiss_cpu-1.10.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6693474be296a7142ade1051ea18e7d85cedbfdee4b7eac9c52f83fed0467855", size = 7693034, upload-time = "2025-01-31T07:44:31.908Z" }, - { url = "https://files.pythonhosted.org/packages/51/46/f4d9de34ed1b06300b1a75b824d4857963216f5826de33f291af78088e39/faiss_cpu-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70ebe60a560414dc8dd6cfe8fed105c8f002c0d11f765f5adfe8d63d42c0467f", size = 3234656, upload-time = "2025-01-31T07:44:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/74/3a/e146861019d9290e0198b3470b8d13a658c3b5f228abefc3658ce0afd63d/faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:74c5712d4890f15c661ab7b1b75867812e9596e1469759956fad900999bedbb5", size = 3663789, upload-time = "2025-01-31T07:44:36.698Z" }, - { url = "https://files.pythonhosted.org/packages/aa/40/624f0002bb777e37aac1aadfadec1eb4391be6ad05b7fcfbf66049b99a48/faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:473d158fbd638d6ad5fb64469ba79a9f09d3494b5f4e8dfb4f40ce2fc335dca4", size = 30673545, upload-time = "2025-01-31T07:44:40.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/93/81800f41cb2c719c199d3eb534fcc154853123261d841e37482e8e468619/faiss_cpu-1.10.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8ff6924b0f00df278afe70940ae86302066466580724c2f3238860039e9946f1", size = 7693037, upload-time = "2025-01-31T07:44:48.97Z" }, - { url = "https://files.pythonhosted.org/packages/8d/83/fc9028f6d6aec2c2f219f53a5d4a2b279434715643242e59a2e9755b1ce0/faiss_cpu-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb80b530a9ded44a7d4031a7355a237aaa0ff1f150c1176df050e0254ea5f6f6", size = 3234657, upload-time = "2025-01-31T07:44:51.399Z" }, - { url = "https://files.pythonhosted.org/packages/af/45/588a02e60daa73f6052611334fbbdffcedf37122320f1c91cb90f3e69b96/faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a9fef4039ed877d40e41d5563417b154c7f8cd57621487dad13c4eb4f32515f", size = 3663710, upload-time = "2025-01-31T07:44:53.198Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cf/9caa08ca4e21ab935f82be0713e5d60566140414c3fff7932d9427c8fd72/faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49b6647aa9e159a2c4603cbff2e1b313becd98ad6e851737ab325c74fe8e0278", size = 30673629, upload-time = "2025-01-31T07:44:56.652Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cc/f6aa1288dbb40b2a4f101d16900885e056541f37d8d08ec70462e92cf277/faiss_cpu-1.10.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:2aca486fe2d680ea64a18d356206c91ff85db99fd34c19a757298c67c23262b1", size = 7720242, upload-time = "2025-01-31T07:45:03.871Z" }, - { url = "https://files.pythonhosted.org/packages/be/56/40901306324a17fbc1eee8a6e86ba67bd99a67e768ce9908f271e648e9e0/faiss_cpu-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1108a4059c66c37c403183e566ca1ed0974a6af7557c92d49207639aab661bc", size = 3239223, upload-time = "2025-01-31T07:45:06.585Z" }, - { url = "https://files.pythonhosted.org/packages/2e/34/5b1463c450c9a6de3109caf8f38fbf0c329ef940ed1973fcf8c8ec7fa27e/faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:449f3eb778d6d937e01a16a3170de4bb8aabfe87c7cb479b458fb790276310c5", size = 3671461, upload-time = "2025-01-31T07:45:09.099Z" }, - { url = "https://files.pythonhosted.org/packages/78/d9/0b78c474289f23b31283d8fb64c8e6a522a7fa47b131a3c6c141c8e6639d/faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9899c340f92bd94071d6faf4bef0ccb5362843daea42144d4ba857a2a1f67511", size = 30663859, upload-time = "2025-01-31T07:45:13.027Z" }, - { url = "https://files.pythonhosted.org/packages/93/25/23239a83142faa319c4f8c025e25fec6cccc7418995eba3515218a57a45b/faiss_cpu-1.10.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:cb8473d69c3964c1bf3f8eb3e04287bb3275f536e6d9635ef32242b5f506b45d", size = 7720240, upload-time = "2025-01-31T07:45:19.943Z" }, - { url = "https://files.pythonhosted.org/packages/18/f1/0e979277831af337739dbacf386d8a359a05eef9642df23d36e6c7d1b1a9/faiss_cpu-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82ca5098de694e7b8495c1a8770e2c08df6e834922546dad0ae1284ff519ced6", size = 3239224, upload-time = "2025-01-31T07:45:21.744Z" }, - { url = "https://files.pythonhosted.org/packages/bd/fa/c2ad85b017a5754f6cdb09c179f8c4f4198d2a264046a8daa7a4d080521f/faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:035e4d797e2db7fc0d0c90531d4a655d089ad5d1382b7a49358c1f2307b3a309", size = 3671236, upload-time = "2025-01-31T07:45:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/759962f2c34800058f6a76457df3b0ab93b24f383650ea1ef0231acd322c/faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574", size = 30663948, upload-time = "2025-01-31T07:45:27.271Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/671f66f6b31ec48e5825d36435f0cb91189fa8bb6b50724029dbff4ca83c/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1", size = 3452776, upload-time = "2025-12-24T10:27:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4a/97150aa1582fb9c2bca95bd8fc37f27d3b470acec6f0a6833844b21e4b40/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317", size = 7896434, upload-time = "2025-12-24T10:27:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d0/0940575f059591ca31b63a881058adb16a387020af1709dcb7669460115c/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1", size = 11485825, upload-time = "2025-12-24T10:27:05.681Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e1/a5acac02aa593809f0123539afe7b4aff61d1db149e7093239888c9053e1/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7", size = 23845772, upload-time = "2025-12-24T10:27:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7b/49dcaf354834ec457e85ca769d50bc9b5f3003fab7c94a9dcf08cf742793/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f", size = 13477567, upload-time = "2025-12-24T10:27:10.822Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6b/12bb4037921c38bb2c0b4cfc213ca7e04bbbebbfea89b0b5746248ce446e/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f", size = 25102239, upload-time = "2025-12-24T10:27:13.476Z" }, ] [[package]] @@ -1413,15 +1402,15 @@ wheels = [ [[package]] name = "gotenberg-client" -version = "0.12.0" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"], 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/61/6d/07ea213c146bbe91dffebff2d8f4dc61e7076d3dd34d4fd1467f9163e752/gotenberg_client-0.12.0.tar.gz", hash = "sha256:1ab50878024469fc003c414ee9810ceeb00d4d7d7c36bd2fb75318fbff139e9b", size = 1210884, upload-time = "2025-10-15T15:32:37.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/6c/aaadd6657ca42fbd148b1c00604b98c1ead5a22552f4e5365ce5f0632430/gotenberg_client-0.13.1.tar.gz", hash = "sha256:cdd6bbb535cd739b87446cd1b4f6347ed7f9af6a0d4b19baf7c064b75528ee54", size = 1211143, upload-time = "2025-12-04T20:45:24.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/39/fcb24ff053b1be7e5124f56c3d358706a23a328f685c6db33bc9dbc5472d/gotenberg_client-0.12.0-py3-none-any.whl", hash = "sha256:a540b35ac518e902c2860a88fbe448c15fe5a56fe8ec8604e6a2c8c2228fd0cb", size = 51051, upload-time = "2025-10-15T15:32:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/79/f6/7a6e6785295332d2538f729ae19516cef712273a5ab8b90d015f08e37a45/gotenberg_client-0.13.1-py3-none-any.whl", hash = "sha256:613f7083a5e8a81699dd8d715c97e5806a424ac48920aad25d7c11b600cdfaf3", size = 51058, upload-time = "2025-12-04T20:45:22.603Z" }, ] [[package]] @@ -2010,6 +1999,62 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, +] + [[package]] name = "llama-index-core" version = "0.14.12" @@ -2587,42 +2632,43 @@ wheels = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, { name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pathspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { 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]] @@ -2818,7 +2864,7 @@ wheels = [ [[package]] name = "ocrmypdf" -version = "16.12.0" +version = "16.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2831,9 +2877,9 @@ dependencies = [ { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/ed/dacc0f189e4fcefc52d709e9961929e3f622a85efa5ae47c9d9663d75cab/ocrmypdf-16.12.0.tar.gz", hash = "sha256:a0f6509e7780b286391f8847fae1811d2b157b14283ad74a2431d6755c5c0ed0", size = 7037326, upload-time = "2025-11-11T22:30:14.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/52/be1aaece0703a736757d8957c0d4f19c37561054169b501eb0e7132f15e5/ocrmypdf-16.13.0.tar.gz", hash = "sha256:29d37e915234ce717374863a9cc5dd32d29e063dfe60c51380dda71254c88248", size = 7042247, upload-time = "2025-12-24T07:58:35.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/34/d9d04420e6f7a71e2135b41599dae273e4ef36e2ce79b065b65fb2471636/ocrmypdf-16.12.0-py3-none-any.whl", hash = "sha256:0ea5c42027db9cf3bd12b0d0b4190689027ef813fdad3377106ea66bba0012c3", size = 163415, upload-time = "2025-11-11T22:30:11.56Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/e2e7ad98de0d3ee05b44dbc3f78ccb158a620f3add82d00c85490120e7f2/ocrmypdf-16.13.0-py3-none-any.whl", hash = "sha256:fad8a6f7cc52cdc6225095c401a1766c778c47efe9f1e854ae4dc64a550a3d37", size = 165377, upload-time = "2025-12-24T07:58:33.925Z" }, ] [[package]] @@ -2950,8 +2996,8 @@ dependencies = [ { name = "sentence-transformers", 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 = "torch", version = "2.8.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.8.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { 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 = "watchdog", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3050,7 +3096,7 @@ requires-dist = [ { name = "dateparser", specifier = "~=1.2" }, { name = "django", specifier = "~=5.2.5" }, { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.12.1" }, - { name = "django-auditlog", specifier = "~=3.3.0" }, + { name = "django-auditlog", specifier = "~=3.4.1" }, { name = "django-cachalot", specifier = "~=2.8.0" }, { name = "django-celery-results", specifier = "~=2.6.0" }, { name = "django-compression-middleware", specifier = "~=0.5.0" }, @@ -3069,7 +3115,7 @@ requires-dist = [ { name = "faiss-cpu", specifier = ">=1.10" }, { name = "filelock", specifier = "~=3.20.0" }, { name = "flower", specifier = "~=2.0.1" }, - { name = "gotenberg-client", specifier = "~=0.12.0" }, + { name = "gotenberg-client", specifier = "~=0.13.1" }, { name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.5.1" }, { name = "httpx-oauth", specifier = "~=0.16" }, { name = "imap-tools", specifier = "~=1.11.0" }, @@ -3084,7 +3130,7 @@ requires-dist = [ { name = "llama-index-vector-stores-faiss", specifier = ">=0.5.2" }, { name = "mysqlclient", marker = "extra == 'mariadb'", specifier = "~=2.2.7" }, { name = "nltk", specifier = "~=3.9.1" }, - { name = "ocrmypdf", specifier = "~=16.12.0" }, + { name = "ocrmypdf", specifier = "~=16.13.0" }, { name = "openai", specifier = ">=1.76" }, { name = "pathvalidate", specifier = "~=3.3.1" }, { name = "pdf2image", specifier = "~=1.17.0" }, @@ -3092,7 +3138,7 @@ requires-dist = [ { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" }, { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" }, { name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.12" }, - { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.2.7" }, + { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.3.0" }, { name = "python-dateutil", specifier = "~=2.9.0" }, { name = "python-dotenv", specifier = "~=1.2.1" }, { name = "python-gnupg", specifier = "~=0.5.4" }, @@ -3106,7 +3152,7 @@ requires-dist = [ { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, { name = "tika-client", specifier = "~=0.10.0" }, - { name = "torch", specifier = "~=2.8.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 = "watchdog", specifier = "~=6.0" }, { name = "whitenoise", specifier = "~=6.9" }, @@ -3124,7 +3170,7 @@ dev = [ { name = "imagehash" }, { name = "mkdocs-glightbox", specifier = "~=0.5.1" }, { name = "mkdocs-material", specifier = "~=9.7.0" }, - { name = "pre-commit", specifier = "~=4.4.0" }, + { name = "pre-commit", specifier = "~=4.5.1" }, { name = "pre-commit-uv", specifier = "~=4.2.0" }, { name = "pytest", specifier = "~=8.4.1" }, { name = "pytest-cov", specifier = "~=7.0.0" }, @@ -3142,7 +3188,7 @@ docs = [ { name = "mkdocs-material", specifier = "~=9.7.0" }, ] lint = [ - { name = "pre-commit", specifier = "~=4.4.0" }, + { name = "pre-commit", specifier = "~=4.5.1" }, { name = "pre-commit-uv", specifier = "~=4.2.0" }, { name = "ruff", specifier = "~=0.14.0" }, ] @@ -3423,7 +3469,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3432,9 +3478,9 @@ dependencies = [ { 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/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } +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 = [ - { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, + { 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" }, ] [[package]] @@ -3610,14 +3656,14 @@ wheels = [ [[package]] name = "psycopg-pool" -version = "3.2.7" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/8f/3ec52b17087c2ed5fa32b64fd4814dde964c9aa4bd49d0d30fc24725ca6d/psycopg_pool-3.2.7.tar.gz", hash = "sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b", size = 29765, upload-time = "2025-10-26T00:46:10.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/59/74e752f605c6f0e351d4cf1c54fb9a1616dc800db4572b95bbfbb1a6225f/psycopg_pool-3.2.7-py3-none-any.whl", hash = "sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f", size = 38232, upload-time = "2025-10-26T00:46:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" }, ] [[package]] @@ -4750,8 +4796,8 @@ dependencies = [ { 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.16.2", 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.8.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.8.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { 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 = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -5065,7 +5111,7 @@ wheels = [ [[package]] name = "torch" -version = "2.8.0" +version = "2.9.1" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -5082,16 +5128,18 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a467b49fe893a6a6cce89e3aee556edfdc64a722d7195fdfdd75cec9dea13779" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl" }, ] [[package]] name = "torch" -version = "2.8.0+cpu" +version = "2.9.1+cpu" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -5110,20 +5158,20 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-linux_s390x.whl", hash = "sha256:5d255d259fbc65439b671580e40fdb8faea4644761b64fed90d6904ffe71bbc1" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b2149858b8340aeeb1f3056e0bff5b82b96e43b596fe49a9dba3184522261213" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:16d75fa4e96ea28a785dfd66083ca55eb1058b6d6c5413f01656ca965ee2077e" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl" }, ] [[package]] From f8ab81cef7c5d038dd98f6db9a94c7fbbb29ba1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:14:01 -0800 Subject: [PATCH 38/57] Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates (#11793) Bumps the utilities-patch group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [channels](https://github.com/django/channels) | `4.3.1` | `4.3.2` | | [django-soft-delete](https://github.com/san4ezy/django_softdelete) | `1.0.21` | `1.0.22` | | [django-treenode](https://github.com/fabiocaccamo/django-treenode) | `0.23.2` | `0.23.3` | | [imap-tools](https://github.com/ikvk/imap_tools) | `1.11.0` | `1.11.1` | | [python-gnupg](https://github.com/vsajip/python-gnupg) | `0.5.5` | `0.5.6` | | [mkdocs-material](https://github.com/squidfunk/mkdocs-material) | `9.7.0` | `9.7.1` | | [ruff](https://github.com/astral-sh/ruff) | `0.14.5` | `0.14.13` | Updates `channels` from 4.3.1 to 4.3.2 - [Changelog](https://github.com/django/channels/blob/main/CHANGELOG.txt) - [Commits](https://github.com/django/channels/compare/4.3.1...4.3.2) Updates `django-soft-delete` from 1.0.21 to 1.0.22 - [Changelog](https://github.com/san4ezy/django_softdelete/blob/master/CHANGELOG.md) - [Commits](https://github.com/san4ezy/django_softdelete/commits) Updates `django-treenode` from 0.23.2 to 0.23.3 - [Release notes](https://github.com/fabiocaccamo/django-treenode/releases) - [Changelog](https://github.com/fabiocaccamo/django-treenode/blob/main/CHANGELOG.md) - [Commits](https://github.com/fabiocaccamo/django-treenode/compare/0.23.2...0.23.3) Updates `imap-tools` from 1.11.0 to 1.11.1 - [Release notes](https://github.com/ikvk/imap_tools/releases) - [Changelog](https://github.com/ikvk/imap_tools/blob/master/docs/release_notes.rst) - [Commits](https://github.com/ikvk/imap_tools/compare/v1.11.0...v1.11.1) Updates `python-gnupg` from 0.5.5 to 0.5.6 - [Release notes](https://github.com/vsajip/python-gnupg/releases) - [Changelog](https://github.com/vsajip/python-gnupg/blob/master/release) - [Commits](https://github.com/vsajip/python-gnupg/compare/0.5.5...0.5.6) Updates `mkdocs-material` from 9.7.0 to 9.7.1 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.7.0...9.7.1) Updates `ruff` from 0.14.5 to 0.14.13 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.5...0.14.13) --- updated-dependencies: - dependency-name: channels dependency-version: 4.3.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: django-soft-delete dependency-version: 1.0.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: django-treenode dependency-version: 0.23.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: imap-tools dependency-version: 1.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: python-gnupg dependency-version: 0.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: mkdocs-material dependency-version: 9.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: utilities-patch - dependency-name: ruff dependency-version: 0.14.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: utilities-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 72 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/uv.lock b/uv.lock index 2f94a7276..8140a289d 100644 --- a/uv.lock +++ b/uv.lock @@ -510,15 +510,15 @@ wheels = [ [[package]] name = "channels" -version = "4.3.1" +version = "4.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", 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/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896, upload-time = "2025-08-01T13:25:19.952Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/92/b18d4bb54d14986a8b35215a1c9e6a7f9f4d57ca63ac9aee8290ebb4957d/channels-4.3.2.tar.gz", hash = "sha256:f2bb6bfb73ad7fb4705041d07613c7b4e69528f01ef8cb9fb6c21d9295f15667", size = 27023, upload-time = "2025-11-20T15:13:05.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286, upload-time = "2025-08-01T13:25:18.845Z" }, + { url = "https://files.pythonhosted.org/packages/16/34/c32915288b7ef482377b6adc401192f98c6a99b3a145423d3b8aed807898/channels-4.3.2-py3-none-any.whl", hash = "sha256:fef47e9055a603900cf16cef85f050d522d9ac4b3daccf24835bd9580705c176", size = 31313, upload-time = "2025-11-20T15:13:02.357Z" }, ] [[package]] @@ -1049,14 +1049,14 @@ wheels = [ [[package]] name = "django-soft-delete" -version = "1.0.21" +version = "1.0.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/bf/13996c18bffee3bbcf294830c1737bfb5564164b8319c51e6714b6bdf783/django_soft_delete-1.0.21.tar.gz", hash = "sha256:542bd4650d2769105a4363ea7bb7fbdb3c28429dbaa66417160f8f4b5dc689d5", size = 21153, upload-time = "2025-09-17T08:46:30.476Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/d1/c990b731676f93bd4594dee4b5133df52f5d0eee1eb8a969b4030014ac54/django_soft_delete-1.0.22.tar.gz", hash = "sha256:32d0bb95f180c28a40163e78a558acc18901fd56011f91f8ee735c171a6d4244", size = 21982, upload-time = "2025-10-25T13:11:46.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/e6/8f4fed14499c63e35ca33cf9f424ad2e14e963ec5545594d7c7dc2f710f4/django_soft_delete-1.0.21-py3-none-any.whl", hash = "sha256:dd91e671d9d431ff96f4db727ce03e7fbb4008ae4541b1d162d5d06cc9becd2a", size = 18681, upload-time = "2025-09-17T08:46:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c2/fca2bf69b7ca7e18aed9ac059e89f1043663e207a514e8fb652450e49631/django_soft_delete-1.0.22-py3-none-any.whl", hash = "sha256:81973c541d21452d249151085d617ebbfb5ec463899f47cd6b1306677481e94c", size = 19221, upload-time = "2025-10-25T13:11:44.755Z" }, ] [[package]] @@ -1095,11 +1095,11 @@ wheels = [ [[package]] name = "django-treenode" -version = "0.23.2" +version = "0.23.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/f3/274b84607fd64c0844e98659985f964190a46c2460f2523a446c4a946216/django_treenode-0.23.2.tar.gz", hash = "sha256:3c5a6ff5e0c83e34da88749f602b3013dd1ab0527f51952c616e3c21bf265d52", size = 26700, upload-time = "2025-09-04T21:16:53.497Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/58/86edbbd1075bb8bc0962c6feb13bc06822405a10fea8352ad73ab2babdd9/django_treenode-0.23.3.tar.gz", hash = "sha256:714c825d5b925a3d2848d0709f29973941ea41a606b8e2b64cbec46010a8cce3", size = 27812, upload-time = "2025-12-01T23:01:24.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/61/e17d3dee5c6bb24b8faf0c101e17f9a8cafeba6384166176e066c80e8cbb/django_treenode-0.23.2-py3-none-any.whl", hash = "sha256:9363cb50f753654a9acfad6ec4df2a664a5f89dfdf8b55ffd964f27461bef85e", size = 21879, upload-time = "2025-09-04T21:16:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/bc/52/696db237167483324ef38d8d090fb0fcc33dbb70ebe66c75868005fb7c75/django_treenode-0.23.3-py3-none-any.whl", hash = "sha256:8072e1ac688c1ed3ab95a98a797c5e965380de5228a389d60a4ef8b9a6449387", size = 22014, upload-time = "2025-12-01T23:01:23.266Z" }, ] [[package]] @@ -1794,11 +1794,11 @@ wheels = [ [[package]] name = "imap-tools" -version = "1.11.0" +version = "1.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/76/2d74bf4702d7d9fb2dd056e058929961a05389be47b990f3275e8596012e/imap_tools-1.11.0.tar.gz", hash = "sha256:77b055d301f24e668ff54ad50cc32a36d1579c6aa9b26e5fb6501fb622feb6ea", size = 46191, upload-time = "2025-06-30T05:47:21.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/eb/de91770dc8f34691c33355f7b6277500db32f68ce039c08e2a40ad5a0536/imap_tools-1.11.1.tar.gz", hash = "sha256:e3aa02ff3415a2b50a47707eacbf7386bb79aabc34e370c6bb95f9ad20504389", size = 46562, upload-time = "2026-01-15T08:25:47.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/8f/75524e1a040183cc437332e2de6e8f975c345fff8b5aaa35e0d20dec24f9/imap_tools-1.11.0-py3-none-any.whl", hash = "sha256:7c797b421fdf1b898b4ee0042fe02d10037d56f9acacca64086c2af36d830a24", size = 34855, upload-time = "2025-06-30T05:47:15.657Z" }, + { url = "https://files.pythonhosted.org/packages/64/bb/94eca066102559a20953475779a4d4d6b39712985014bceba726e1f65aab/imap_tools-1.11.1-py3-none-any.whl", hash = "sha256:0749d0a1f2b9041be1533ea98cc3e9f7977ba86ad3669c1fcf89c27969dcfb0a", size = 35291, upload-time = "2026-01-15T08:25:42.773Z" }, ] [[package]] @@ -2465,7 +2465,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2480,9 +2480,9 @@ dependencies = [ { name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -3138,7 +3138,7 @@ requires-dist = [ { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" }, { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" }, { name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.12" }, - { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.3.0" }, + { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.3" }, { name = "python-dateutil", specifier = "~=2.9.0" }, { name = "python-dotenv", specifier = "~=1.2.1" }, { name = "python-gnupg", specifier = "~=0.5.4" }, @@ -4002,11 +4002,11 @@ wheels = [ [[package]] name = "python-gnupg" -version = "0.5.5" +version = "0.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/d0/72a14a79f26c6119b281f6ccc475a787432ef155560278e60df97ce68a86/python-gnupg-0.5.5.tar.gz", hash = "sha256:3fdcaf76f60a1b948ff8e37dc398d03cf9ce7427065d583082b92da7a4ff5a63", size = 66467, upload-time = "2025-08-04T19:26:55.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/2c/6cd2c7cff4bdbb434be5429ef6b8e96ee6b50155551361f30a1bb2ea3c1d/python_gnupg-0.5.6.tar.gz", hash = "sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac", size = 66825, upload-time = "2025-12-31T17:19:33.19Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/19/c147f78cc18c8788f54d4a16a22f6c05deba85ead5672d3ddf6dcba5a5fe/python_gnupg-0.5.5-py2.py3-none-any.whl", hash = "sha256:51fa7b8831ff0914bc73d74c59b99c613de7247b91294323c39733bb85ac3fc1", size = 21916, upload-time = "2025-08-04T19:26:54.307Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ab/0ea9de971caf3cd2e268d2b05dfe9883b21cfe686a59249bd2dccb4bae33/python_gnupg-0.5.6-py2.py3-none-any.whl", hash = "sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a", size = 22082, upload-time = "2025-12-31T17:16:22.743Z" }, ] [[package]] @@ -4543,25 +4543,25 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.5" +version = "0.14.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, - { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, - { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, - { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, - { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, - { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, ] [[package]] From b2541f3e8c3b59a07365428ef0b3d5b4c86df771 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:21:20 -0800 Subject: [PATCH 39/57] Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent (#11811) --- src-ui/src/app/components/common/input/tags/tags.component.html | 2 +- src-ui/src/app/components/common/input/tags/tags.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index f04863f40..960245984 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -28,7 +28,7 @@ -
    +
    @if (item.id && tags) { @if (getTag(item.id)?.parent) { diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 52292d5cb..c86792728 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -23,7 +23,7 @@ // Dropdown hierarchy reveal for ng-select options ::ng-deep .ng-dropdown-panel .ng-option { - overflow-x: scroll; + overflow-x: scroll !important; .tag-option-row { font-size: 1rem; From 37477d391ef2a28686468cc526a996d3e9e8b517 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:21:20 -0800 Subject: [PATCH 40/57] Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent (#11811) --- src-ui/src/app/components/common/input/tags/tags.component.html | 2 +- src-ui/src/app/components/common/input/tags/tags.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index f04863f40..960245984 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -28,7 +28,7 @@ -
    +
    @if (item.id && tags) { @if (getTag(item.id)?.parent) { diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 52292d5cb..c86792728 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -23,7 +23,7 @@ // Dropdown hierarchy reveal for ng-select options ::ng-deep .ng-dropdown-panel .ng-option { - overflow-x: scroll; + overflow-x: scroll !important; .tag-option-row { font-size: 1rem; From fa6a0a81f4af3d3951c3ddae800fab9578a72a41 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:20:54 -0800 Subject: [PATCH 41/57] Chore: reverse migration order (#11813) --- ...076_workflowaction_order.py => 1075_workflowaction_order.py} | 2 +- ...stask_task_name.py => 1076_alter_paperlesstask_task_name.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/documents/migrations/{1076_workflowaction_order.py => 1075_workflowaction_order.py} (89%) rename src/documents/migrations/{1075_alter_paperlesstask_task_name.py => 1076_alter_paperlesstask_task_name.py} (90%) diff --git a/src/documents/migrations/1076_workflowaction_order.py b/src/documents/migrations/1075_workflowaction_order.py similarity index 89% rename from src/documents/migrations/1076_workflowaction_order.py rename to src/documents/migrations/1075_workflowaction_order.py index 5c9f7ff52..f7101bf7e 100644 --- a/src/documents/migrations/1076_workflowaction_order.py +++ b/src/documents/migrations/1075_workflowaction_order.py @@ -12,7 +12,7 @@ def populate_action_order(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("documents", "1075_alter_paperlesstask_task_name"), + ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), ] operations = [ diff --git a/src/documents/migrations/1075_alter_paperlesstask_task_name.py b/src/documents/migrations/1076_alter_paperlesstask_task_name.py similarity index 90% rename from src/documents/migrations/1075_alter_paperlesstask_task_name.py rename to src/documents/migrations/1076_alter_paperlesstask_task_name.py index 2df0eaeb9..bb5406255 100644 --- a/src/documents/migrations/1075_alter_paperlesstask_task_name.py +++ b/src/documents/migrations/1076_alter_paperlesstask_task_name.py @@ -6,7 +6,7 @@ from django.db import models class Migration(migrations.Migration): dependencies = [ - ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), + ("documents", "1075_workflowaction_order"), ] operations = [ From ecfeff505453d8ca54d1c5f40c13102ba984a38c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:20:54 -0800 Subject: [PATCH 42/57] Chore: reverse migration order (#11813) --- ...076_workflowaction_order.py => 1075_workflowaction_order.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/documents/migrations/{1076_workflowaction_order.py => 1075_workflowaction_order.py} (89%) diff --git a/src/documents/migrations/1076_workflowaction_order.py b/src/documents/migrations/1075_workflowaction_order.py similarity index 89% rename from src/documents/migrations/1076_workflowaction_order.py rename to src/documents/migrations/1075_workflowaction_order.py index 5c9f7ff52..f7101bf7e 100644 --- a/src/documents/migrations/1076_workflowaction_order.py +++ b/src/documents/migrations/1075_workflowaction_order.py @@ -12,7 +12,7 @@ def populate_action_order(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("documents", "1075_alter_paperlesstask_task_name"), + ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), ] operations = [ From 62248f5702cac19a4fc10811c3347de08585c04d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:04:51 -0800 Subject: [PATCH 43/57] Chore: use consts in doc details --- .../components/document-detail/document-detail.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 5054ed517..5bac6fe72 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -285,10 +285,10 @@ export class DocumentDetailComponent if ( element && element.nativeElement.offsetParent !== null && - this.nav?.activeId == 4 + this.nav?.activeId == DocumentDetailNavIDs.Preview ) { // its visible - setTimeout(() => this.nav?.select(1)) + setTimeout(() => this.nav?.select(DocumentDetailNavIDs.Details)) } } From 771f3f150a745fa6c8ab4c1392fffd2f59104d15 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:18:23 -0800 Subject: [PATCH 44/57] Bump version to 2.20.5 --- pyproject.toml | 2 +- src-ui/package.json | 2 +- src-ui/src/environments/environment.prod.ts | 2 +- src/paperless/version.py | 2 +- uv.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64df97c17..d4d24a7e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.4" +version = "2.20.5" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index 9690e86c0..6d9046f65 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.4", + "version": "2.20.5", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index d27ab9966..9ebf29d16 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.4', + version: '2.20.5', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/paperless/version.py b/src/paperless/version.py index 0ce227357..aeeee68e0 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 4) +__version__: Final[tuple[int, int, int]] = (2, 20, 5) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/uv.lock b/uv.lock index fccc00ada..ac7763525 100644 --- a/uv.lock +++ b/uv.lock @@ -2115,7 +2115,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.4" +version = "2.20.5" source = { virtual = "." } dependencies = [ { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, From 245d9fb4a1989c5b9d11376b75cdba9822acff76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:57:26 -0800 Subject: [PATCH 45/57] Documentation: Add v2.20.5 changelog (#11824) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/changelog.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 29f955256..f222a7305 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,21 @@ # Changelog +## paperless-ngx 2.20.5 + +### Bug Fixes + +- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811)) +- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781)) + +### All App Changes + +
    +2 changes + +- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811)) +- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781)) +
    + ## paperless-ngx 2.20.4 ### Security From 29016938605836fcfe8e1a0541f0790c5267b489 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:16:48 -0800 Subject: [PATCH 46/57] Fix: fix tag list horizontal scroll, again (#11839) --- src-ui/src/app/components/common/input/tags/tags.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index c86792728..94da62699 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -23,7 +23,7 @@ // Dropdown hierarchy reveal for ng-select options ::ng-deep .ng-dropdown-panel .ng-option { - overflow-x: scroll !important; + overflow-x: auto !important; .tag-option-row { font-size: 1rem; From 6c454553848314df532d5553997d0712b6c2ac05 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:29:47 -0800 Subject: [PATCH 47/57] Narrow scope of these css rules --- .../app/components/common/input/tags/tags.component.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 94da62699..2f06247bd 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -22,7 +22,7 @@ } // Dropdown hierarchy reveal for ng-select options -::ng-deep .ng-dropdown-panel .ng-option { +:host ::ng-deep .ng-dropdown-panel .ng-option { overflow-x: auto !important; .tag-option-row { @@ -41,12 +41,12 @@ } } -::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { +:host ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { max-width: 1000px; } ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { background: transparent; } From 5381bc590704de9f0a51cce171285c26aa0a1e0f Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:16:48 -0800 Subject: [PATCH 48/57] Fix: fix tag list horizontal scroll, again (#11839) --- src-ui/src/app/components/common/input/tags/tags.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index c86792728..94da62699 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -23,7 +23,7 @@ // Dropdown hierarchy reveal for ng-select options ::ng-deep .ng-dropdown-panel .ng-option { - overflow-x: scroll !important; + overflow-x: auto !important; .tag-option-row { font-size: 1rem; From d1aa76e4cee0ba74416bf23f1a4dfbeb5be46b5e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:29:47 -0800 Subject: [PATCH 49/57] Narrow scope of these css rules --- .../app/components/common/input/tags/tags.component.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 94da62699..2f06247bd 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -22,7 +22,7 @@ } // Dropdown hierarchy reveal for ng-select options -::ng-deep .ng-dropdown-panel .ng-option { +:host ::ng-deep .ng-dropdown-panel .ng-option { overflow-x: auto !important; .tag-option-row { @@ -41,12 +41,12 @@ } } -::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { +:host ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { max-width: 1000px; } ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { background: transparent; } From 51b466a86be82ef22b95e1c6aca6d3b980e9fdee Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:37:48 -0800 Subject: [PATCH 50/57] Feature: Simplify and improve the consumer (#11753) --- docs/advanced_usage.md | 6 +- docs/configuration.md | 90 +- docs/migration.md | 19 + docs/setup.md | 3 +- docs/troubleshooting.md | 48 +- docs/usage.md | 2 +- mkdocs.yml | 3 +- paperless.conf.example | 4 +- pyproject.toml | 3 +- .../management/commands/document_consumer.py | 743 +++++---- .../tests/test_management_consumer.py | 1370 ++++++++++++----- src/paperless/settings.py | 27 +- uv.lock | 115 +- 13 files changed, 1625 insertions(+), 808 deletions(-) create mode 100644 docs/migration.md diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index de1068864..89e076167 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -501,7 +501,7 @@ The `datetime` filter formats a datetime string or datetime object using Python' See the [strftime format code documentation](https://docs.python.org/3.13/library/datetime.html#strftime-and-strptime-format-codes) for the possible codes and their meanings. -##### Date Localization +##### Date Localization {#date-localization} The `localize_date` filter formats a date or datetime object into a localized string using Babel internationalization. This takes into account the provided locale for translation. Since this must be used on a date or datetime object, @@ -851,8 +851,8 @@ followed by the even pages. It's important that the scan files get consumed in the correct order, and one at a time. You therefore need to make sure that Paperless is running while you upload the files into -the directory; and if you're using [polling](configuration.md#polling), make sure that -`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear, +the directory; and if you're using polling, make sure that +`CONSUMER_POLLING_INTERVAL` is set to a value lower than it takes for the second scan to appear, like 5-10 or even lower. Another thing that might happen is that you start a double sided scan, but then forget diff --git a/docs/configuration.md b/docs/configuration.md index b7b24d313..cc829342d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1175,21 +1175,45 @@ don't exist yet. #### [`PAPERLESS_CONSUMER_IGNORE_PATTERNS=`](#PAPERLESS_CONSUMER_IGNORE_PATTERNS) {#PAPERLESS_CONSUMER_IGNORE_PATTERNS} -: By default, paperless ignores certain files and folders in the -consumption directory, such as system files created by the Mac OS -or hidden folders some tools use to store data. +: Additional regex patterns for files to ignore in the consumption directory. Patterns are matched against filenames only (not full paths) +using Python's `re.match()`, which anchors at the start of the filename. - This can be adjusted by configuring a custom json array with - patterns to exclude. + See the [watchfiles documentation](https://watchfiles.helpmanual.io/api/filters/#watchfiles.BaseFilter.ignore_entity_patterns) - For example, `.DS_STORE/*` will ignore any files found in a folder - named `.DS_STORE`, including `.DS_STORE/bar.pdf` and `foo/.DS_STORE/bar.pdf` + This setting is for additional patterns beyond the built-in defaults. Common system files and directories are already ignored automatically. + The patterns will be compiled via Python's standard `re` module. - A pattern like `._*` will ignore anything starting with `._`, including: - `._foo.pdf` and `._bar/foo.pdf` + Example custom patterns: - Defaults to - `[".DS_Store", ".DS_STORE", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*", "Thumbs.db"]`. + ```json + ["^temp_", "\\.bak$", "^~"] + ``` + + This would ignore: + + - Files starting with `temp_` (e.g., `temp_scan.pdf`) + - Files ending with `.bak` (e.g., `document.pdf.bak`) + - Files starting with `~` (e.g., `~$document.docx`) + + Defaults to `[]` (empty list, uses only built-in defaults). + + The default ignores are `[.DS_Store, .DS_STORE, ._*, desktop.ini, Thumbs.db]` and cannot be overridden. + +#### [`PAPERLESS_CONSUMER_IGNORE_DIRS=`](#PAPERLESS_CONSUMER_IGNORE_DIRS) {#PAPERLESS_CONSUMER_IGNORE_DIRS} + +: Additional directory names to ignore in the consumption directory. Directories matching these names (and all their contents) will be skipped. + + This setting is for additional directories beyond the built-in defaults. Matching is done by directory name only, not full path. + + Example: + + ```json + ["temp", "incoming", ".hidden"] + ``` + + Defaults to `[]` (empty list, uses only built-in defaults). + + The default ignores are `[.stfolder, .stversions, .localized, @eaDir, .Spotlight-V100, .Trashes, __MACOSX]` and cannot be overridden. #### [`PAPERLESS_CONSUMER_BARCODE_SCANNER=`](#PAPERLESS_CONSUMER_BARCODE_SCANNER) {#PAPERLESS_CONSUMER_BARCODE_SCANNER} @@ -1288,48 +1312,24 @@ within your documents. Defaults to false. -### Polling {#polling} +#### [`PAPERLESS_CONSUMER_POLLING_INTERVAL=`](#PAPERLESS_CONSUMER_POLLING_INTERVAL) {#PAPERLESS_CONSUMER_POLLING_INTERVAL} -#### [`PAPERLESS_CONSUMER_POLLING=`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING} +: Configures how the consumer detects new files in the consumption directory. -: If paperless won't find documents added to your consume folder, it -might not be able to automatically detect filesystem changes. In -that case, specify a polling interval in seconds here, which will -then cause paperless to periodically check your consumption -directory for changes. This will also disable listening for file -system changes with `inotify`. + When set to `0` (default), paperless uses native filesystem notifications for efficient, immediate detection of new files. - Defaults to 0, which disables polling and uses filesystem - notifications. + When set to a positive number, paperless polls the consumption directory at that interval in seconds. Use polling for network filesystems (NFS, SMB/CIFS) where native notifications may not work reliably. -#### [`PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=`](#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT) {#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT} + Defaults to 0. -: If consumer polling is enabled, sets the maximum number of times -paperless will check for a file to remain unmodified. If a file's -modification time and size are identical for two consecutive checks, it -will be consumed. +#### [`PAPERLESS_CONSUMER_STABILITY_DELAY=`](#PAPERLESS_CONSUMER_STABILITY_DELAY) {#PAPERLESS_CONSUMER_STABILITY_DELAY} - Defaults to 5. +: Sets the time in seconds that a file must remain unchanged (same size and modification time) before paperless will begin consuming it. -#### [`PAPERLESS_CONSUMER_POLLING_DELAY=`](#PAPERLESS_CONSUMER_POLLING_DELAY) {#PAPERLESS_CONSUMER_POLLING_DELAY} + Increase this value if you experience issues with files being consumed before they are fully written, particularly on slower network storage or + with certain scanner quirks -: If consumer polling is enabled, sets the delay in seconds between -each check (above) paperless will do while waiting for a file to -remain unmodified. - - Defaults to 5. - -### iNotify {#inotify} - -#### [`PAPERLESS_CONSUMER_INOTIFY_DELAY=`](#PAPERLESS_CONSUMER_INOTIFY_DELAY) {#PAPERLESS_CONSUMER_INOTIFY_DELAY} - -: Sets the time in seconds the consumer will wait for additional -events from inotify before the consumer will consider a file ready -and begin consumption. Certain scanners or network setups may -generate multiple events for a single file, leading to multiple -consumers working on the same file. Configure this to prevent that. - - Defaults to 0.5 seconds. + Defaults to 5.0 seconds. ## Workflow webhooks diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 000000000..2ef850cbe --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,19 @@ +# v3 Migration Guide + +## Consumer Settings Changes + +The v3 consumer command uses a [different library](https://watchfiles.helpmanual.io/) to unify +the watching for new files in the consume directory. For the user, this removes several configuration options related to delays and retries +and replaces with a single unified setting. It also adjusts how the consumer ignore filtering happens, replaced `fnmatch` with `regex` and +separating the directory ignore from the file ignore. + +### Summary + +| Old Setting | New Setting | Notes | +| ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `CONSUMER_POLLING` | [`CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL) | Renamed for clarity | +| `CONSUMER_INOTIFY_DELAY` | [`CONSUMER_STABILITY_DELAY`](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) | Unified for all modes | +| `CONSUMER_POLLING_DELAY` | _Removed_ | Use `CONSUMER_STABILITY_DELAY` | +| `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking | +| `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones | +| _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults | diff --git a/docs/setup.md b/docs/setup.md index 3e7ac1be3..f0381f076 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -124,8 +124,7 @@ account. The script essentially automatically performs the steps described in [D system notifications with `inotify`. When storing the consumption directory on such a file system, paperless will not pick up new files with the default configuration. You will need to use - [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See - [here](configuration.md#polling). + [`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL), which will disable inotify. 5. Run `docker compose pull`. This will pull the image from the GitHub container registry by default but you can change the image to pull from Docker Hub by changing the `image` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e20751875..94e12307e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -46,9 +46,9 @@ run: If you notice that the consumer will only pickup files in the consumption directory at startup, but won't find any other files added later, you will need to enable filesystem polling with the configuration -option [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING). +option [`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL). -This will disable listening to filesystem changes with inotify and +This will disable automatic listening for filesystem changes and paperless will manually check the consumption directory for changes instead. @@ -234,47 +234,9 @@ FileNotFoundError: [Errno 2] No such file or directory: '/tmp/ocrmypdf.io.yhk3zb This probably indicates paperless tried to consume the same file twice. This can happen for a number of reasons, depending on how documents are -placed into the consume folder. If paperless is using inotify (the -default) to check for documents, try adjusting the -[inotify configuration](configuration.md#inotify). If polling is enabled, try adjusting the -[polling configuration](configuration.md#polling). - -## Consumer fails waiting for file to remain unmodified. - -You might find messages like these in your log files: - -``` -[ERROR] [paperless.management.consumer] Timeout while waiting on file /usr/src/paperless/src/../consume/SCN_0001.pdf to remain unmodified. -``` - -This indicates paperless timed out while waiting for the file to be -completely written to the consume folder. Adjusting -[polling configuration](configuration.md#polling) values should resolve the issue. - -!!! note - - The user will need to manually move the file out of the consume folder - and back in, for the initial failing file to be consumed. - -## Consumer fails reporting "OS reports file as busy still". - -You might find messages like these in your log files: - -``` -[WARNING] [paperless.management.consumer] Not consuming file /usr/src/paperless/src/../consume/SCN_0001.pdf: OS reports file as busy still -``` - -This indicates paperless was unable to open the file, as the OS reported -the file as still being in use. To prevent a crash, paperless did not -try to consume the file. If paperless is using inotify (the default) to -check for documents, try adjusting the -[inotify configuration](configuration.md#inotify). If polling is enabled, try adjusting the -[polling configuration](configuration.md#polling). - -!!! note - - The user will need to manually move the file out of the consume folder - and back in, for the initial failing file to be consumed. +placed into the consume folder, such as how a scanner may modify a file multiple times as it scans. +Try adjusting the +[file stability delay](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) to a larger value. ## Log reports "Creating PaperlessTask failed". diff --git a/docs/usage.md b/docs/usage.md index f5c99aeaf..cac07f4a5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -565,7 +565,7 @@ This allows for complex logic to be used to generate the title, including [logic and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11). The template is provided as a string. -Using Jinja2 Templates is also useful for [Date localization](advanced_usage.md#Date-Localization) in the title. +Using Jinja2 Templates is also useful for [Date localization](advanced_usage.md#date-localization) in the title. The available inputs differ depending on the type of workflow trigger. This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been diff --git a/mkdocs.yml b/mkdocs.yml index 05826f25f..69a15193a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,8 +69,9 @@ nav: - development.md - 'FAQs': faq.md - troubleshooting.md + - 'Migration to v3': migration.md - changelog.md -copyright: Copyright © 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team +copyright: Copyright © 2016 - 2026 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team extra: social: - icon: fontawesome/brands/github diff --git a/paperless.conf.example b/paperless.conf.example index 1ba21f41d..424f6cce9 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -55,10 +55,10 @@ #PAPERLESS_TASK_WORKERS=1 #PAPERLESS_THREADS_PER_WORKER=1 #PAPERLESS_TIME_ZONE=UTC -#PAPERLESS_CONSUMER_POLLING=10 +#PAPERLESS_CONSUMER_POLLING_INTERVAL=10 #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false #PAPERLESS_CONSUMER_RECURSIVE=false -#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"] +#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[] # Defaults are built in; add filename regexes, e.g. ["^\\.DS_Store$", "^desktop\\.ini$"] #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false #PAPERLESS_CONSUMER_ENABLE_BARCODES=false #PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT diff --git a/pyproject.toml b/pyproject.toml index e35dcd1bf..090854388 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,6 @@ dependencies = [ "gotenberg-client~=0.13.1", "httpx-oauth~=0.16", "imap-tools~=1.11.0", - "inotifyrecursive~=0.3", "jinja2~=3.1.5", "langdetect~=1.0.9", "llama-index-core>=0.14.12", @@ -79,7 +78,7 @@ dependencies = [ "tika-client~=0.10.0", "torch~=2.9.1", "tqdm~=4.67.1", - "watchdog~=6.0", + "watchfiles>=1.1.1", "whitenoise~=6.9", "whoosh-reloaded>=2.7.5", "zxing-cpp~=2.3.0", diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 97027e02d..e57569129 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -1,135 +1,343 @@ +""" +Document consumer management command. + +Watches a consumption directory for new documents and queues them for processing. +Uses watchfiles for efficient file system monitoring with support for both +native OS notifications and polling fallback. +""" + +from __future__ import annotations + import logging -import os -from concurrent.futures import ThreadPoolExecutor -from fnmatch import filter +from dataclasses import dataclass from pathlib import Path -from pathlib import PurePath from threading import Event from time import monotonic -from time import sleep +from typing import TYPE_CHECKING from typing import Final from django import db from django.conf import settings from django.core.management.base import BaseCommand from django.core.management.base import CommandError -from watchdog.events import FileSystemEventHandler -from watchdog.observers.polling import PollingObserver +from watchfiles import Change +from watchfiles import DefaultFilter +from watchfiles import watch from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource from documents.models import Tag -from documents.parsers import is_file_ext_supported +from documents.parsers import get_supported_file_extensions from documents.tasks import consume_file -try: - from inotifyrecursive import INotify - from inotifyrecursive import flags -except ImportError: # pragma: no cover - INotify = flags = None +if TYPE_CHECKING: + from collections.abc import Iterator + logger = logging.getLogger("paperless.management.consumer") -def _tags_from_path(filepath: Path) -> list[int]: +@dataclass +class TrackedFile: + """Represents a file being tracked for stability.""" + + path: Path + last_event_time: float + last_mtime: float | None = None + last_size: int | None = None + + def update_stats(self) -> bool: + """ + Update file stats. Returns True if file exists and stats were updated. + """ + try: + stat = self.path.stat() + self.last_mtime = stat.st_mtime + self.last_size = stat.st_size + return True + except OSError: + return False + + def is_unchanged(self) -> bool: + """ + Check if file stats match the previously recorded values. + Returns False if file doesn't exist or stats changed. + """ + try: + stat = self.path.stat() + return stat.st_mtime == self.last_mtime and stat.st_size == self.last_size + except OSError: + return False + + +class FileStabilityTracker: """ - Walk up the directory tree from filepath to CONSUMPTION_DIR + Tracks file events and determines when files are stable for consumption. + + A file is considered stable when: + 1. No new events have been received for it within the stability delay + 2. Its size and modification time haven't changed + 3. It still exists as a regular file + + This handles various edge cases: + - Network copies that write in chunks + - Scanners that open/close files multiple times + - Temporary files that get renamed + - Files that are deleted before becoming stable + """ + + def __init__(self, stability_delay: float = 1.0) -> None: + """ + Initialize the tracker. + + Args: + stability_delay: Time in seconds a file must remain unchanged + before being considered stable. + """ + self.stability_delay = stability_delay + self._tracked: dict[Path, TrackedFile] = {} + + def track(self, path: Path, change: Change) -> None: + """ + Register a file event. + + Args: + path: The file path that changed. + change: The type of change (added, modified, deleted). + """ + path = path.resolve() + + match change: + case Change.deleted: + self._tracked.pop(path, None) + logger.debug(f"Stopped tracking deleted file: {path}") + case Change.added | Change.modified: + current_time = monotonic() + if path in self._tracked: + tracked = self._tracked[path] + tracked.last_event_time = current_time + tracked.update_stats() + logger.debug(f"Updated tracking for: {path}") + else: + tracked = TrackedFile(path=path, last_event_time=current_time) + if tracked.update_stats(): + self._tracked[path] = tracked + logger.debug(f"Started tracking: {path}") + else: + logger.debug(f"Could not stat file, not tracking: {path}") + + def get_stable_files(self) -> Iterator[Path]: + """ + Yield files that have been stable for the configured delay. + + Files are removed from tracking once yielded or determined to be invalid. + """ + current_time = monotonic() + to_remove: list[Path] = [] + to_yield: list[Path] = [] + + for path, tracked in self._tracked.items(): + time_since_event = current_time - tracked.last_event_time + + if time_since_event < self.stability_delay: + continue + + # File has waited long enough, verify it's unchanged + if not tracked.is_unchanged(): + # Stats changed or file gone - update and wait again + if tracked.update_stats(): + tracked.last_event_time = current_time + logger.debug(f"File changed during stability check: {path}") + else: + # File no longer exists, remove from tracking + to_remove.append(path) + logger.debug(f"File disappeared during stability check: {path}") + continue + + # File is stable, we can return it + to_yield.append(path) + logger.info(f"File is stable: {path}") + + # Remove files that are no longer valid + for path in to_remove: + self._tracked.pop(path, None) + + # Remove and yield stable files + for path in to_yield: + self._tracked.pop(path, None) + yield path + + def has_pending_files(self) -> bool: + """Check if there are files waiting for stability check.""" + return len(self._tracked) > 0 + + @property + def pending_count(self) -> int: + """Number of files being tracked.""" + return len(self._tracked) + + +class ConsumerFilter(DefaultFilter): + """ + Filter for watchfiles that accepts only supported document types + and ignores system files/directories. + + Extends DefaultFilter leveraging its built-in filtering: + - `ignore_dirs`: Directory names to ignore (and all their contents) + - `ignore_entity_patterns`: Regex patterns matched against filename/dirname only + + We add custom logic for file extension filtering (only accept supported + document types), which the library doesn't provide. + """ + + # Regex patterns for files to always ignore (matched against filename only) + # These are passed to DefaultFilter.ignore_entity_patterns + DEFAULT_IGNORE_PATTERNS: Final[tuple[str, ...]] = ( + r"^\.DS_Store$", + r"^\.DS_STORE$", + r"^\._.*", + r"^desktop\.ini$", + r"^Thumbs\.db$", + ) + + # Directories to always ignore (passed to DefaultFilter.ignore_dirs) + # These are matched by directory name, not full path + DEFAULT_IGNORE_DIRS: Final[tuple[str, ...]] = ( + ".stfolder", # Syncthing + ".stversions", # Syncthing + ".localized", # macOS + "@eaDir", # Synology NAS + ".Spotlight-V100", # macOS + ".Trashes", # macOS + "__MACOSX", # macOS archive artifacts + ) + + def __init__( + self, + *, + supported_extensions: frozenset[str] | None = None, + ignore_patterns: list[str] | None = None, + ignore_dirs: list[str] | None = None, + ) -> None: + """ + Initialize the consumer filter. + + Args: + supported_extensions: Set of file extensions to accept (e.g., {".pdf", ".png"}). + If None, uses get_supported_file_extensions(). + ignore_patterns: Additional regex patterns to ignore (matched against filename). + ignore_dirs: Additional directory names to ignore (merged with defaults). + """ + # Get supported extensions + if supported_extensions is None: + supported_extensions = frozenset(get_supported_file_extensions()) + self._supported_extensions = supported_extensions + + # Combine default and user patterns + all_patterns: list[str] = list(self.DEFAULT_IGNORE_PATTERNS) + if ignore_patterns: + all_patterns.extend(ignore_patterns) + + # Combine default and user ignore_dirs + all_ignore_dirs: list[str] = list(self.DEFAULT_IGNORE_DIRS) + if ignore_dirs: + all_ignore_dirs.extend(ignore_dirs) + + # Let DefaultFilter handle all the pattern and directory filtering + super().__init__( + ignore_dirs=tuple(all_ignore_dirs), + ignore_entity_patterns=tuple(all_patterns), + ignore_paths=(), + ) + + def __call__(self, change: Change, path: str) -> bool: + """ + Filter function for watchfiles. + + Returns True if the path should be watched, False to ignore. + + The parent DefaultFilter handles: + - Hidden files/directories (starting with .) + - Directories in ignore_dirs + - Files/directories matching ignore_entity_patterns + + We additionally filter files by extension. + """ + # Let parent filter handle directory ignoring and pattern matching + if not super().__call__(change, path): + return False + + path_obj = Path(path) + + # For directories, parent filter already handled everything + if path_obj.is_dir(): + return True + + # For files, check extension + return self._has_supported_extension(path_obj) + + def _has_supported_extension(self, path: Path) -> bool: + """Check if the file has a supported extension.""" + suffix = path.suffix.lower() + return suffix in self._supported_extensions + + +def _tags_from_path(filepath: Path, consumption_dir: Path) -> list[int]: + """ + Walk up the directory tree from filepath to consumption_dir and get or create Tag IDs for every directory. - Returns set of Tag models + Returns list of Tag primary keys. """ db.close_old_connections() - tag_ids = set() - path_parts = filepath.relative_to(settings.CONSUMPTION_DIR).parent.parts + tag_ids: set[int] = set() + path_parts = filepath.relative_to(consumption_dir).parent.parts + for part in path_parts: - tag_ids.add( - Tag.objects.get_or_create(name__iexact=part, defaults={"name": part})[0].pk, + tag, _ = Tag.objects.get_or_create( + name__iexact=part, + defaults={"name": part}, ) + tag_ids.add(tag.pk) return list(tag_ids) -def _is_ignored(filepath: Path) -> bool: +def _consume_file( + filepath: Path, + consumption_dir: Path, + *, + subdirs_as_tags: bool, +) -> None: """ - Checks if the given file should be ignored, based on configured - patterns. + Queue a file for consumption. - Returns True if the file is ignored, False otherwise + Args: + filepath: Path to the file to consume. + consumption_dir: Base consumption directory. + subdirs_as_tags: Whether to create tags from subdirectory names. """ - # Trim out the consume directory, leaving only filename and it's - # path relative to the consume directory - filepath_relative = PurePath(filepath).relative_to(settings.CONSUMPTION_DIR) - - # March through the components of the path, including directories and the filename - # looking for anything matching - # foo/bar/baz/file.pdf -> (foo, bar, baz, file.pdf) - parts = [] - for part in filepath_relative.parts: - # If the part is not the name (ie, it's a dir) - # Need to append the trailing slash or fnmatch doesn't match - # fnmatch("dir", "dir/*") == False - # fnmatch("dir/", "dir/*") == True - if part != filepath_relative.name: - part = part + "/" - parts.append(part) - - for pattern in settings.CONSUMER_IGNORE_PATTERNS: - if len(filter(parts, pattern)): - return True - - return False - - -def _consume(filepath: Path) -> None: - # Check permissions early + # Verify file still exists and is accessible try: - filepath.stat() - except (PermissionError, OSError): - logger.warning(f"Not consuming file {filepath}: Permission denied.") + if not filepath.is_file(): + logger.debug(f"Not consuming {filepath}: not a file or doesn't exist") + return + except OSError as e: + logger.warning(f"Not consuming {filepath}: {e}") return - if filepath.is_dir() or _is_ignored(filepath): - return - - if not filepath.is_file(): - logger.debug(f"Not consuming file {filepath}: File has moved.") - return - - if not is_file_ext_supported(filepath.suffix): - logger.warning(f"Not consuming file {filepath}: Unknown file extension.") - return - - # Total wait time: up to 500ms - os_error_retry_count: Final[int] = 50 - os_error_retry_wait: Final[float] = 0.01 - - read_try_count = 0 - file_open_ok = False - os_error_str = None - - while (read_try_count < os_error_retry_count) and not file_open_ok: + # Get tags from path if configured + tag_ids: list[int] | None = None + if subdirs_as_tags: try: - with filepath.open("rb"): - file_open_ok = True - except OSError as e: - read_try_count += 1 - os_error_str = str(e) - sleep(os_error_retry_wait) + tag_ids = _tags_from_path(filepath, consumption_dir) + except Exception: + logger.exception(f"Error creating tags from path for {filepath}") - if read_try_count >= os_error_retry_count: - logger.warning(f"Not consuming file {filepath}: OS reports {os_error_str}") - return - - tag_ids = None + # Queue for consumption try: - if settings.CONSUMER_SUBDIRS_AS_TAGS: - tag_ids = _tags_from_path(filepath) - except Exception: - logger.exception("Error creating tags from path") - - try: - logger.info(f"Adding {filepath} to the task queue.") + logger.info(f"Adding {filepath} to the task queue") consume_file.delay( ConsumableDocument( source=DocumentSource.ConsumeFolder, @@ -138,228 +346,209 @@ def _consume(filepath: Path) -> None: DocumentMetadataOverrides(tag_ids=tag_ids), ) except Exception: - # Catch all so that the consumer won't crash. - # This is also what the test case is listening for to check for - # errors. - logger.exception("Error while consuming document") - - -def _consume_wait_unmodified(file: Path) -> None: - """ - Waits for the given file to appear unmodified based on file size - and modification time. Will wait a configured number of seconds - and retry a configured number of times before either consuming or - giving up - """ - if _is_ignored(file): - return - - logger.debug(f"Waiting for file {file} to remain unmodified") - mtime = -1 - size = -1 - current_try = 0 - while current_try < settings.CONSUMER_POLLING_RETRY_COUNT: - try: - stat_data = file.stat() - new_mtime = stat_data.st_mtime - new_size = stat_data.st_size - except FileNotFoundError: - logger.debug( - f"File {file} moved while waiting for it to remain unmodified.", - ) - return - if new_mtime == mtime and new_size == size: - _consume(file) - return - mtime = new_mtime - size = new_size - sleep(settings.CONSUMER_POLLING_DELAY) - current_try += 1 - - logger.error(f"Timeout while waiting on file {file} to remain unmodified.") - - -class Handler(FileSystemEventHandler): - def __init__(self, pool: ThreadPoolExecutor) -> None: - super().__init__() - self._pool = pool - - def on_created(self, event): - self._pool.submit(_consume_wait_unmodified, Path(event.src_path)) - - def on_moved(self, event): - self._pool.submit(_consume_wait_unmodified, Path(event.dest_path)) + logger.exception(f"Error while queuing document {filepath}") class Command(BaseCommand): """ - On every iteration of an infinite loop, consume what we can from the - consumption directory. + Watch a consumption directory and queue new documents for processing. + + Uses watchfiles for efficient file system monitoring. Supports both + native OS notifications (inotify on Linux, FSEvents on macOS) and + polling for network filesystems. """ - # This is here primarily for the tests and is irrelevant in production. - stop_flag = Event() - # Also only for testing, configures in one place the timeout used before checking - # the stop flag - testing_timeout_s: Final[float] = 0.5 - testing_timeout_ms: Final[float] = testing_timeout_s * 1000.0 + help = "Watch the consumption directory for new documents" - def add_arguments(self, parser): + # For testing - allows tests to stop the consumer + stop_flag: Event = Event() + + # Testing timeout in seconds + testing_timeout_s: Final[float] = 0.5 + + def add_arguments(self, parser) -> None: parser.add_argument( "directory", - default=settings.CONSUMPTION_DIR, + default=None, nargs="?", - help="The consumption directory.", + help="The consumption directory (defaults to CONSUMPTION_DIR setting)", + ) + parser.add_argument( + "--oneshot", + action="store_true", + help="Process existing files and exit without watching", ) - parser.add_argument("--oneshot", action="store_true", help="Run only once.") - - # Only use during unit testing, will configure a timeout - # Leaving it unset or false and the consumer will exit when it - # receives SIGINT parser.add_argument( "--testing", action="store_true", - help="Flag used only for unit testing", + help="Enable testing mode with shorter timeouts", default=False, ) - def handle(self, *args, **options): - directory = options["directory"] - recursive = settings.CONSUMER_RECURSIVE - + def handle(self, *args, **options) -> None: + # Resolve consumption directory + directory = options.get("directory") if not directory: - raise CommandError("CONSUMPTION_DIR does not appear to be set.") + directory = getattr(settings, "CONSUMPTION_DIR", None) + if not directory: + raise CommandError("CONSUMPTION_DIR is not configured") directory = Path(directory).resolve() - if not directory.is_dir(): - raise CommandError(f"Consumption directory {directory} does not exist") + if not directory.exists(): + raise CommandError(f"Consumption directory does not exist: {directory}") - # Consumer will need this + if not directory.is_dir(): + raise CommandError(f"Consumption path is not a directory: {directory}") + + # Ensure scratch directory exists settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True) - if recursive: - for dirpath, _, filenames in os.walk(directory): - for filename in filenames: - filepath = Path(dirpath) / filename - _consume(filepath) - else: - for filepath in directory.iterdir(): - _consume(filepath) + # Get settings + recursive: bool = settings.CONSUMER_RECURSIVE + subdirs_as_tags: bool = settings.CONSUMER_SUBDIRS_AS_TAGS + polling_interval: float = settings.CONSUMER_POLLING_INTERVAL + stability_delay: float = settings.CONSUMER_STABILITY_DELAY + ignore_patterns: list[str] = settings.CONSUMER_IGNORE_PATTERNS + ignore_dirs: list[str] = settings.CONSUMER_IGNORE_DIRS + is_testing: bool = options.get("testing", False) + is_oneshot: bool = options.get("oneshot", False) - if options["oneshot"]: + # Create filter + consumer_filter = ConsumerFilter( + ignore_patterns=ignore_patterns, + ignore_dirs=ignore_dirs, + ) + + # Process existing files + self._process_existing_files( + directory=directory, + recursive=recursive, + subdirs_as_tags=subdirs_as_tags, + consumer_filter=consumer_filter, + ) + + if is_oneshot: + logger.info("Oneshot mode: processed existing files, exiting") return - if settings.CONSUMER_POLLING == 0 and INotify: - self.handle_inotify(directory, recursive, is_testing=options["testing"]) + # Start watching + self._watch_directory( + directory=directory, + recursive=recursive, + subdirs_as_tags=subdirs_as_tags, + consumer_filter=consumer_filter, + polling_interval=polling_interval, + stability_delay=stability_delay, + is_testing=is_testing, + ) + + logger.debug("Consumer exiting") + + def _process_existing_files( + self, + *, + directory: Path, + recursive: bool, + subdirs_as_tags: bool, + consumer_filter: ConsumerFilter, + ) -> None: + """Process any existing files in the consumption directory.""" + logger.info(f"Processing existing files in {directory}") + + glob_pattern = "**/*" if recursive else "*" + + for filepath in directory.glob(glob_pattern): + # Use filter to check if file should be processed + if not filepath.is_file(): + continue + + if not consumer_filter(Change.added, str(filepath)): + continue + + _consume_file( + filepath=filepath, + consumption_dir=directory, + subdirs_as_tags=subdirs_as_tags, + ) + + def _watch_directory( + self, + *, + directory: Path, + recursive: bool, + subdirs_as_tags: bool, + consumer_filter: ConsumerFilter, + polling_interval: float, + stability_delay: float, + is_testing: bool, + ) -> None: + """Watch directory for changes and process stable files.""" + use_polling = polling_interval > 0 + poll_delay_ms = int(polling_interval * 1000) if use_polling else 0 + + if use_polling: + logger.info( + f"Watching {directory} using polling (interval: {polling_interval}s)", + ) else: - if INotify is None and settings.CONSUMER_POLLING == 0: # pragma: no cover - logger.warning("Using polling as INotify import failed") - self.handle_polling(directory, recursive, is_testing=options["testing"]) + logger.info(f"Watching {directory} using native file system events") - logger.debug("Consumer exiting.") + # Create stability tracker + tracker = FileStabilityTracker(stability_delay=stability_delay) - def handle_polling(self, directory, recursive, *, is_testing: bool): - logger.info(f"Polling directory for changes: {directory}") + # Calculate timeouts + stability_timeout_ms = int(stability_delay * 1000) + testing_timeout_ms = int(self.testing_timeout_s * 1000) - timeout = None - if is_testing: - timeout = self.testing_timeout_s - logger.debug(f"Configuring timeout to {timeout}s") + # Start with no timeout (wait indefinitely for first event) + # unless in testing mode + timeout_ms = testing_timeout_ms if is_testing else 0 - polling_interval = settings.CONSUMER_POLLING - if polling_interval == 0: # pragma: no cover - # Only happens if INotify failed to import - logger.warning("Using polling of 10s, consider setting this") - polling_interval = 10 + self.stop_flag.clear() - with ThreadPoolExecutor(max_workers=4) as pool: - observer = PollingObserver(timeout=polling_interval) - observer.schedule(Handler(pool), directory, recursive=recursive) - observer.start() + while not self.stop_flag.is_set(): try: - while observer.is_alive(): - observer.join(timeout) - if self.stop_flag.is_set(): - observer.stop() - except KeyboardInterrupt: - observer.stop() - observer.join() - - def handle_inotify(self, directory, recursive, *, is_testing: bool): - logger.info(f"Using inotify to watch directory for changes: {directory}") - - timeout_ms = None - if is_testing: - timeout_ms = self.testing_timeout_ms - logger.debug(f"Configuring timeout to {timeout_ms}ms") - - inotify = INotify() - inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY - if recursive: - inotify.add_watch_recursive(directory, inotify_flags) - else: - inotify.add_watch(directory, inotify_flags) - - inotify_debounce_secs: Final[float] = settings.CONSUMER_INOTIFY_DELAY - inotify_debounce_ms: Final[int] = inotify_debounce_secs * 1000 - - finished = False - - notified_files = {} - - try: - while not finished: - try: - for event in inotify.read(timeout=timeout_ms): - path = inotify.get_path(event.wd) if recursive else directory - filepath = Path(path) / event.name - if flags.MODIFY in flags.from_mask(event.mask): - notified_files.pop(filepath, None) - else: - notified_files[filepath] = monotonic() - - # Check the files against the timeout - still_waiting = {} - # last_event_time is time of the last inotify event for this file - for filepath, last_event_time in notified_files.items(): - # Current time - last time over the configured timeout - waited_long_enough = ( - monotonic() - last_event_time - ) > inotify_debounce_secs - - # Also make sure the file exists still, some scanners might write a - # temporary file first - try: - file_still_exists = filepath.exists() and filepath.is_file() - except (PermissionError, OSError): # pragma: no cover - # If we can't check, let it fail in the _consume function - file_still_exists = True + for changes in watch( + directory, + watch_filter=consumer_filter, + rust_timeout=timeout_ms, + yield_on_timeout=True, + force_polling=use_polling, + poll_delay_ms=poll_delay_ms, + recursive=recursive, + stop_event=self.stop_flag, + ): + # Process each change + for change_type, path in changes: + path = Path(path).resolve() + if not path.is_file(): continue + logger.debug(f"Event: {change_type.name} for {path}") + tracker.track(path, change_type) - if waited_long_enough and file_still_exists: - _consume(filepath) - elif file_still_exists: - still_waiting[filepath] = last_event_time + # Check for stable files + for stable_path in tracker.get_stable_files(): + _consume_file( + filepath=stable_path, + consumption_dir=directory, + subdirs_as_tags=subdirs_as_tags, + ) - # These files are still waiting to hit the timeout - notified_files = still_waiting + # Exit watch loop to reconfigure timeout + break - # If files are waiting, need to exit read() to check them - # Otherwise, go back to infinite sleep time, but only if not testing - if len(notified_files) > 0: - timeout_ms = inotify_debounce_ms - elif is_testing: - timeout_ms = self.testing_timeout_ms - else: - timeout_ms = None + # Determine next timeout + if tracker.has_pending_files(): + # Check pending files at stability interval + timeout_ms = stability_timeout_ms + elif is_testing: + # In testing, use short timeout to check stop flag + timeout_ms = testing_timeout_ms + else: # pragma: nocover + # No pending files, wait indefinitely + timeout_ms = 0 - if self.stop_flag.is_set(): - logger.debug("Finishing because event is set") - finished = True - - except KeyboardInterrupt: - logger.info("Received SIGINT, stopping inotify") - finished = True - finally: - inotify.close() + except KeyboardInterrupt: # pragma: nocover + logger.info("Received interrupt, stopping consumer") + self.stop_flag.set() diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 38b9eadda..46aa3d374 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -1,438 +1,1018 @@ -import filecmp +""" +Tests for the document consumer management command. + +Tests are organized into classes by component: +- TestFileStabilityTracker: Unit tests for FileStabilityTracker +- TestConsumerFilter: Unit tests for ConsumerFilter +- TestConsumeFile: Unit tests for the _consume_file function +- TestTagsFromPath: Unit tests for _tags_from_path +- TestCommandValidation: Tests for command argument validation +- TestCommandOneshot: Tests for oneshot mode +- TestCommandWatch: Integration tests for the watch loop +""" + +from __future__ import annotations + +import re import shutil from pathlib import Path from threading import Thread +from time import monotonic from time import sleep -from unittest import mock +from typing import TYPE_CHECKING -from django.conf import settings +import pytest +from django import db from django.core.management import CommandError -from django.core.management import call_command -from django.test import TransactionTestCase +from django.db import DatabaseError from django.test import override_settings +from watchfiles import Change -from documents.consumer import ConsumerError from documents.data_models import ConsumableDocument -from documents.management.commands import document_consumer +from documents.data_models import DocumentSource +from documents.management.commands.document_consumer import Command +from documents.management.commands.document_consumer import ConsumerFilter +from documents.management.commands.document_consumer import FileStabilityTracker +from documents.management.commands.document_consumer import TrackedFile +from documents.management.commands.document_consumer import _consume_file +from documents.management.commands.document_consumer import _tags_from_path from documents.models import Tag -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import DocumentConsumeDelayMixin + +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Generator + from unittest.mock import MagicMock + + from pytest_django.fixtures import SettingsWrapper + from pytest_mock import MockerFixture + + +@pytest.fixture +def stability_tracker() -> FileStabilityTracker: + """Create a FileStabilityTracker with a short delay for testing.""" + return FileStabilityTracker(stability_delay=0.1) + + +@pytest.fixture +def temp_file(tmp_path: Path) -> Path: + """Create a temporary file for testing.""" + file_path = tmp_path / "test_file.pdf" + file_path.write_bytes(b"test content") + return file_path + + +@pytest.fixture +def consumption_dir(tmp_path: Path) -> Path: + """Create a temporary consumption directory for testing.""" + consume_dir = tmp_path / "consume" + consume_dir.mkdir() + return consume_dir + + +@pytest.fixture +def scratch_dir(tmp_path: Path) -> Path: + """Create a temporary scratch directory for testing.""" + scratch = tmp_path / "scratch" + scratch.mkdir() + return scratch + + +@pytest.fixture +def sample_pdf(tmp_path: Path) -> Path: + """Create a sample PDF file.""" + pdf_content = b"%PDF-1.4\n%test\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF" + pdf_path = tmp_path / "sample.pdf" + pdf_path.write_bytes(pdf_content) + return pdf_path + + +@pytest.fixture +def consumer_filter() -> ConsumerFilter: + """Create a ConsumerFilter for testing.""" + return ConsumerFilter( + supported_extensions=frozenset({".pdf", ".png", ".jpg"}), + ignore_patterns=[r"^custom_ignore"], + ) + + +@pytest.fixture +def mock_consume_file_delay(mocker: MockerFixture) -> MagicMock: + """Mock the consume_file.delay celery task.""" + mock_task = mocker.patch( + "documents.management.commands.document_consumer.consume_file", + ) + mock_task.delay = mocker.MagicMock() + return mock_task + + +@pytest.fixture +def mock_supported_extensions(mocker: MockerFixture) -> MagicMock: + """Mock get_supported_file_extensions to return only .pdf.""" + return mocker.patch( + "documents.management.commands.document_consumer.get_supported_file_extensions", + return_value={".pdf"}, + ) + + +class TestTrackedFile: + """Tests for the TrackedFile dataclass.""" + + def test_update_stats_existing_file(self, temp_file: Path) -> None: + """Test update_stats succeeds for existing file.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + assert tracked.update_stats() is True + assert tracked.last_mtime is not None + assert tracked.last_size is not None + assert tracked.last_size == len(b"test content") + + def test_update_stats_nonexistent_file(self, tmp_path: Path) -> None: + """Test update_stats fails for nonexistent file.""" + tracked = TrackedFile( + path=tmp_path / "nonexistent.pdf", + last_event_time=monotonic(), + ) + assert tracked.update_stats() is False + assert tracked.last_mtime is None + assert tracked.last_size is None + + def test_is_unchanged_same_stats(self, temp_file: Path) -> None: + """Test is_unchanged returns True when stats haven't changed.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + assert tracked.is_unchanged() is True + + def test_is_unchanged_modified_file(self, temp_file: Path) -> None: + """Test is_unchanged returns False when file is modified.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + temp_file.write_bytes(b"modified content that is longer") + assert tracked.is_unchanged() is False + + def test_is_unchanged_deleted_file(self, temp_file: Path) -> None: + """Test is_unchanged returns False when file is deleted.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + temp_file.unlink() + assert tracked.is_unchanged() is False + + +class TestFileStabilityTracker: + """Tests for the FileStabilityTracker class.""" + + def test_track_new_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a new file adds it to pending.""" + stability_tracker.track(temp_file, Change.added) + assert stability_tracker.pending_count == 1 + assert stability_tracker.has_pending_files() is True + + def test_track_modified_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a modified file updates its event time.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.05) + stability_tracker.track(temp_file, Change.modified) + assert stability_tracker.pending_count == 1 + + def test_track_deleted_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a deleted file removes it from pending.""" + stability_tracker.track(temp_file, Change.added) + assert stability_tracker.pending_count == 1 + stability_tracker.track(temp_file, Change.deleted) + assert stability_tracker.pending_count == 0 + assert stability_tracker.has_pending_files() is False + + def test_track_nonexistent_file( + self, + stability_tracker: FileStabilityTracker, + tmp_path: Path, + ) -> None: + """Test tracking a nonexistent file doesn't add it.""" + nonexistent = tmp_path / "nonexistent.pdf" + stability_tracker.track(nonexistent, Change.added) + assert stability_tracker.pending_count == 0 + + def test_get_stable_files_before_delay( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test get_stable_files returns nothing before delay expires.""" + stability_tracker.track(temp_file, Change.added) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 0 + assert stability_tracker.pending_count == 1 + + def test_get_stable_files_after_delay( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test get_stable_files returns file after delay expires.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.15) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == temp_file + assert stability_tracker.pending_count == 0 + + def test_get_stable_files_modified_during_check( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test file is not returned if modified during stability check.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.12) + temp_file.write_bytes(b"modified content") + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 0 + assert stability_tracker.pending_count == 1 + + def test_get_stable_files_deleted_during_check(self, temp_file: Path) -> None: + """Test deleted file is not returned during stability check.""" + tracker = FileStabilityTracker(stability_delay=0.1) + tracker.track(temp_file, Change.added) + sleep(0.12) + temp_file.unlink() + stable = list(tracker.get_stable_files()) + assert len(stable) == 0 + assert tracker.pending_count == 0 + + def test_get_stable_files_error_during_check( + self, + temp_file: Path, + mocker: MockerFixture, + ) -> None: + """Test a file which has become inaccessible is removed from tracking""" + + mocker.patch.object(Path, "stat", side_effect=PermissionError("denied")) + + tracker = FileStabilityTracker(stability_delay=0.1) + tracker.track(temp_file, Change.added) + stable = list(tracker.get_stable_files()) + assert len(stable) == 0 + assert tracker.pending_count == 0 + + def test_multiple_files_tracking( + self, + stability_tracker: FileStabilityTracker, + tmp_path: Path, + ) -> None: + """Test tracking multiple files independently.""" + file1 = tmp_path / "file1.pdf" + file2 = tmp_path / "file2.pdf" + file1.write_bytes(b"content1") + file2.write_bytes(b"content2") + + stability_tracker.track(file1, Change.added) + sleep(0.05) + stability_tracker.track(file2, Change.added) + + assert stability_tracker.pending_count == 2 + + sleep(0.06) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == file1 + + sleep(0.06) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == file2 + + def test_track_resolves_path( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test that tracking resolves paths consistently.""" + stability_tracker.track(temp_file, Change.added) + stability_tracker.track(temp_file.resolve(), Change.modified) + assert stability_tracker.pending_count == 1 + + +class TestConsumerFilter: + """Tests for the ConsumerFilter class.""" + + @pytest.mark.parametrize( + ("filename", "should_accept"), + [ + pytest.param("document.pdf", True, id="supported_pdf"), + pytest.param("image.png", True, id="supported_png"), + pytest.param("photo.jpg", True, id="supported_jpg"), + pytest.param("document.PDF", True, id="case_insensitive"), + pytest.param("document.xyz", False, id="unsupported_ext"), + pytest.param("document", False, id="no_extension"), + pytest.param(".DS_Store", False, id="ds_store"), + pytest.param(".DS_STORE", False, id="ds_store_upper"), + pytest.param("._document.pdf", False, id="macos_resource_fork"), + pytest.param("._hidden", False, id="macos_resource_no_ext"), + pytest.param("Thumbs.db", False, id="thumbs_db"), + pytest.param("desktop.ini", False, id="desktop_ini"), + pytest.param("custom_ignore_this.pdf", False, id="custom_pattern"), + pytest.param("stfolder.pdf", True, id="similar_to_ignored"), + pytest.param("my_document.pdf", True, id="normal_with_underscore"), + ], + ) + def test_file_filtering( + self, + consumer_filter: ConsumerFilter, + tmp_path: Path, + filename: str, + should_accept: bool, # noqa: FBT001 + ) -> None: + """Test filter correctly accepts or rejects files.""" + test_file = tmp_path / filename + test_file.touch() + assert consumer_filter(Change.added, str(test_file)) is should_accept + + @pytest.mark.parametrize( + ("dirname", "should_accept"), + [ + pytest.param(".stfolder", False, id="syncthing_stfolder"), + pytest.param(".stversions", False, id="syncthing_stversions"), + pytest.param("@eaDir", False, id="synology_eadir"), + pytest.param(".Spotlight-V100", False, id="macos_spotlight"), + pytest.param(".Trashes", False, id="macos_trashes"), + pytest.param("__MACOSX", False, id="macos_archive"), + pytest.param(".localized", False, id="macos_localized"), + pytest.param("documents", True, id="normal_dir"), + pytest.param("invoices", True, id="normal_dir_2"), + ], + ) + def test_directory_filtering( + self, + consumer_filter: ConsumerFilter, + tmp_path: Path, + dirname: str, + should_accept: bool, # noqa: FBT001 + ) -> None: + """Test filter correctly accepts or rejects directories.""" + test_dir = tmp_path / dirname + test_dir.mkdir() + assert consumer_filter(Change.added, str(test_dir)) is should_accept + + def test_default_patterns_are_valid_regex(self) -> None: + """Test that default patterns are valid regex.""" + for pattern in ConsumerFilter.DEFAULT_IGNORE_PATTERNS: + re.compile(pattern) + + def test_custom_ignore_dirs(self, tmp_path: Path) -> None: + """Test filter respects custom ignore_dirs.""" + filter_obj = ConsumerFilter( + supported_extensions=frozenset({".pdf"}), + ignore_dirs=["custom_ignored_dir"], + ) + + # Custom ignored directory should be rejected + custom_dir = tmp_path / "custom_ignored_dir" + custom_dir.mkdir() + assert filter_obj(Change.added, str(custom_dir)) is False + + # Normal directory should be accepted + normal_dir = tmp_path / "normal_dir" + normal_dir.mkdir() + assert filter_obj(Change.added, str(normal_dir)) is True + + # Default ignored directories should still be ignored + stfolder = tmp_path / ".stfolder" + stfolder.mkdir() + assert filter_obj(Change.added, str(stfolder)) is False + + +class TestConsumerFilterDefaults: + """Tests for ConsumerFilter with default settings.""" + + def test_filter_with_mocked_extensions( + self, + tmp_path: Path, + mocker: MockerFixture, + ) -> None: + """Test filter works when using mocked extensions from parser.""" + mocker.patch( + "documents.management.commands.document_consumer.get_supported_file_extensions", + return_value={".pdf", ".png"}, + ) + filter_obj = ConsumerFilter() + test_file = tmp_path / "document.pdf" + test_file.touch() + assert filter_obj(Change.added, str(test_file)) is True + + +class TestConsumeFile: + """Tests for the _consume_file function.""" + + def test_consume_queues_file( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file queues a valid file.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + + mock_consume_file_delay.delay.assert_called_once() + call_args = mock_consume_file_delay.delay.call_args + consumable_doc = call_args[0][0] + assert isinstance(consumable_doc, ConsumableDocument) + assert consumable_doc.original_file == target + assert consumable_doc.source == DocumentSource.ConsumeFolder + + def test_consume_nonexistent_file( + self, + consumption_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file handles nonexistent files gracefully.""" + _consume_file( + filepath=consumption_dir / "nonexistent.pdf", + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_directory( + self, + consumption_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file ignores directories.""" + subdir = consumption_dir / "subdir" + subdir.mkdir() + + _consume_file( + filepath=subdir, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_with_permission_error( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + mocker: MockerFixture, + ) -> None: + """Test _consume_file handles permission errors.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + mocker.patch.object(Path, "is_file", side_effect=PermissionError("denied")) + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_with_tags_error( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + mocker: MockerFixture, + ) -> None: + """Test _consume_file handles errors during tag creation""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + mocker.patch( + "documents.management.commands.document_consumer._tags_from_path", + side_effect=DatabaseError("Something happened"), + ) + + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=True, + ) + mock_consume_file_delay.delay.assert_called_once() + call_args = mock_consume_file_delay.delay.call_args + overrides = call_args[0][1] + assert overrides.tag_ids is None + + +@pytest.mark.django_db +class TestTagsFromPath: + """Tests for the _tags_from_path function.""" + + def test_creates_tags_from_subdirectories(self, consumption_dir: Path) -> None: + """Test tags are created for each subdirectory.""" + subdir = consumption_dir / "Invoice" / "2024" + subdir.mkdir(parents=True) + target = subdir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 2 + assert Tag.objects.filter(name="Invoice").exists() + assert Tag.objects.filter(name="2024").exists() + + def test_reuses_existing_tags(self, consumption_dir: Path) -> None: + """Test existing tags are reused (case-insensitive).""" + existing_tag = Tag.objects.create(name="existing") + + subdir = consumption_dir / "EXISTING" + subdir.mkdir(parents=True) + target = subdir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 1 + assert existing_tag.pk in tag_ids + assert Tag.objects.filter(name__iexact="existing").count() == 1 + + def test_no_tags_for_root_file(self, consumption_dir: Path) -> None: + """Test no tags created for files directly in consumption dir.""" + target = consumption_dir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 0 + + +class TestCommandValidation: + """Tests for command argument validation.""" + + def test_raises_for_missing_consumption_dir( + self, + settings: SettingsWrapper, + ) -> None: + """Test command raises error when directory is not provided.""" + settings.CONSUMPTION_DIR = None + with pytest.raises(CommandError, match="not configured"): + cmd = Command() + cmd.handle(directory=None, oneshot=True, testing=False) + + def test_raises_for_nonexistent_directory(self, tmp_path: Path) -> None: + """Test command raises error for nonexistent directory.""" + nonexistent = tmp_path / "nonexistent" + + with pytest.raises(CommandError, match="does not exist"): + cmd = Command() + cmd.handle(directory=str(nonexistent), oneshot=True, testing=False) + + def test_raises_for_file_instead_of_directory(self, sample_pdf: Path) -> None: + """Test command raises error when path is a file, not directory.""" + with pytest.raises(CommandError, match="not a directory"): + cmd = Command() + cmd.handle(directory=str(sample_pdf), oneshot=True, testing=False) + + +@pytest.mark.usefixtures("mock_supported_extensions") +class TestCommandOneshot: + """Tests for oneshot mode.""" + + def test_processes_existing_files( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode processes existing files.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_called_once() + + def test_processes_recursive( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode processes files recursively.""" + subdir = consumption_dir / "subdir" + subdir.mkdir() + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_RECURSIVE = True + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_called_once() + + def test_ignores_unsupported_extensions( + self, + consumption_dir: Path, + scratch_dir: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode ignores unsupported file extensions.""" + target = consumption_dir / "document.xyz" + target.write_bytes(b"content") + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_not_called() class ConsumerThread(Thread): - def __init__(self): + """Thread wrapper for running the consumer command with proper cleanup.""" + + def __init__( + self, + consumption_dir: Path, + scratch_dir: Path, + *, + recursive: bool = False, + subdirs_as_tags: bool = False, + polling_interval: float = 0, + stability_delay: float = 0.1, + ) -> None: super().__init__() - self.cmd = document_consumer.Command() + self.consumption_dir = consumption_dir + self.scratch_dir = scratch_dir + self.recursive = recursive + self.subdirs_as_tags = subdirs_as_tags + self.polling_interval = polling_interval + self.stability_delay = stability_delay + self.cmd = Command() self.cmd.stop_flag.clear() + # Non-daemon ensures finally block runs and connections are closed + self.daemon = False + self.exception: Exception | None = None def run(self) -> None: - self.cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False, testing=True) + try: + # Use override_settings to avoid polluting global settings + # which would affect other tests running on the same worker + with override_settings( + SCRATCH_DIR=self.scratch_dir, + CONSUMER_RECURSIVE=self.recursive, + CONSUMER_SUBDIRS_AS_TAGS=self.subdirs_as_tags, + CONSUMER_POLLING_INTERVAL=self.polling_interval, + CONSUMER_STABILITY_DELAY=self.stability_delay, + CONSUMER_IGNORE_PATTERNS=[], + ): + self.cmd.handle( + directory=str(self.consumption_dir), + oneshot=False, + testing=True, + ) + except Exception as e: + self.exception = e + finally: + # Close database connections created in this thread + db.connections.close_all() - def stop(self): - # Consumer checks this every second. + def stop(self) -> None: self.cmd.stop_flag.set() - -def chunked(size, source): - for i in range(0, len(source), size): - yield source[i : i + size] - - -class ConsumerThreadMixin(DocumentConsumeDelayMixin): - """ - Provides a thread which runs the consumer management command at setUp - and stops it at tearDown - """ - - sample_file: Path = ( - Path(__file__).parent / Path("samples") / Path("simple.pdf") - ).resolve() - - def setUp(self) -> None: - super().setUp() - self.t = None - - def t_start(self): - self.t = ConsumerThread() - self.t.start() - # give the consumer some time to do initial work - sleep(1) - - def tearDown(self) -> None: - if self.t: - # set the stop flag - self.t.stop() - # wait for the consumer to exit. - self.t.join() - self.t = None - - super().tearDown() - - def wait_for_task_mock_call(self, expected_call_count=1): - n = 0 - while n < 50: - if self.consume_file_mock.call_count >= expected_call_count: - # give task_mock some time to finish and raise errors - sleep(1) - return - n += 1 - sleep(0.1) - - # A bogus async_task that will simply check the file for - # completeness and raise an exception otherwise. - def bogus_task( - self, - input_doc: ConsumableDocument, - overrides=None, - ): - eq = filecmp.cmp(input_doc.original_file, self.sample_file, shallow=False) - if not eq: - print("Consumed an INVALID file.") # noqa: T201 - raise ConsumerError("Incomplete File READ FAILED") - else: - print("Consumed a perfectly valid file.") # noqa: T201 - - def slow_write_file(self, target, *, incomplete=False): - with Path(self.sample_file).open("rb") as f: - pdf_bytes = f.read() - - if incomplete: - pdf_bytes = pdf_bytes[: len(pdf_bytes) - 100] - - with Path(target).open("wb") as f: - # this will take 2 seconds, since the file is about 20k. - print("Start writing file.") # noqa: T201 - for b in chunked(1000, pdf_bytes): - f.write(b) - sleep(0.1) - print("file completed.") # noqa: T201 - - -@override_settings( - CONSUMER_INOTIFY_DELAY=0.01, -) -class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase): - def test_consume_file(self): - self.t_start() - - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, f) - - def test_consume_file_invalid_ext(self): - self.t_start() - - f = Path(self.dirs.consumption_dir) / "my_file.wow" - shutil.copy(self.sample_file, f) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_not_called() - - def test_consume_existing_file(self): - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) - - self.t_start() - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, f) - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_pdf(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.pdf" - - self.slow_write_file(fname) - - self.wait_for_task_mock_call() - - error_logger.assert_not_called() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname) - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_and_move(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.~df" - fname2 = Path(self.dirs.consumption_dir) / "my_file.pdf" - - self.slow_write_file(fname) - shutil.move(fname, fname2) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname2) - - error_logger.assert_not_called() - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_incomplete(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.pdf" - self.slow_write_file(fname, incomplete=True) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname) - - # assert that we have an error logged with this invalid file. - error_logger.assert_called_once() - - @mock.patch("documents.management.commands.document_consumer.logger.warning") - def test_permission_error_on_prechecks(self, warning_logger): - filepath = Path(self.dirs.consumption_dir) / "selinux.txt" - filepath.touch() - - original_stat = Path.stat - - def raising_stat(self, *args, **kwargs): - if self == filepath: - raise PermissionError("Permission denied") - return original_stat(self, *args, **kwargs) - - with mock.patch("pathlib.Path.stat", new=raising_stat): - document_consumer._consume(filepath) - - warning_logger.assert_called_once() - (args, _) = warning_logger.call_args - self.assertIn("Permission denied", args[0]) - self.consume_file_mock.assert_not_called() - - @override_settings(CONSUMPTION_DIR="does_not_exist") - def test_consumption_directory_invalid(self): - self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") - - @override_settings(CONSUMPTION_DIR="") - def test_consumption_directory_unset(self): - self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") - - def test_mac_write(self): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / ".DS_STORE", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "my_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "._my_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "my_second_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "._my_second_file.pdf", - ) - - sleep(5) - - self.wait_for_task_mock_call(expected_call_count=2) - - self.assertEqual(2, self.consume_file_mock.call_count) - - consumed_files = [] - for input_doc, _ in self.get_all_consume_delay_call_args(): - consumed_files.append(input_doc.original_file.name) - - self.assertCountEqual(consumed_files, ["my_file.pdf", "my_second_file.pdf"]) - - def test_is_ignored(self): - test_paths = [ - { - "path": str(Path(self.dirs.consumption_dir) / "foo.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / "foo" / "bar.pdf", - ), - "ignore": False, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".DS_STORE"), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".DS_Store"), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf", - ), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".stfolder.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf", - ), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stversions.pdf", - ), - "ignore": False, - }, - { - "path": str(Path(self.dirs.consumption_dir) / "._foo.pdf"), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / "my_foo.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf", - ), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) - / "@eaDir" - / "SYNO@.fileindexdb" - / "_1jk.fnm", - ), - "ignore": True, - }, - ] - for test_setup in test_paths: - filepath = test_setup["path"] - expected_ignored_result = test_setup["ignore"] - self.assertEqual( - expected_ignored_result, - document_consumer._is_ignored(filepath), - f'_is_ignored("{filepath}") != {expected_ignored_result}', + def stop_and_wait(self, timeout: float = 5.0) -> None: + """Stop the thread and wait for it to finish, with cleanup.""" + self.stop() + self.join(timeout=timeout) + if self.is_alive(): + # Thread didn't stop in time - this is a test failure + raise RuntimeError( + f"Consumer thread did not stop within {timeout}s timeout", ) - @mock.patch("documents.management.commands.document_consumer.Path.open") - def test_consume_file_busy(self, open_mock): - # Calling this mock always raises this - open_mock.side_effect = OSError - self.t_start() +@pytest.fixture +def start_consumer( + consumption_dir: Path, + scratch_dir: Path, + mock_supported_extensions: MagicMock, +) -> Generator[Callable[..., ConsumerThread], None, None]: + """Start a consumer thread and ensure cleanup.""" + threads: list[ConsumerThread] = [] - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) + def _start(**kwargs) -> ConsumerThread: + thread = ConsumerThread(consumption_dir, scratch_dir, **kwargs) + threads.append(thread) + thread.start() + sleep(0.5) # Give thread time to start + return thread - self.wait_for_task_mock_call() + try: + yield _start + finally: + # Cleanup all threads that were started + for thread in threads: + thread.stop_and_wait() - self.consume_file_mock.assert_not_called() + failed_threads = [] + for thread in threads: + thread.join(timeout=5.0) + if thread.is_alive(): + failed_threads.append(thread) + + # Clean up any Tags created by threads (they bypass test transaction isolation) + Tag.objects.all().delete() + + db.connections.close_all() + + if failed_threads: + pytest.fail( + f"{len(failed_threads)} consumer thread(s) did not stop within timeout", + ) -@override_settings( - CONSUMER_POLLING=1, - # please leave the delay here and down below - # see https://github.com/paperless-ngx/paperless-ngx/pull/66 - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, -) -class TestConsumerPolling(TestConsumer): - # just do all the tests with polling - pass +@pytest.mark.django_db +class TestCommandWatch: + """Integration tests for the watch loop.""" + + def test_detects_new_file( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode detects and consumes new files.""" + thread = start_consumer() + + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.5) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_detects_moved_file( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode detects moved/renamed files.""" + temp_location = scratch_dir / "temp.pdf" + shutil.copy(sample_pdf, temp_location) + + thread = start_consumer() + + target = consumption_dir / "document.pdf" + shutil.move(temp_location, target) + sleep(0.5) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_handles_slow_write( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode waits for slow writes to complete.""" + pdf_bytes = sample_pdf.read_bytes() + + thread = start_consumer(stability_delay=0.2) + + target = consumption_dir / "document.pdf" + with target.open("wb") as f: + for i in range(0, len(pdf_bytes), 100): + f.write(pdf_bytes[i : i + 100]) + f.flush() + sleep(0.05) + + sleep(0.5) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_ignores_macos_files( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode ignores macOS system files.""" + thread = start_consumer() + + (consumption_dir / ".DS_Store").write_bytes(b"test") + (consumption_dir / "._document.pdf").write_bytes(b"test") + shutil.copy(sample_pdf, consumption_dir / "valid.pdf") + + sleep(0.5) + + if thread.exception: + raise thread.exception + + assert mock_consume_file_delay.delay.call_count == 1 + call_args = mock_consume_file_delay.delay.call_args[0][0] + assert call_args.original_file.name == "valid.pdf" + + @pytest.mark.django_db + @pytest.mark.usefixtures("mock_supported_extensions") + def test_stop_flag_stops_consumer( + self, + consumption_dir: Path, + scratch_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test stop flag properly stops the consumer.""" + thread = ConsumerThread(consumption_dir, scratch_dir) + try: + thread.start() + sleep(0.3) + assert thread.is_alive() + finally: + thread.stop_and_wait(timeout=5.0) + # Clean up any Tags created by the thread + Tag.objects.all().delete() + + assert not thread.is_alive() -@override_settings(CONSUMER_INOTIFY_DELAY=0.01, CONSUMER_RECURSIVE=True) -class TestConsumerRecursive(TestConsumer): - # just do all the tests with recursive - pass +class TestCommandWatchPolling: + """Tests for polling mode.""" + + @pytest.mark.django_db + @pytest.mark.flaky(reruns=2) + def test_polling_mode_works( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """ + Test polling mode detects files. + Note: At times, there appears to be a timing issue, where delay has not yet been called, hence this is marked as flaky. + """ + # Use shorter polling interval for faster test + thread = start_consumer(polling_interval=0.5, stability_delay=0.1) + + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + # Wait for: poll interval + stability delay + another poll + margin + # CI can be slow, so use generous timeout + sleep(3.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() -@override_settings( - CONSUMER_RECURSIVE=True, - CONSUMER_POLLING=1, - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, -) -class TestConsumerRecursivePolling(TestConsumer): - # just do all the tests with polling and recursive - pass +@pytest.mark.django_db +class TestCommandWatchRecursive: + """Tests for recursive watching.""" + + def test_recursive_detects_nested_files( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test recursive mode detects files in subdirectories.""" + subdir = consumption_dir / "level1" / "level2" + subdir.mkdir(parents=True) + + thread = start_consumer(recursive=True) + + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.5) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_subdirs_as_tags( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + mocker: MockerFixture, + ) -> None: + """Test subdirs_as_tags creates tags from directory names.""" + # Mock _tags_from_path to avoid database operations in the consumer thread + mock_tags = mocker.patch( + "documents.management.commands.document_consumer._tags_from_path", + return_value=[1, 2], + ) + + subdir = consumption_dir / "Invoices" / "2024" + subdir.mkdir(parents=True) + + thread = start_consumer(recursive=True, subdirs_as_tags=True) + + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.5) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + mock_tags.assert_called() + call_args = mock_consume_file_delay.delay.call_args + overrides = call_args[0][1] + assert overrides.tag_ids is not None + assert len(overrides.tag_ids) == 2 -class TestConsumerTags(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase): - @override_settings(CONSUMER_RECURSIVE=True, CONSUMER_SUBDIRS_AS_TAGS=True) - def test_consume_file_with_path_tags(self): - tag_names = ("existingTag", "Space Tag") - # Create a Tag prior to consuming a file using it in path - tag_ids = [ - Tag.objects.create(name="existingtag").pk, - ] +@pytest.mark.django_db +class TestCommandWatchEdgeCases: + """Tests for edge cases and error handling.""" - self.t_start() + def test_handles_deleted_before_stable( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test handles files deleted before becoming stable.""" + thread = start_consumer(stability_delay=0.3) - path = Path(self.dirs.consumption_dir) / "/".join(tag_names) - path.mkdir(parents=True, exist_ok=True) - f = path / "my_file.pdf" - # Wait at least inotify read_delay for recursive watchers - # to be created for the new directories - sleep(1) - shutil.copy(self.sample_file, f) + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.1) + target.unlink() - self.wait_for_task_mock_call() + sleep(0.5) - self.consume_file_mock.assert_called_once() + if thread.exception: + raise thread.exception - # Add the pk of the Tag created by _consume() - tag_ids.append(Tag.objects.get(name=tag_names[1]).pk) + mock_consume_file_delay.delay.assert_not_called() - input_doc, overrides = self.get_last_consume_delay_call_args() + @pytest.mark.usefixtures("mock_supported_extensions") + def test_handles_task_exception( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mocker: MockerFixture, + ) -> None: + """Test handles exceptions from consume task gracefully.""" + mock_task = mocker.patch( + "documents.management.commands.document_consumer.consume_file", + ) + mock_task.delay.side_effect = Exception("Task error") - self.assertEqual(input_doc.original_file, f) + thread = ConsumerThread(consumption_dir, scratch_dir) + try: + thread.start() + sleep(0.3) - # assertCountEqual has a bad name, but test that the first - # sequence contains the same elements as second, regardless of - # their order. - self.assertCountEqual(overrides.tag_ids, tag_ids) + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.5) - @override_settings( - CONSUMER_POLLING=1, - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, - ) - def test_consume_file_with_path_tags_polling(self): - self.test_consume_file_with_path_tags() + # Consumer should still be running despite the exception + assert thread.is_alive() + finally: + thread.stop_and_wait(timeout=5.0) + # Clean up any Tags created by the thread + Tag.objects.all().delete() diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 9b94ebb7b..024c7f076 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -1044,29 +1044,30 @@ IGNORABLE_FILES: Final[list[str]] = [ "Thumbs.db", ] -CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0)) +CONSUMER_POLLING_INTERVAL = float(os.getenv("PAPERLESS_CONSUMER_POLLING_INTERVAL", 0)) -CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5)) - -CONSUMER_POLLING_RETRY_COUNT = int( - os.getenv("PAPERLESS_CONSUMER_POLLING_RETRY_COUNT", 5), -) - -CONSUMER_INOTIFY_DELAY: Final[float] = __get_float( - "PAPERLESS_CONSUMER_INOTIFY_DELAY", - 0.5, -) +CONSUMER_STABILITY_DELAY = float(os.getenv("PAPERLESS_CONSUMER_STABILITY_DELAY", 5)) CONSUMER_DELETE_DUPLICATES = __get_boolean("PAPERLESS_CONSUMER_DELETE_DUPLICATES") CONSUMER_RECURSIVE = __get_boolean("PAPERLESS_CONSUMER_RECURSIVE") -# Ignore glob patterns, relative to PAPERLESS_CONSUMPTION_DIR +# Ignore regex patterns, matched against filename only CONSUMER_IGNORE_PATTERNS = list( json.loads( os.getenv( "PAPERLESS_CONSUMER_IGNORE_PATTERNS", - json.dumps(IGNORABLE_FILES), + json.dumps([]), + ), + ), +) + +# Directories to always ignore. These are matched by directory name, not full path +CONSUMER_IGNORE_DIRS = list( + json.loads( + os.getenv( + "PAPERLESS_CONSUMER_IGNORE_DIRS", + json.dumps([]), ), ), ) diff --git a/uv.lock b/uv.lock index dc28c9d2b..43ffbf002 100644 --- a/uv.lock +++ b/uv.lock @@ -1842,26 +1842,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "inotify-simple" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/5c/bfe40e15d684bc30b0073aa97c39be410a5fbef3d33cad6f0bf2012571e0/inotify_simple-2.0.1.tar.gz", hash = "sha256:f010bbbd8283bd71a9f4eb2de94765804ede24bd47320b0e6ef4136e541cdc2c", size = 7101, upload-time = "2025-08-25T06:28:20.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/86/8be1ac7e90f80b413e81f1e235148e8db771218886a2353392f02da01be3/inotify_simple-2.0.1-py3-none-any.whl", hash = "sha256:e5da495f2064889f8e68b67f9358b0d102e03b783c2d42e5b8e132ab859a5d8a", size = 7449, upload-time = "2025-08-25T06:28:19.919Z" }, -] - -[[package]] -name = "inotifyrecursive" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "inotify-simple", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/3a/9ed038cb750a3ba8090869cf3ad50f5628077a936d911aee14ca83e40f6a/inotifyrecursive-0.3.5.tar.gz", hash = "sha256:a2c450b317693e4538416f90eb1d7858506dafe6b8b885037bd2dd9ae2dafa1e", size = 4576, upload-time = "2020-11-20T12:38:48.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" }, -] [[package]] name = "isodate" @@ -2969,7 +2949,6 @@ dependencies = [ { name = "gotenberg-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "httpx-oauth", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "imap-tools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "inotifyrecursive", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "langdetect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2999,7 +2978,7 @@ dependencies = [ { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { 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 = "watchdog", 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 = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zxing-cpp", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(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 (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, @@ -3119,7 +3098,6 @@ requires-dist = [ { name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.5.1" }, { name = "httpx-oauth", specifier = "~=0.16" }, { name = "imap-tools", specifier = "~=1.11.0" }, - { name = "inotifyrecursive", specifier = "~=0.3" }, { name = "jinja2", specifier = "~=3.1.5" }, { name = "langdetect", specifier = "~=1.0.9" }, { name = "llama-index-core", specifier = ">=0.14.12" }, @@ -3154,7 +3132,7 @@ requires-dist = [ { name = "tika-client", specifier = "~=0.10.0" }, { name = "torch", specifier = "~=2.9.1", index = "https://download.pytorch.org/whl/cpu" }, { name = "tqdm", specifier = "~=4.67.1" }, - { name = "watchdog", specifier = "~=6.0" }, + { name = "watchfiles", specifier = ">=1.1.1" }, { name = "whitenoise", specifier = "~=6.9" }, { name = "whoosh-reloaded", specifier = ">=2.7.5" }, { name = "zxing-cpp", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64') or (python_full_version != '3.12.*' and platform_machine == 'x86_64') or (platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", specifier = "~=2.3.0" }, @@ -5600,6 +5578,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + [[package]] name = "wcwidth" version = "0.2.14" From c06e1e7cba00fad844c6b0bd827e16f4a91f625f Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:53:54 -0800 Subject: [PATCH 51/57] Resolves gpg-agent hanging around and using inotify handles too (#11848) --- src/paperless_mail/tests/test_preprocessor.py | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/src/paperless_mail/tests/test_preprocessor.py b/src/paperless_mail/tests/test_preprocessor.py index 90df77ba8..2ad9410f9 100644 --- a/src/paperless_mail/tests/test_preprocessor.py +++ b/src/paperless_mail/tests/test_preprocessor.py @@ -1,5 +1,7 @@ import email import email.contentmanager +import shutil +import subprocess import tempfile from email.message import Message from email.mime.application import MIMEApplication @@ -34,6 +36,30 @@ class MessageEncryptor: ) self.gpg.gen_key(input_data) + def cleanup(self) -> None: + """ + Kill the gpg-agent process and clean up the temporary GPG home directory. + + This uses gpgconf to properly terminate the agent, which is the officially + recommended cleanup method from the GnuPG project. python-gnupg does not + provide built-in cleanup methods as it's only a wrapper around the gpg CLI. + """ + # Kill the gpg-agent using the official GnuPG cleanup tool + try: + subprocess.run( + ["gpgconf", "--kill", "gpg-agent"], + env={"GNUPGHOME": self.gpg_home}, + check=False, + capture_output=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + # gpgconf not found or hung - agent will timeout eventually + pass + + # Clean up the temporary directory + shutil.rmtree(self.gpg_home, ignore_errors=True) + @staticmethod def get_email_body_without_headers(email_message: Message) -> bytes: """ @@ -85,8 +111,20 @@ class MessageEncryptor: class TestMailMessageGpgDecryptor(TestMail): + @classmethod + def setUpClass(cls): + """Create GPG encryptor once for all tests in this class.""" + super().setUpClass() + cls.messageEncryptor = MessageEncryptor() + + @classmethod + def tearDownClass(cls): + """Clean up GPG resources after all tests complete.""" + if hasattr(cls, "messageEncryptor"): + cls.messageEncryptor.cleanup() + super().tearDownClass() + def setUp(self): - self.messageEncryptor = MessageEncryptor() with override_settings( EMAIL_GNUPG_HOME=self.messageEncryptor.gpg_home, EMAIL_ENABLE_GPG_DECRYPTOR=True, @@ -138,13 +176,28 @@ class TestMailMessageGpgDecryptor(TestMail): def test_decrypt_fails(self): encrypted_message, _ = self.create_encrypted_unencrypted_message_pair() + # This test creates its own empty GPG home to test decryption failure empty_gpg_home = tempfile.mkdtemp() - with override_settings( - EMAIL_ENABLE_GPG_DECRYPTOR=True, - EMAIL_GNUPG_HOME=empty_gpg_home, - ): - message_decryptor = MailMessageDecryptor() - self.assertRaises(Exception, message_decryptor.run, encrypted_message) + try: + with override_settings( + EMAIL_ENABLE_GPG_DECRYPTOR=True, + EMAIL_GNUPG_HOME=empty_gpg_home, + ): + message_decryptor = MailMessageDecryptor() + self.assertRaises(Exception, message_decryptor.run, encrypted_message) + finally: + # Clean up the temporary GPG home used only by this test + try: + subprocess.run( + ["gpgconf", "--kill", "gpg-agent"], + env={"GNUPGHOME": empty_gpg_home}, + check=False, + capture_output=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + shutil.rmtree(empty_gpg_home, ignore_errors=True) def test_decrypt_encrypted_mail(self): """ From 32b236cfa2df7b4fb35dbe2e80f9d4c61172bbfa Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:05:19 -0800 Subject: [PATCH 52/57] Enhancement: support doc_id placeholder in workflow templates (#11847) --- docs/usage.md | 1 + src/documents/templating/workflows.py | 3 +++ src/documents/tests/test_workflows.py | 7 +++++-- src/documents/workflows/actions.py | 6 ++++++ src/documents/workflows/mutations.py | 3 +++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index cac07f4a5..7da83a3e1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -597,6 +597,7 @@ The following placeholders are only available for "added" or "updated" triggers - `{{created_day}}`: created day - `{{created_time}}`: created time in HH:MM format - `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set. +- `{{doc_id}}`: Document ID ##### Examples diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index 67f3ac930..66fd97e01 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -40,6 +40,7 @@ def parse_w_workflow_placeholders( created: date | None = None, doc_title: str | None = None, doc_url: str | None = None, + doc_id: int | None = None, ) -> str: """ Available title placeholders for Workflows depend on what has already been assigned, @@ -79,6 +80,8 @@ def parse_w_workflow_placeholders( formatting.update({"doc_title": doc_title}) if doc_url is not None: formatting.update({"doc_url": doc_url}) + if doc_id is not None: + formatting.update({"doc_id": str(doc_id)}) logger.debug(f"Parsing Workflow Jinja template: {text}") try: diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index deb40a165..d2f843a68 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -3298,7 +3298,7 @@ class TestWorkflows( ) webhook_action = WorkflowActionWebhook.objects.create( use_params=False, - body="Test message: {{doc_url}}", + body="Test message: {{doc_url}} with id {{doc_id}}", url="http://paperless-ngx.com", include_document=False, ) @@ -3328,7 +3328,10 @@ class TestWorkflows( mock_post.assert_called_once_with( url="http://paperless-ngx.com", - data=f"Test message: http://localhost:8000/paperless/documents/{doc.id}/", + data=( + f"Test message: http://localhost:8000/paperless/documents/{doc.id}/" + f" with id {doc.id}" + ), headers={}, files=None, as_json=False, diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py index 040cbc127..a61b9930e 100644 --- a/src/documents/workflows/actions.py +++ b/src/documents/workflows/actions.py @@ -44,6 +44,7 @@ def build_workflow_action_context( "current_filename": document.filename or "", "added": timezone.localtime(document.added), "created": document.created, + "id": document.pk, } correspondent_obj = ( @@ -75,6 +76,7 @@ def build_workflow_action_context( "current_filename": filename, "added": timezone.localtime(timezone.now()), "created": overrides.created if overrides else None, + "id": "", } @@ -109,6 +111,7 @@ def execute_email_action( context["created"], context["title"], context["doc_url"], + context["id"], ) if action.email.subject else "" @@ -125,6 +128,7 @@ def execute_email_action( context["created"], context["title"], context["doc_url"], + context["id"], ) if action.email.body else "" @@ -203,6 +207,7 @@ def execute_webhook_action( context["created"], context["title"], context["doc_url"], + context["id"], ) except Exception as e: logger.error( @@ -221,6 +226,7 @@ def execute_webhook_action( context["created"], context["title"], context["doc_url"], + context["id"], ) headers = {} if action.webhook.headers: diff --git a/src/documents/workflows/mutations.py b/src/documents/workflows/mutations.py index ef85dba0f..b93a26781 100644 --- a/src/documents/workflows/mutations.py +++ b/src/documents/workflows/mutations.py @@ -55,6 +55,9 @@ def apply_assignment_to_document( document.original_filename or "", document.filename or "", document.created, + "", # dont pass the title to avoid recursion + "", # no urls in titles + document.pk, ) except Exception: # pragma: no cover logger.exception( From 213bd7e244955272bd44cd0bf10fee04b07a9af3 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:19:20 -0800 Subject: [PATCH 53/57] Chore: Manually upgrades allauth to resolve a security issue with it (#11853) --- pyproject.toml | 2 +- uv.lock | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 090854388..097e2c19b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # WARNING: django does not use semver. # Only patch versions are guaranteed to not introduce breaking changes. "django~=5.2.5", - "django-allauth[mfa,socialaccount]~=65.12.1", + "django-allauth[mfa,socialaccount]~=65.13.1", "django-auditlog~=3.4.1", "django-cachalot~=2.8.0", "django-celery-results~=2.6.0", diff --git a/uv.lock b/uv.lock index 43ffbf002..da7c721f5 100644 --- a/uv.lock +++ b/uv.lock @@ -899,13 +899,16 @@ wheels = [ [[package]] name = "django-allauth" -version = "65.12.1" +version = "65.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", 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/52/94/75d7f8c59e061d1b66a6d917b287817fe02d2671c9e6376a4ddfb3954989/django_allauth-65.12.1.tar.gz", hash = "sha256:662666ff2d5c71766f66b1629ac7345c30796813221184e13e11ed7460940c6a", size = 1967971, upload-time = "2025-10-16T16:39:58.342Z" } +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 = [ + { 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] mfa = [ @@ -1842,7 +1845,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] - [[package]] name = "isodate" version = "0.7.2" @@ -3074,7 +3076,7 @@ requires-dist = [ { name = "concurrent-log-handler", specifier = "~=0.9.25" }, { name = "dateparser", specifier = "~=1.2" }, { name = "django", specifier = "~=5.2.5" }, - { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.12.1" }, + { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.13.1" }, { name = "django-auditlog", specifier = "~=3.4.1" }, { name = "django-cachalot", specifier = "~=2.8.0" }, { name = "django-celery-results", specifier = "~=2.6.0" }, From 2d9717a330eb24b4db7fa39bfa8d9b0646dff037 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:45:50 -0800 Subject: [PATCH 54/57] Fix: ensure css color-scheme for dark mode (#11855) --- src-ui/src/theme.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index eacc3b4e7..6ff5f4a09 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -73,6 +73,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml, Path: - if self.filename: - fname: str = str(self.filename) - else: - fname = f"{self.pk:07}.{self.file_type}" - if self.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" - - return Path(settings.ORIGINALS_DIR) / fname - - -def add_mime_types(apps, schema_editor): - Document = apps.get_model("documents", "Document") - documents = Document.objects.all() - - for d in documents: - with Path(source_path(d)).open("rb") as f: - if d.storage_type == STORAGE_TYPE_GPG: - data = GnuPG.decrypted(f) - else: - data = f.read(1024) - - d.mime_type = magic.from_buffer(data, mime=True) - d.save() - - -def add_file_extensions(apps, schema_editor): - Document = apps.get_model("documents", "Document") - documents = Document.objects.all() - - for d in documents: - d.file_type = Path(d.filename).suffix.lstrip(".") - d.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1002_auto_20201111_1105"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="mime_type", - field=models.CharField(default="-", editable=False, max_length=256), - preserve_default=False, - ), - migrations.RunPython(add_mime_types, migrations.RunPython.noop), - # This operation is here so that we can revert the entire migration: - # By allowing this field to be blank and null, we can revert the - # remove operation further down and the database won't complain about - # NOT NULL violations. - migrations.AlterField( - model_name="document", - name="file_type", - field=models.CharField( - choices=[ - ("pdf", "PDF"), - ("png", "PNG"), - ("jpg", "JPG"), - ("gif", "GIF"), - ("tiff", "TIFF"), - ("txt", "TXT"), - ("csv", "CSV"), - ("md", "MD"), - ], - editable=False, - max_length=4, - null=True, - blank=True, - ), - ), - migrations.RunPython(migrations.RunPython.noop, add_file_extensions), - migrations.RemoveField( - model_name="document", - name="file_type", - ), - ] diff --git a/src/documents/migrations/1004_sanity_check_schedule.py b/src/documents/migrations/1004_sanity_check_schedule.py deleted file mode 100644 index 018cf2492..000000000 --- a/src/documents/migrations/1004_sanity_check_schedule.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-25 14:53 - -from django.db import migrations -from django.db.migrations import RunPython - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1003_mime_types"), - ] - - operations = [RunPython(migrations.RunPython.noop, migrations.RunPython.noop)] diff --git a/src/documents/migrations/1005_checksums.py b/src/documents/migrations/1005_checksums.py deleted file mode 100644 index 4637e06ce..000000000 --- a/src/documents/migrations/1005_checksums.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-29 00:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1004_sanity_check_schedule"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - ), - ), - ] diff --git a/src/documents/migrations/1006_auto_20201208_2209.py b/src/documents/migrations/1006_auto_20201208_2209.py deleted file mode 100644 index 425f0a768..000000000 --- a/src/documents/migrations/1006_auto_20201208_2209.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-08 22:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1005_checksums"), - ] - - operations = [ - migrations.RemoveField( - model_name="correspondent", - name="slug", - ), - migrations.RemoveField( - model_name="documenttype", - name="slug", - ), - migrations.RemoveField( - model_name="tag", - name="slug", - ), - ] diff --git a/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py b/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py deleted file mode 100644 index aa8cb6deb..000000000 --- a/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py +++ /dev/null @@ -1,485 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:01 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1006_auto_20201208_2209"), - ("documents", "1007_savedview_savedviewfilterrule"), - ("documents", "1008_auto_20201216_1736"), - ("documents", "1009_auto_20201216_2005"), - ("documents", "1010_auto_20210101_2159"), - ("documents", "1011_auto_20210101_2340"), - ] - - dependencies = [ - ("documents", "1005_checksums"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name="correspondent", - name="slug", - ), - migrations.RemoveField( - model_name="documenttype", - name="slug", - ), - migrations.RemoveField( - model_name="tag", - name="slug", - ), - migrations.CreateModel( - name="SavedView", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, verbose_name="name")), - ( - "show_on_dashboard", - models.BooleanField(verbose_name="show on dashboard"), - ), - ( - "show_in_sidebar", - models.BooleanField(verbose_name="show in sidebar"), - ), - ( - "sort_field", - models.CharField(max_length=128, verbose_name="sort field"), - ), - ( - "sort_reverse", - models.BooleanField(default=False, verbose_name="sort reverse"), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "ordering": ("name",), - "verbose_name": "saved view", - "verbose_name_plural": "saved views", - }, - ), - migrations.AlterModelOptions( - name="correspondent", - options={ - "ordering": ("name",), - "verbose_name": "correspondent", - "verbose_name_plural": "correspondents", - }, - ), - migrations.AlterModelOptions( - name="document", - options={ - "ordering": ("-created",), - "verbose_name": "document", - "verbose_name_plural": "documents", - }, - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="log", - options={ - "ordering": ("-created",), - "verbose_name": "log", - "verbose_name_plural": "logs", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={"verbose_name": "tag", "verbose_name_plural": "tags"}, - ), - migrations.AlterField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="correspondent", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="document", - name="added", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="added", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - verbose_name="archive checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.IntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - verbose_name="archive serial number", - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - verbose_name="checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.correspondent", - verbose_name="correspondent", - ), - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - migrations.AlterField( - model_name="document", - name="document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.documenttype", - verbose_name="document type", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - verbose_name="filename", - ), - ), - migrations.AlterField( - model_name="document", - name="mime_type", - field=models.CharField( - editable=False, - max_length=256, - verbose_name="mime type", - ), - ), - migrations.AlterField( - model_name="document", - name="modified", - field=models.DateTimeField( - auto_now=True, - db_index=True, - verbose_name="modified", - ), - ), - migrations.AlterField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="unencrypted", - editable=False, - max_length=11, - verbose_name="storage type", - ), - ), - migrations.AlterField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.tag", - verbose_name="tags", - ), - ), - migrations.AlterField( - model_name="document", - name="title", - field=models.CharField( - blank=True, - db_index=True, - max_length=128, - verbose_name="title", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="documenttype", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="log", - name="created", - field=models.DateTimeField(auto_now_add=True, verbose_name="created"), - ), - migrations.AlterField( - model_name="log", - name="group", - field=models.UUIDField(blank=True, null=True, verbose_name="group"), - ), - migrations.AlterField( - model_name="log", - name="level", - field=models.PositiveIntegerField( - choices=[ - (10, "debug"), - (20, "information"), - (30, "warning"), - (40, "error"), - (50, "critical"), - ], - default=20, - verbose_name="level", - ), - ), - migrations.AlterField( - model_name="log", - name="message", - field=models.TextField(verbose_name="message"), - ), - migrations.CreateModel( - name="SavedViewFilterRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "rule_type", - models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - ], - verbose_name="rule type", - ), - ), - ( - "value", - models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="value", - ), - ), - ( - "saved_view", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - verbose_name="saved view", - ), - ), - ], - options={ - "verbose_name": "filter rule", - "verbose_name_plural": "filter rules", - }, - ), - migrations.AlterField( - model_name="tag", - name="colour", - field=models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - verbose_name="color", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_inbox_tag", - field=models.BooleanField( - default=False, - help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", - verbose_name="is inbox tag", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="tag", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ] diff --git a/src/documents/migrations/1007_savedview_savedviewfilterrule.py b/src/documents/migrations/1007_savedview_savedviewfilterrule.py deleted file mode 100644 index 64564c6af..000000000 --- a/src/documents/migrations/1007_savedview_savedviewfilterrule.py +++ /dev/null @@ -1,90 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-12 14:41 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1006_auto_20201208_2209"), - ] - - operations = [ - migrations.CreateModel( - name="SavedView", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128)), - ("show_on_dashboard", models.BooleanField()), - ("show_in_sidebar", models.BooleanField()), - ("sort_field", models.CharField(max_length=128)), - ("sort_reverse", models.BooleanField(default=False)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name="SavedViewFilterRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "rule_type", - models.PositiveIntegerField( - choices=[ - (0, "Title contains"), - (1, "Content contains"), - (2, "ASN is"), - (3, "Correspondent is"), - (4, "Document type is"), - (5, "Is in inbox"), - (6, "Has tag"), - (7, "Has any tag"), - (8, "Created before"), - (9, "Created after"), - (10, "Created year is"), - (11, "Created month is"), - (12, "Created day is"), - (13, "Added before"), - (14, "Added after"), - (15, "Modified before"), - (16, "Modified after"), - (17, "Does not have tag"), - ], - ), - ), - ("value", models.CharField(max_length=128)), - ( - "saved_view", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1008_auto_20201216_1736.py b/src/documents/migrations/1008_auto_20201216_1736.py deleted file mode 100644 index 76f0343b1..000000000 --- a/src/documents/migrations/1008_auto_20201216_1736.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-16 17:36 - -import django.db.models.functions.text -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1007_savedview_savedviewfilterrule"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="document", - options={"ordering": ("-created",)}, - ), - migrations.AlterModelOptions( - name="documenttype", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="savedview", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="tag", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - ] diff --git a/src/documents/migrations/1009_auto_20201216_2005.py b/src/documents/migrations/1009_auto_20201216_2005.py deleted file mode 100644 index 37bae8881..000000000 --- a/src/documents/migrations/1009_auto_20201216_2005.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-16 20:05 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1008_auto_20201216_1736"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="documenttype", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="savedview", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="tag", - options={"ordering": ("name",)}, - ), - ] diff --git a/src/documents/migrations/1010_auto_20210101_2159.py b/src/documents/migrations/1010_auto_20210101_2159.py deleted file mode 100644 index 0c6a42d30..000000000 --- a/src/documents/migrations/1010_auto_20210101_2159.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 21:59 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1009_auto_20201216_2005"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField(blank=True, max_length=128, null=True), - ), - ] diff --git a/src/documents/migrations/1011_auto_20210101_2340.py b/src/documents/migrations/1011_auto_20210101_2340.py deleted file mode 100644 index dea107715..000000000 --- a/src/documents/migrations/1011_auto_20210101_2340.py +++ /dev/null @@ -1,454 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 23:40 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1010_auto_20210101_2159"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={ - "ordering": ("name",), - "verbose_name": "correspondent", - "verbose_name_plural": "correspondents", - }, - ), - migrations.AlterModelOptions( - name="document", - options={ - "ordering": ("-created",), - "verbose_name": "document", - "verbose_name_plural": "documents", - }, - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="log", - options={ - "ordering": ("-created",), - "verbose_name": "log", - "verbose_name_plural": "logs", - }, - ), - migrations.AlterModelOptions( - name="savedview", - options={ - "ordering": ("name",), - "verbose_name": "saved view", - "verbose_name_plural": "saved views", - }, - ), - migrations.AlterModelOptions( - name="savedviewfilterrule", - options={ - "verbose_name": "filter rule", - "verbose_name_plural": "filter rules", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={"verbose_name": "tag", "verbose_name_plural": "tags"}, - ), - migrations.AlterField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="correspondent", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="document", - name="added", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="added", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - verbose_name="archive checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.IntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - verbose_name="archive serial number", - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - verbose_name="checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.correspondent", - verbose_name="correspondent", - ), - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - migrations.AlterField( - model_name="document", - name="document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.documenttype", - verbose_name="document type", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - verbose_name="filename", - ), - ), - migrations.AlterField( - model_name="document", - name="mime_type", - field=models.CharField( - editable=False, - max_length=256, - verbose_name="mime type", - ), - ), - migrations.AlterField( - model_name="document", - name="modified", - field=models.DateTimeField( - auto_now=True, - db_index=True, - verbose_name="modified", - ), - ), - migrations.AlterField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="unencrypted", - editable=False, - max_length=11, - verbose_name="storage type", - ), - ), - migrations.AlterField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.Tag", - verbose_name="tags", - ), - ), - migrations.AlterField( - model_name="document", - name="title", - field=models.CharField( - blank=True, - db_index=True, - max_length=128, - verbose_name="title", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="documenttype", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="log", - name="created", - field=models.DateTimeField(auto_now_add=True, verbose_name="created"), - ), - migrations.AlterField( - model_name="log", - name="group", - field=models.UUIDField(blank=True, null=True, verbose_name="group"), - ), - migrations.AlterField( - model_name="log", - name="level", - field=models.PositiveIntegerField( - choices=[ - (10, "debug"), - (20, "information"), - (30, "warning"), - (40, "error"), - (50, "critical"), - ], - default=20, - verbose_name="level", - ), - ), - migrations.AlterField( - model_name="log", - name="message", - field=models.TextField(verbose_name="message"), - ), - migrations.AlterField( - model_name="savedview", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="savedview", - name="show_in_sidebar", - field=models.BooleanField(verbose_name="show in sidebar"), - ), - migrations.AlterField( - model_name="savedview", - name="show_on_dashboard", - field=models.BooleanField(verbose_name="show on dashboard"), - ), - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField(max_length=128, verbose_name="sort field"), - ), - migrations.AlterField( - model_name="savedview", - name="sort_reverse", - field=models.BooleanField(default=False, verbose_name="sort reverse"), - ), - migrations.AlterField( - model_name="savedview", - name="user", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="saved_view", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - verbose_name="saved view", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="value", - ), - ), - migrations.AlterField( - model_name="tag", - name="colour", - field=models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - verbose_name="color", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_inbox_tag", - field=models.BooleanField( - default=False, - help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", - verbose_name="is inbox tag", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="tag", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ] diff --git a/src/documents/migrations/1012_fix_archive_files.py b/src/documents/migrations/1012_fix_archive_files.py deleted file mode 100644 index a97fa7a80..000000000 --- a/src/documents/migrations/1012_fix_archive_files.py +++ /dev/null @@ -1,367 +0,0 @@ -# Generated by Django 3.1.6 on 2021-02-07 22:26 -import datetime -import hashlib -import logging -import os -import shutil -from collections import defaultdict -from pathlib import Path -from time import sleep - -import pathvalidate -from django.conf import settings -from django.db import migrations -from django.db import models -from django.template.defaultfilters import slugify - -logger = logging.getLogger("paperless.migrations") - - -############################################################################### -# This is code copied straight paperless before the change. -############################################################################### -class defaultdictNoStr(defaultdict): - def __str__(self): # pragma: no cover - raise ValueError("Don't use {tags} directly.") - - -def many_to_dictionary(field): # pragma: no cover - # Converts ManyToManyField to dictionary by assuming, that field - # entries contain an _ or - which will be used as a delimiter - mydictionary = dict() - - for index, t in enumerate(field.all()): - # Populate tag names by index - mydictionary[index] = slugify(t.name) - - # Find delimiter - delimiter = t.name.find("_") - - if delimiter == -1: - delimiter = t.name.find("-") - - if delimiter == -1: - continue - - key = t.name[:delimiter] - value = t.name[delimiter + 1 :] - - mydictionary[slugify(key)] = slugify(value) - - return mydictionary - - -def archive_name_from_filename(filename: Path) -> Path: - return Path(filename.stem + ".pdf") - - -def archive_path_old(doc) -> Path: - if doc.filename: - fname = archive_name_from_filename(Path(doc.filename)) - else: - fname = Path(f"{doc.pk:07}.pdf") - - return settings.ARCHIVE_DIR / fname - - -STORAGE_TYPE_GPG = "gpg" - - -def archive_path_new(doc) -> Path | None: - if doc.archive_filename is not None: - return settings.ARCHIVE_DIR / doc.archive_filename - else: - return None - - -def source_path(doc) -> Path: - if doc.filename: - fname = doc.filename - else: - fname = f"{doc.pk:07}{doc.file_type}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname = Path(str(fname) + ".gpg") # pragma: no cover - - return settings.ORIGINALS_DIR / fname - - -def generate_unique_filename(doc, *, archive_filename=False): - if archive_filename: - old_filename = doc.archive_filename - root = settings.ARCHIVE_DIR - else: - old_filename = doc.filename - root = settings.ORIGINALS_DIR - - counter = 0 - - while True: - new_filename = generate_filename( - doc, - counter=counter, - archive_filename=archive_filename, - ) - if new_filename == old_filename: - # still the same as before. - return new_filename - - if (root / new_filename).exists(): - counter += 1 - else: - return new_filename - - -def generate_filename(doc, *, counter=0, append_gpg=True, archive_filename=False): - path = "" - - try: - if settings.FILENAME_FORMAT is not None: - tags = defaultdictNoStr(lambda: slugify(None), many_to_dictionary(doc.tags)) - - tag_list = pathvalidate.sanitize_filename( - ",".join(sorted([tag.name for tag in doc.tags.all()])), - replacement_text="-", - ) - - if doc.correspondent: - correspondent = pathvalidate.sanitize_filename( - doc.correspondent.name, - replacement_text="-", - ) - else: - correspondent = "none" - - if doc.document_type: - document_type = pathvalidate.sanitize_filename( - doc.document_type.name, - replacement_text="-", - ) - else: - document_type = "none" - - path = settings.FILENAME_FORMAT.format( - title=pathvalidate.sanitize_filename(doc.title, replacement_text="-"), - correspondent=correspondent, - document_type=document_type, - created=datetime.date.isoformat(doc.created), - created_year=doc.created.year if doc.created else "none", - created_month=f"{doc.created.month:02}" if doc.created else "none", - created_day=f"{doc.created.day:02}" if doc.created else "none", - added=datetime.date.isoformat(doc.added), - added_year=doc.added.year if doc.added else "none", - added_month=f"{doc.added.month:02}" if doc.added else "none", - added_day=f"{doc.added.day:02}" if doc.added else "none", - tags=tags, - tag_list=tag_list, - ).strip() - - path = path.strip(os.sep) - - except (ValueError, KeyError, IndexError): - logger.warning( - f"Invalid PAPERLESS_FILENAME_FORMAT: " - f"{settings.FILENAME_FORMAT}, falling back to default", - ) - - counter_str = f"_{counter:02}" if counter else "" - - filetype_str = ".pdf" if archive_filename else doc.file_type - - if len(path) > 0: - filename = f"{path}{counter_str}{filetype_str}" - else: - filename = f"{doc.pk:07}{counter_str}{filetype_str}" - - # Append .gpg for encrypted files - if append_gpg and doc.storage_type == STORAGE_TYPE_GPG: - filename += ".gpg" - - return filename - - -############################################################################### -# This code performs bidirection archive file transformation. -############################################################################### - - -def parse_wrapper(parser, path, mime_type, file_name): - # this is here so that I can mock this out for testing. - parser.parse(path, mime_type, file_name) - - -def create_archive_version(doc, retry_count=3): - from documents.parsers import DocumentParser - from documents.parsers import ParseError - from documents.parsers import get_parser_class_for_mime_type - - logger.info(f"Regenerating archive document for document ID:{doc.id}") - parser_class = get_parser_class_for_mime_type(doc.mime_type) - for try_num in range(retry_count): - parser: DocumentParser = parser_class(None, None) - try: - parse_wrapper( - parser, - source_path(doc), - doc.mime_type, - Path(doc.filename).name, - ) - doc.content = parser.get_text() - - if parser.get_archive_path() and Path(parser.get_archive_path()).is_file(): - doc.archive_filename = generate_unique_filename( - doc, - archive_filename=True, - ) - with Path(parser.get_archive_path()).open("rb") as f: - doc.archive_checksum = hashlib.md5(f.read()).hexdigest() - archive_path_new(doc).parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(parser.get_archive_path(), archive_path_new(doc)) - else: - doc.archive_checksum = None - logger.error( - f"Parser did not return an archive document for document " - f"ID:{doc.id}. Removing archive document.", - ) - doc.save() - return - except ParseError: - if try_num + 1 == retry_count: - logger.exception( - f"Unable to regenerate archive document for ID:{doc.id}. You " - f"need to invoke the document_archiver management command " - f"manually for that document.", - ) - doc.archive_checksum = None - doc.save() - return - else: - # This is mostly here for the tika parser in docker - # environments. The servers for parsing need to come up first, - # and the docker setup doesn't ensure that tika is running - # before attempting migrations. - logger.error("Parse error, will try again in 5 seconds...") - sleep(5) - finally: - parser.cleanup() - - -def move_old_to_new_locations(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - affected_document_ids = set() - - old_archive_path_to_id = {} - - # check for documents that have incorrect archive versions - for doc in Document.objects.filter(archive_checksum__isnull=False): - old_path = archive_path_old(doc) - - if old_path in old_archive_path_to_id: - affected_document_ids.add(doc.id) - affected_document_ids.add(old_archive_path_to_id[old_path]) - else: - old_archive_path_to_id[old_path] = doc.id - - # check that archive files of all unaffected documents are in place - for doc in Document.objects.filter(archive_checksum__isnull=False): - old_path = archive_path_old(doc) - if doc.id not in affected_document_ids and not old_path.is_file(): - raise ValueError( - f"Archived document ID:{doc.id} does not exist at: {old_path}", - ) - - # check that we can regenerate affected archive versions - for doc_id in affected_document_ids: - from documents.parsers import get_parser_class_for_mime_type - - doc = Document.objects.get(id=doc_id) - parser_class = get_parser_class_for_mime_type(doc.mime_type) - if not parser_class: - raise ValueError( - f"Document ID:{doc.id} has an invalid archived document, " - f"but no parsers are available. Cannot migrate.", - ) - - for doc in Document.objects.filter(archive_checksum__isnull=False): - if doc.id in affected_document_ids: - old_path = archive_path_old(doc) - # remove affected archive versions - if old_path.is_file(): - logger.debug(f"Removing {old_path}") - old_path.unlink() - else: - # Set archive path for unaffected files - doc.archive_filename = archive_name_from_filename(Path(doc.filename)) - Document.objects.filter(id=doc.id).update( - archive_filename=doc.archive_filename, - ) - - # regenerate archive documents - for doc_id in affected_document_ids: - doc = Document.objects.get(id=doc_id) - create_archive_version(doc) - - -def move_new_to_old_locations(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - old_archive_paths = set() - - for doc in Document.objects.filter(archive_checksum__isnull=False): - new_archive_path = archive_path_new(doc) - old_archive_path = archive_path_old(doc) - if old_archive_path in old_archive_paths: - raise ValueError( - f"Cannot migrate: Archive file name {old_archive_path} of " - f"document {doc.filename} would clash with another archive " - f"filename.", - ) - old_archive_paths.add(old_archive_path) - if new_archive_path != old_archive_path and old_archive_path.is_file(): - raise ValueError( - f"Cannot migrate: Cannot move {new_archive_path} to " - f"{old_archive_path}: file already exists.", - ) - - for doc in Document.objects.filter(archive_checksum__isnull=False): - new_archive_path = archive_path_new(doc) - old_archive_path = archive_path_old(doc) - if new_archive_path != old_archive_path: - logger.debug(f"Moving {new_archive_path} to {old_archive_path}") - shutil.move(new_archive_path, old_archive_path) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1011_auto_20210101_2340"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="archive_filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current archive filename in storage", - max_length=1024, - null=True, - unique=True, - verbose_name="archive filename", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - unique=True, - verbose_name="filename", - ), - ), - migrations.RunPython(move_old_to_new_locations, move_new_to_old_locations), - ] diff --git a/src/documents/migrations/1013_migrate_tag_colour.py b/src/documents/migrations/1013_migrate_tag_colour.py deleted file mode 100644 index 6cae10898..000000000 --- a/src/documents/migrations/1013_migrate_tag_colour.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-02 21:43 - -from django.db import migrations -from django.db import models - -COLOURS_OLD = { - 1: "#a6cee3", - 2: "#1f78b4", - 3: "#b2df8a", - 4: "#33a02c", - 5: "#fb9a99", - 6: "#e31a1c", - 7: "#fdbf6f", - 8: "#ff7f00", - 9: "#cab2d6", - 10: "#6a3d9a", - 11: "#b15928", - 12: "#000000", - 13: "#cccccc", -} - - -def forward(apps, schema_editor): - Tag = apps.get_model("documents", "Tag") - - for tag in Tag.objects.all(): - colour_old_id = tag.colour_old - rgb = COLOURS_OLD[colour_old_id] - tag.color = rgb - tag.save() - - -def reverse(apps, schema_editor): - Tag = apps.get_model("documents", "Tag") - - def _get_colour_id(rdb): - for idx, rdbx in COLOURS_OLD.items(): - if rdbx == rdb: - return idx - # Return colour 1 if we can't match anything - return 1 - - for tag in Tag.objects.all(): - colour_id = _get_colour_id(tag.color) - tag.colour_old = colour_id - tag.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1012_fix_archive_files"), - ] - - operations = [ - migrations.RenameField( - model_name="tag", - old_name="colour", - new_name="colour_old", - ), - migrations.AddField( - model_name="tag", - name="color", - field=models.CharField( - default="#a6cee3", - max_length=7, - verbose_name="color", - ), - ), - migrations.RunPython(forward, reverse), - migrations.RemoveField( - model_name="tag", - name="colour_old", - ), - ] diff --git a/src/documents/migrations/1014_auto_20210228_1614.py b/src/documents/migrations/1014_auto_20210228_1614.py deleted file mode 100644 index 5785bcb53..000000000 --- a/src/documents/migrations/1014_auto_20210228_1614.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.1.7 on 2021-02-28 15:14 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1013_migrate_tag_colour"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1015_remove_null_characters.py b/src/documents/migrations/1015_remove_null_characters.py deleted file mode 100644 index 9872b3a75..000000000 --- a/src/documents/migrations/1015_remove_null_characters.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.7 on 2021-04-04 18:28 -import logging - -from django.db import migrations - -logger = logging.getLogger("paperless.migrations") - - -def remove_null_characters(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - content: str = doc.content - if "\0" in content: - logger.info(f"Removing null characters from document {doc}...") - doc.content = content.replace("\0", " ") - doc.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1014_auto_20210228_1614"), - ] - - operations = [ - migrations.RunPython(remove_null_characters, migrations.RunPython.noop), - ] diff --git a/src/documents/migrations/1016_auto_20210317_1351.py b/src/documents/migrations/1016_auto_20210317_1351.py deleted file mode 100644 index 67147fd4c..000000000 --- a/src/documents/migrations/1016_auto_20210317_1351.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 3.1.7 on 2021-03-17 12:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1015_remove_null_characters"), - ] - - operations = [ - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="sort field", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py b/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py deleted file mode 100644 index a7f92e931..000000000 --- a/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py +++ /dev/null @@ -1,190 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:09 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1016_auto_20210317_1351"), - ("documents", "1017_alter_savedviewfilterrule_rule_type"), - ("documents", "1018_alter_savedviewfilterrule_value"), - ("documents", "1019_uisettings"), - ("documents", "1019_storagepath_document_storage_path"), - ("documents", "1020_merge_20220518_1839"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1015_remove_null_characters"), - ] - - operations = [ - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="sort field", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="value", - ), - ), - migrations.CreateModel( - name="UiSettings", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("settings", models.JSONField(null=True)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="ui_settings", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name="StoragePath", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ("path", models.CharField(max_length=512, verbose_name="path")), - ], - options={ - "verbose_name": "storage path", - "verbose_name_plural": "storage paths", - "ordering": ("name",), - }, - ), - migrations.AddField( - model_name="document", - name="storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.storagepath", - verbose_name="storage path", - ), - ), - ] diff --git a/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index ab18f1bc1..000000000 --- a/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-17 11:59 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1016_auto_20210317_1351"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1018_alter_savedviewfilterrule_value.py b/src/documents/migrations/1018_alter_savedviewfilterrule_value.py deleted file mode 100644 index 95ef4861d..000000000 --- a/src/documents/migrations/1018_alter_savedviewfilterrule_value.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.0.3 on 2022-04-01 22:50 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1017_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="value", - ), - ), - ] diff --git a/src/documents/migrations/1019_storagepath_document_storage_path.py b/src/documents/migrations/1019_storagepath_document_storage_path.py deleted file mode 100644 index b09941bf5..000000000 --- a/src/documents/migrations/1019_storagepath_document_storage_path.py +++ /dev/null @@ -1,73 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-02 15:56 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1018_alter_savedviewfilterrule_value"), - ] - - operations = [ - migrations.CreateModel( - name="StoragePath", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ("path", models.CharField(max_length=512, verbose_name="path")), - ], - options={ - "verbose_name": "storage path", - "verbose_name_plural": "storage paths", - "ordering": ("name",), - }, - ), - migrations.AddField( - model_name="document", - name="storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.storagepath", - verbose_name="storage path", - ), - ), - ] diff --git a/src/documents/migrations/1019_uisettings.py b/src/documents/migrations/1019_uisettings.py deleted file mode 100644 index e84138077..000000000 --- a/src/documents/migrations/1019_uisettings.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-07 05:10 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1018_alter_savedviewfilterrule_value"), - ] - - operations = [ - migrations.CreateModel( - name="UiSettings", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("settings", models.JSONField(null=True)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="ui_settings", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1020_merge_20220518_1839.py b/src/documents/migrations/1020_merge_20220518_1839.py deleted file mode 100644 index a766aaa20..000000000 --- a/src/documents/migrations/1020_merge_20220518_1839.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-18 18:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1019_storagepath_document_storage_path"), - ("documents", "1019_uisettings"), - ] - - operations = [] diff --git a/src/documents/migrations/1021_webp_thumbnail_conversion.py b/src/documents/migrations/1021_webp_thumbnail_conversion.py deleted file mode 100644 index 50b12b156..000000000 --- a/src/documents/migrations/1021_webp_thumbnail_conversion.py +++ /dev/null @@ -1,104 +0,0 @@ -# Generated by Django 4.0.5 on 2022-06-11 15:40 -import logging -import multiprocessing.pool -import shutil -import tempfile -import time -from pathlib import Path - -from django.conf import settings -from django.db import migrations - -from documents.parsers import run_convert - -logger = logging.getLogger("paperless.migrations") - - -def _do_convert(work_package): - existing_thumbnail, converted_thumbnail = work_package - try: - logger.info(f"Converting thumbnail: {existing_thumbnail}") - - # Run actual conversion - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{existing_thumbnail}[0]", - output_file=str(converted_thumbnail), - ) - - # Copy newly created thumbnail to thumbnail directory - shutil.copy(converted_thumbnail, existing_thumbnail.parent) - - # Remove the PNG version - existing_thumbnail.unlink() - - logger.info( - "Conversion to WebP completed, " - f"replaced {existing_thumbnail.name} with {converted_thumbnail.name}", - ) - - except Exception as e: - logger.error(f"Error converting thumbnail (existing file unchanged): {e}") - - -def _convert_thumbnails_to_webp(apps, schema_editor): - start = time.time() - - with tempfile.TemporaryDirectory() as tempdir: - work_packages = [] - - for file in Path(settings.THUMBNAIL_DIR).glob("*.png"): - existing_thumbnail = file.resolve() - - # Change the existing filename suffix from png to webp - converted_thumbnail_name = existing_thumbnail.with_suffix( - ".webp", - ).name - - # Create the expected output filename in the tempdir - converted_thumbnail = ( - Path(tempdir) / Path(converted_thumbnail_name) - ).resolve() - - # Package up the necessary info - work_packages.append( - (existing_thumbnail, converted_thumbnail), - ) - - if work_packages: - logger.info( - "\n\n" - " This is a one-time only migration to convert thumbnails for all of your\n" - " documents into WebP format. If you have a lot of documents though, \n" - " this may take a while, so a coffee break may be in order." - "\n", - ) - - with multiprocessing.pool.Pool( - processes=min(multiprocessing.cpu_count(), 4), - maxtasksperchild=4, - ) as pool: - pool.map(_do_convert, work_packages) - - end = time.time() - duration = end - start - - logger.info(f"Conversion completed in {duration:.3f}s") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1020_merge_20220518_1839"), - ] - - operations = [ - migrations.RunPython( - code=_convert_thumbnails_to_webp, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1022_paperlesstask.py b/src/documents/migrations/1022_paperlesstask.py deleted file mode 100644 index c7b3f7744..000000000 --- a/src/documents/migrations/1022_paperlesstask.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-23 07:14 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1021_webp_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="PaperlessTask", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("task_id", models.CharField(max_length=128)), - ("name", models.CharField(max_length=256, null=True)), - ( - "created", - models.DateTimeField(auto_now=True, verbose_name="created"), - ), - ( - "started", - models.DateTimeField(null=True, verbose_name="started"), - ), - ("acknowledged", models.BooleanField(default=False)), - ( - "attempted_task", - models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - # This is a dummy field, 1026 will fix up the column - # This manual change is required, as django doesn't django doesn't really support - # removing an app which has migration deps like this - to="documents.document", - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index dc73a51a2..000000000 --- a/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,668 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:10 - -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1022_paperlesstask"), - ("documents", "1023_add_comments"), - ("documents", "1024_document_original_filename"), - ("documents", "1025_alter_savedviewfilterrule_rule_type"), - ("documents", "1026_transition_to_celery"), - ("documents", "1027_remove_paperlesstask_attempted_task_and_more"), - ("documents", "1028_remove_paperlesstask_task_args_and_more"), - ("documents", "1029_alter_document_archive_serial_number"), - ("documents", "1030_alter_paperlesstask_task_file_name"), - ("documents", "1031_remove_savedview_user_correspondent_owner_and_more"), - ("documents", "1032_alter_correspondent_matching_algorithm_and_more"), - ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"), - ("documents", "1034_alter_savedviewfilterrule_rule_type"), - ("documents", "1035_rename_comment_note"), - ("documents", "1036_alter_savedviewfilterrule_rule_type"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("django_celery_results", "0011_taskresult_periodic_task_name"), - ("documents", "1021_webp_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="Comment", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "comment", - models.TextField( - blank=True, - help_text="Comment for the document", - verbose_name="content", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.document", - verbose_name="document", - ), - ), - ( - "user", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="users", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "verbose_name": "comment", - "verbose_name_plural": "comments", - "ordering": ("created",), - }, - ), - migrations.AddField( - model_name="document", - name="original_filename", - field=models.CharField( - default=None, - editable=False, - help_text="The original name of the file when it was uploaded", - max_length=1024, - null=True, - verbose_name="original filename", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - ], - verbose_name="rule type", - ), - ), - migrations.CreateModel( - name="PaperlessTask", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("task_id", models.CharField(max_length=128)), - ("acknowledged", models.BooleanField(default=False)), - ( - "attempted_task", - models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - to="django_celery_results.taskresult", - ), - ), - ], - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_ormq", - reverse_sql="", - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_schedule", - reverse_sql="", - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_task", - reverse_sql="", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - migrations.AddField( - model_name="paperlesstask", - name="date_created", - field=models.DateTimeField( - default=django.utils.timezone.now, - help_text="Datetime field when the task result was created in UTC", - null=True, - verbose_name="Created DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_done", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was completed in UTC", - null=True, - verbose_name="Completed DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_started", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was started in UTC", - null=True, - verbose_name="Started DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="result", - field=models.TextField( - default=None, - help_text="The data returned by the task", - null=True, - verbose_name="Result Data", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="status", - field=models.CharField( - choices=[ - ("FAILURE", "FAILURE"), - ("PENDING", "PENDING"), - ("RECEIVED", "RECEIVED"), - ("RETRY", "RETRY"), - ("REVOKED", "REVOKED"), - ("STARTED", "STARTED"), - ("SUCCESS", "SUCCESS"), - ], - default="PENDING", - help_text="Current state of the task being run", - max_length=30, - verbose_name="Task State", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - help_text="Name of the Task which was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="acknowledged", - field=models.BooleanField( - default=False, - help_text="If the task is acknowledged via the frontend or API", - verbose_name="Acknowledged", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_id", - field=models.CharField( - help_text="Celery ID for the Task that was run", - max_length=255, - unique=True, - verbose_name="Task ID", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.PositiveIntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - validators=[ - django.core.validators.MaxValueValidator(4294967295), - django.core.validators.MinValueValidator(0), - ], - verbose_name="archive serial number", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Filename", - ), - ), - migrations.RenameField( - model_name="savedview", - old_name="user", - new_name="owner", - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "ordering": ("name",), - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={ - "ordering": ("name",), - "verbose_name": "tag", - "verbose_name_plural": "tags", - }, - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="storagepath", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="correspondent", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_correspondent_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="correspondent", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_correspondent_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="documenttype", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_documenttype_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="documenttype", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_documenttype_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="storagepath", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_storagepath_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="storagepath", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_storagepath_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="tag", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_tag_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="tag", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_tag_name_uniq", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - ], - verbose_name="rule type", - ), - ), - migrations.RenameModel( - old_name="Comment", - new_name="Note", - ), - migrations.RenameField( - model_name="note", - old_name="comment", - new_name="note", - ), - migrations.AlterModelOptions( - name="note", - options={ - "ordering": ("created",), - "verbose_name": "note", - "verbose_name_plural": "notes", - }, - ), - migrations.AlterField( - model_name="note", - name="document", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="notes", - to="documents.document", - verbose_name="document", - ), - ), - migrations.AlterField( - model_name="note", - name="note", - field=models.TextField( - blank=True, - help_text="Note for the document", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="note", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="notes", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1023_add_comments.py b/src/documents/migrations/1023_add_comments.py deleted file mode 100644 index 0b26739bc..000000000 --- a/src/documents/migrations/1023_add_comments.py +++ /dev/null @@ -1,70 +0,0 @@ -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1022_paperlesstask"), - ] - - operations = [ - migrations.CreateModel( - name="Comment", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "comment", - models.TextField( - blank=True, - help_text="Comment for the document", - verbose_name="content", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.document", - verbose_name="document", - ), - ), - ( - "user", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="users", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "verbose_name": "comment", - "verbose_name_plural": "comments", - "ordering": ("created",), - }, - ), - ] diff --git a/src/documents/migrations/1024_document_original_filename.py b/src/documents/migrations/1024_document_original_filename.py deleted file mode 100644 index 05be7269e..000000000 --- a/src/documents/migrations/1024_document_original_filename.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.0.6 on 2022-07-25 06:34 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1023_add_comments"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="original_filename", - field=models.CharField( - default=None, - editable=False, - help_text="The original name of the file when it was uploaded", - max_length=1024, - null=True, - verbose_name="original filename", - ), - ), - ] diff --git a/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index a2deb9579..000000000 --- a/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.0.5 on 2022-08-26 16:49 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1024_document_original_filename"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1026_transition_to_celery.py b/src/documents/migrations/1026_transition_to_celery.py deleted file mode 100644 index 227188d22..000000000 --- a/src/documents/migrations/1026_transition_to_celery.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.1.1 on 2022-09-27 19:31 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("django_celery_results", "0011_taskresult_periodic_task_name"), - ("documents", "1025_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="created", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="name", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="started", - ), - # Remove the field from the model - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - # Add the field back, pointing to the correct model - # This resolves a problem where the temporary change in 1022 - # results in a type mismatch - migrations.AddField( - model_name="paperlesstask", - name="attempted_task", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - to="django_celery_results.taskresult", - ), - ), - # Drop the django-q tables entirely - # Must be done last or there could be references here - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_ormq", - reverse_sql=migrations.RunSQL.noop, - ), - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_schedule", - reverse_sql=migrations.RunSQL.noop, - ), - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_task", - reverse_sql=migrations.RunSQL.noop, - ), - ] diff --git a/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py b/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py deleted file mode 100644 index c169c3096..000000000 --- a/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py +++ /dev/null @@ -1,134 +0,0 @@ -# Generated by Django 4.1.2 on 2022-10-17 16:31 - -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1026_transition_to_celery"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - migrations.AddField( - model_name="paperlesstask", - name="date_created", - field=models.DateTimeField( - default=django.utils.timezone.now, - help_text="Datetime field when the task result was created in UTC", - null=True, - verbose_name="Created DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_done", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was completed in UTC", - null=True, - verbose_name="Completed DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_started", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was started in UTC", - null=True, - verbose_name="Started DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="result", - field=models.TextField( - default=None, - help_text="The data returned by the task", - null=True, - verbose_name="Result Data", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="status", - field=models.CharField( - choices=[ - ("FAILURE", "FAILURE"), - ("PENDING", "PENDING"), - ("RECEIVED", "RECEIVED"), - ("RETRY", "RETRY"), - ("REVOKED", "REVOKED"), - ("STARTED", "STARTED"), - ("SUCCESS", "SUCCESS"), - ], - default="PENDING", - help_text="Current state of the task being run", - max_length=30, - verbose_name="Task State", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_args", - field=models.JSONField( - help_text="JSON representation of the positional arguments used with the task", - null=True, - verbose_name="Task Positional Arguments", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_kwargs", - field=models.JSONField( - help_text="JSON representation of the named arguments used with the task", - null=True, - verbose_name="Task Named Arguments", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - help_text="Name of the Task which was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="acknowledged", - field=models.BooleanField( - default=False, - help_text="If the task is acknowledged via the frontend or API", - verbose_name="Acknowledged", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_id", - field=models.CharField( - help_text="Celery ID for the Task that was run", - max_length=255, - unique=True, - verbose_name="Task ID", - ), - ), - ] diff --git a/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py b/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py deleted file mode 100644 index 6e03c124b..000000000 --- a/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-22 17:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1027_remove_paperlesstask_attempted_task_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="task_args", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="task_kwargs", - ), - ] diff --git a/src/documents/migrations/1029_alter_document_archive_serial_number.py b/src/documents/migrations/1029_alter_document_archive_serial_number.py deleted file mode 100644 index 57848b2dc..000000000 --- a/src/documents/migrations/1029_alter_document_archive_serial_number.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.1.4 on 2023-01-24 17:56 - -import django.core.validators -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1028_remove_paperlesstask_task_args_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.PositiveIntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - validators=[ - django.core.validators.MaxValueValidator(4294967295), - django.core.validators.MinValueValidator(0), - ], - verbose_name="archive serial number", - ), - ), - ] diff --git a/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py b/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py deleted file mode 100644 index 37e918bee..000000000 --- a/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.5 on 2023-02-03 21:53 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1029_alter_document_archive_serial_number"), - ] - - operations = [ - migrations.AlterField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Filename", - ), - ), - ] diff --git a/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py b/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py deleted file mode 100644 index 56e4355ef..000000000 --- a/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py +++ /dev/null @@ -1,87 +0,0 @@ -# Generated by Django 4.1.4 on 2022-02-03 04:24 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1030_alter_paperlesstask_task_file_name"), - ] - - operations = [ - migrations.RenameField( - model_name="savedview", - old_name="user", - new_name="owner", - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py b/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py deleted file mode 100644 index 3d1c5658a..000000000 --- a/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py +++ /dev/null @@ -1,81 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-22 00:45 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1031_remove_savedview_user_correspondent_owner_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ] diff --git a/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index b648ac839..000000000 --- a/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-15 07:10 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1035_rename_comment_note.py b/src/documents/migrations/1035_rename_comment_note.py deleted file mode 100644 index 9f9aaca94..000000000 --- a/src/documents/migrations/1035_rename_comment_note.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-17 22:15 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1034_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RenameModel( - old_name="Comment", - new_name="Note", - ), - migrations.RenameField(model_name="note", old_name="comment", new_name="note"), - migrations.AlterModelOptions( - name="note", - options={ - "ordering": ("created",), - "verbose_name": "note", - "verbose_name_plural": "notes", - }, - ), - migrations.AlterField( - model_name="note", - name="document", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="notes", - to="documents.document", - verbose_name="document", - ), - ), - migrations.AlterField( - model_name="note", - name="note", - field=models.TextField( - blank=True, - help_text="Note for the document", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="note", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="notes", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ] diff --git a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index e65586ad8..000000000 --- a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 4.1.7 on 2023-05-04 04:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1035_rename_comment_note"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py b/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py deleted file mode 100644 index 13996132f..000000000 --- a/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py +++ /dev/null @@ -1,164 +0,0 @@ -# Generated by Django 4.1.9 on 2023-06-29 19:29 -import logging -import multiprocessing.pool -import shutil -import tempfile -import time -from pathlib import Path - -import gnupg -from django.conf import settings -from django.db import migrations - -from documents.parsers import run_convert - -logger = logging.getLogger("paperless.migrations") - - -def _do_convert(work_package) -> None: - ( - existing_encrypted_thumbnail, - converted_encrypted_thumbnail, - passphrase, - ) = work_package - - try: - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - logger.info(f"Decrypting thumbnail: {existing_encrypted_thumbnail}") - - # Decrypt png - decrypted_thumbnail = existing_encrypted_thumbnail.with_suffix("").resolve() - - with existing_encrypted_thumbnail.open("rb") as existing_encrypted_file: - raw_thumb = gpg.decrypt_file( - existing_encrypted_file, - passphrase=passphrase, - always_trust=True, - ).data - with Path(decrypted_thumbnail).open("wb") as decrypted_file: - decrypted_file.write(raw_thumb) - - converted_decrypted_thumbnail = Path( - str(converted_encrypted_thumbnail).replace("webp.gpg", "webp"), - ).resolve() - - logger.info(f"Converting decrypted thumbnail: {decrypted_thumbnail}") - - # Convert to webp - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{decrypted_thumbnail}[0]", - output_file=str(converted_decrypted_thumbnail), - ) - - logger.info( - f"Encrypting converted thumbnail: {converted_decrypted_thumbnail}", - ) - - # Encrypt webp - with Path(converted_decrypted_thumbnail).open("rb") as converted_decrypted_file: - encrypted = gpg.encrypt_file( - fileobj_or_path=converted_decrypted_file, - recipients=None, - passphrase=passphrase, - symmetric=True, - always_trust=True, - ).data - - with Path(converted_encrypted_thumbnail).open( - "wb", - ) as converted_encrypted_file: - converted_encrypted_file.write(encrypted) - - # Copy newly created thumbnail to thumbnail directory - shutil.copy(converted_encrypted_thumbnail, existing_encrypted_thumbnail.parent) - - # Remove the existing encrypted PNG version - existing_encrypted_thumbnail.unlink() - - # Remove the decrypted PNG version - decrypted_thumbnail.unlink() - - # Remove the decrypted WebP version - converted_decrypted_thumbnail.unlink() - - logger.info( - "Conversion to WebP completed, " - f"replaced {existing_encrypted_thumbnail.name} with {converted_encrypted_thumbnail.name}", - ) - - except Exception as e: - logger.error(f"Error converting thumbnail (existing file unchanged): {e}") - - -def _convert_encrypted_thumbnails_to_webp(apps, schema_editor) -> None: - start: float = time.time() - - with tempfile.TemporaryDirectory() as tempdir: - work_packages = [] - - if len(list(Path(settings.THUMBNAIL_DIR).glob("*.png.gpg"))) > 0: - passphrase = settings.PASSPHRASE - - if not passphrase: - raise Exception( - "Passphrase not defined, encrypted thumbnails cannot be migrated" - "without this", - ) - - for file in Path(settings.THUMBNAIL_DIR).glob("*.png.gpg"): - existing_thumbnail: Path = file.resolve() - - # Change the existing filename suffix from png to webp - converted_thumbnail_name: str = Path( - str(existing_thumbnail).replace(".png.gpg", ".webp.gpg"), - ).name - - # Create the expected output filename in the tempdir - converted_thumbnail: Path = ( - Path(tempdir) / Path(converted_thumbnail_name) - ).resolve() - - # Package up the necessary info - work_packages.append( - (existing_thumbnail, converted_thumbnail, passphrase), - ) - - if work_packages: - logger.info( - "\n\n" - " This is a one-time only migration to convert thumbnails for all of your\n" - " *encrypted* documents into WebP format. If you have a lot of encrypted documents, \n" - " this may take a while, so a coffee break may be in order." - "\n", - ) - - with multiprocessing.pool.Pool( - processes=min(multiprocessing.cpu_count(), 4), - maxtasksperchild=4, - ) as pool: - pool.map(_do_convert, work_packages) - - end: float = time.time() - duration: float = end - start - - logger.info(f"Conversion completed in {duration:.3f}s") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1036_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RunPython( - code=_convert_encrypted_thumbnails_to_webp, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1038_sharelink.py b/src/documents/migrations/1038_sharelink.py deleted file mode 100644 index fa2860b6f..000000000 --- a/src/documents/migrations/1038_sharelink.py +++ /dev/null @@ -1,126 +0,0 @@ -# Generated by Django 4.1.10 on 2023-08-14 14:51 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_sharelink_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - sharelink_permissions = Permission.objects.filter(codename__contains="sharelink") - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*sharelink_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*sharelink_permissions) - - -def remove_sharelink_permissions(apps, schema_editor): - sharelink_permissions = Permission.objects.filter(codename__contains="sharelink") - - for user in User.objects.all(): - user.user_permissions.remove(*sharelink_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*sharelink_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1037_webp_encrypted_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="ShareLink", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - blank=True, - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "expiration", - models.DateTimeField( - blank=True, - db_index=True, - null=True, - verbose_name="expiration", - ), - ), - ( - "slug", - models.SlugField( - blank=True, - editable=False, - unique=True, - verbose_name="slug", - ), - ), - ( - "file_version", - models.CharField( - choices=[("archive", "Archive"), ("original", "Original")], - default="archive", - max_length=50, - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="share_links", - to="documents.document", - verbose_name="document", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="share_links", - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ], - options={ - "verbose_name": "share link", - "verbose_name_plural": "share links", - "ordering": ("created",), - }, - ), - migrations.RunPython(add_sharelink_permissions, remove_sharelink_permissions), - ] diff --git a/src/documents/migrations/1039_consumptiontemplate.py b/src/documents/migrations/1039_consumptiontemplate.py deleted file mode 100644 index cf8b9fd91..000000000 --- a/src/documents/migrations/1039_consumptiontemplate.py +++ /dev/null @@ -1,219 +0,0 @@ -# Generated by Django 4.1.11 on 2023-09-16 18:04 - -import django.db.models.deletion -import multiselectfield.db.fields -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_consumptiontemplate_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - consumptiontemplate_permissions = Permission.objects.filter( - codename__contains="consumptiontemplate", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*consumptiontemplate_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*consumptiontemplate_permissions) - - -def remove_consumptiontemplate_permissions(apps, schema_editor): - consumptiontemplate_permissions = Permission.objects.filter( - codename__contains="consumptiontemplate", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*consumptiontemplate_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*consumptiontemplate_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ("documents", "1038_sharelink"), - ("paperless_mail", "0021_alter_mailaccount_password"), - ] - - operations = [ - migrations.CreateModel( - name="ConsumptionTemplate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - ("order", models.IntegerField(default=0, verbose_name="order")), - ( - "sources", - multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - ], - default="1,2,3", - max_length=3, - ), - ), - ( - "filter_path", - models.CharField( - blank=True, - help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter path", - ), - ), - ( - "filter_filename", - models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter filename", - ), - ), - ( - "filter_mailrule", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="paperless_mail.mailrule", - verbose_name="filter documents from this mail rule", - ), - ), - ( - "assign_change_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant change permissions to these groups", - ), - ), - ( - "assign_change_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant change permissions to these users", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - ( - "assign_owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="assign this owner", - ), - ), - ( - "assign_storage_path", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - ( - "assign_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ( - "assign_title", - models.CharField( - blank=True, - help_text="Assign a document title, can include some placeholders, see documentation.", - max_length=256, - null=True, - verbose_name="assign title", - ), - ), - ( - "assign_view_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant view permissions to these groups", - ), - ), - ( - "assign_view_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant view permissions to these users", - ), - ), - ], - options={ - "verbose_name": "consumption template", - "verbose_name_plural": "consumption templates", - }, - ), - migrations.RunPython( - add_consumptiontemplate_permissions, - remove_consumptiontemplate_permissions, - ), - ] diff --git a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py b/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py deleted file mode 100644 index ecd715a57..000000000 --- a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py +++ /dev/null @@ -1,171 +0,0 @@ -# Generated by Django 4.2.6 on 2023-11-02 17:38 - -import django.db.models.deletion -import django.utils.timezone -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_customfield_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - customfield_permissions = Permission.objects.filter( - codename__contains="customfield", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*customfield_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*customfield_permissions) - - -def remove_customfield_permissions(apps, schema_editor): - customfield_permissions = Permission.objects.filter( - codename__contains="customfield", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*customfield_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*customfield_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1039_consumptiontemplate"), - ] - - operations = [ - migrations.CreateModel( - name="CustomField", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ("name", models.CharField(max_length=128)), - ( - "data_type", - models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ], - options={ - "verbose_name": "custom field", - "verbose_name_plural": "custom fields", - "ordering": ("created",), - }, - ), - migrations.CreateModel( - name="CustomFieldInstance", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ("value_text", models.CharField(max_length=128, null=True)), - ("value_bool", models.BooleanField(null=True)), - ("value_url", models.URLField(null=True)), - ("value_date", models.DateField(null=True)), - ("value_int", models.IntegerField(null=True)), - ("value_float", models.FloatField(null=True)), - ( - "value_monetary", - models.DecimalField(decimal_places=2, max_digits=12, null=True), - ), - ( - "document", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - related_name="custom_fields", - to="documents.document", - ), - ), - ( - "field", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - related_name="fields", - to="documents.customfield", - ), - ), - ], - options={ - "verbose_name": "custom field instance", - "verbose_name_plural": "custom field instances", - "ordering": ("created",), - }, - ), - migrations.AddConstraint( - model_name="customfield", - constraint=models.UniqueConstraint( - fields=("name",), - name="documents_customfield_unique_name", - ), - ), - migrations.AddConstraint( - model_name="customfieldinstance", - constraint=models.UniqueConstraint( - fields=("document", "field"), - name="documents_customfieldinstance_unique_document_field", - ), - ), - migrations.RunPython( - add_customfield_permissions, - remove_customfield_permissions, - ), - ] diff --git a/src/documents/migrations/1041_alter_consumptiontemplate_sources.py b/src/documents/migrations/1041_alter_consumptiontemplate_sources.py deleted file mode 100644 index c96dc53cf..000000000 --- a/src/documents/migrations/1041_alter_consumptiontemplate_sources.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-30 14:29 - -import multiselectfield.db.fields -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1040_customfield_customfieldinstance_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="consumptiontemplate", - name="sources", - field=multiselectfield.db.fields.MultiSelectField( - choices=[(1, "Consume Folder"), (2, "Api Upload"), (3, "Mail Fetch")], - default="1,2,3", - max_length=5, - ), - ), - ] diff --git a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py b/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py deleted file mode 100644 index ffd0dbefa..000000000 --- a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-04 04:03 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1041_alter_consumptiontemplate_sources"), - ] - - operations = [ - migrations.AddField( - model_name="consumptiontemplate", - name="assign_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="assign these custom fields", - ), - ), - migrations.AddField( - model_name="customfieldinstance", - name="value_document_ids", - field=models.JSONField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index bd62673df..000000000 --- a/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 18:13 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1042_consumptiontemplate_assign_custom_fields_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py b/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py deleted file mode 100644 index 2cdd631bb..000000000 --- a/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py +++ /dev/null @@ -1,524 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-23 22:51 - -import django.db.models.deletion -import multiselectfield.db.fields -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.db import migrations -from django.db import models -from django.db import transaction -from django.db.models import Q - - -def add_workflow_permissions(apps, schema_editor): - app_name = "auth" - User = apps.get_model(app_label=app_name, model_name="User") - Group = apps.get_model(app_label=app_name, model_name="Group") - Permission = apps.get_model(app_label=app_name, model_name="Permission") - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - workflow_permissions = Permission.objects.filter( - codename__contains="workflow", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*workflow_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*workflow_permissions) - - -def remove_workflow_permissions(apps, schema_editor): - app_name = "auth" - User = apps.get_model(app_label=app_name, model_name="User") - Group = apps.get_model(app_label=app_name, model_name="Group") - Permission = apps.get_model(app_label=app_name, model_name="Permission") - workflow_permissions = Permission.objects.filter( - codename__contains="workflow", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*workflow_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*workflow_permissions) - - -def migrate_consumption_templates(apps, schema_editor): - """ - Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists - but objects are not returned as their true model so we have to manually do that - """ - app_name = "documents" - - ConsumptionTemplate = apps.get_model( - app_label=app_name, - model_name="ConsumptionTemplate", - ) - Workflow = apps.get_model(app_label=app_name, model_name="Workflow") - WorkflowAction = apps.get_model(app_label=app_name, model_name="WorkflowAction") - WorkflowTrigger = apps.get_model(app_label=app_name, model_name="WorkflowTrigger") - DocumentType = apps.get_model(app_label=app_name, model_name="DocumentType") - Correspondent = apps.get_model(app_label=app_name, model_name="Correspondent") - StoragePath = apps.get_model(app_label=app_name, model_name="StoragePath") - Tag = apps.get_model(app_label=app_name, model_name="Tag") - CustomField = apps.get_model(app_label=app_name, model_name="CustomField") - MailRule = apps.get_model(app_label="paperless_mail", model_name="MailRule") - User = apps.get_model(app_label="auth", model_name="User") - Group = apps.get_model(app_label="auth", model_name="Group") - - with transaction.atomic(): - for template in ConsumptionTemplate.objects.all(): - trigger = WorkflowTrigger( - type=1, # WorkflowTriggerType.CONSUMPTION - sources=template.sources, - filter_path=template.filter_path, - filter_filename=template.filter_filename, - ) - if template.filter_mailrule is not None: - trigger.filter_mailrule = MailRule.objects.get( - id=template.filter_mailrule.id, - ) - trigger.save() - - action = WorkflowAction.objects.create( - assign_title=template.assign_title, - ) - if template.assign_document_type is not None: - action.assign_document_type = DocumentType.objects.get( - id=template.assign_document_type.id, - ) - if template.assign_correspondent is not None: - action.assign_correspondent = Correspondent.objects.get( - id=template.assign_correspondent.id, - ) - if template.assign_storage_path is not None: - action.assign_storage_path = StoragePath.objects.get( - id=template.assign_storage_path.id, - ) - if template.assign_owner is not None: - action.assign_owner = User.objects.get(id=template.assign_owner.id) - if template.assign_tags is not None: - action.assign_tags.set( - Tag.objects.filter( - id__in=[t.id for t in template.assign_tags.all()], - ).all(), - ) - if template.assign_view_users is not None: - action.assign_view_users.set( - User.objects.filter( - id__in=[u.id for u in template.assign_view_users.all()], - ).all(), - ) - if template.assign_view_groups is not None: - action.assign_view_groups.set( - Group.objects.filter( - id__in=[g.id for g in template.assign_view_groups.all()], - ).all(), - ) - if template.assign_change_users is not None: - action.assign_change_users.set( - User.objects.filter( - id__in=[u.id for u in template.assign_change_users.all()], - ).all(), - ) - if template.assign_change_groups is not None: - action.assign_change_groups.set( - Group.objects.filter( - id__in=[g.id for g in template.assign_change_groups.all()], - ).all(), - ) - if template.assign_custom_fields is not None: - action.assign_custom_fields.set( - CustomField.objects.filter( - id__in=[cf.id for cf in template.assign_custom_fields.all()], - ).all(), - ) - action.save() - - workflow = Workflow.objects.create( - name=template.name, - order=template.order, - ) - workflow.triggers.set([trigger]) - workflow.actions.set([action]) - workflow.save() - - -def unmigrate_consumption_templates(apps, schema_editor): - app_name = "documents" - - ConsumptionTemplate = apps.get_model( - app_label=app_name, - model_name="ConsumptionTemplate", - ) - Workflow = apps.get_model(app_label=app_name, model_name="Workflow") - - for workflow in Workflow.objects.all(): - template = ConsumptionTemplate.objects.create( - name=workflow.name, - order=workflow.order, - sources=workflow.triggers.first().sources, - filter_path=workflow.triggers.first().filter_path, - filter_filename=workflow.triggers.first().filter_filename, - filter_mailrule=workflow.triggers.first().filter_mailrule, - assign_title=workflow.actions.first().assign_title, - assign_document_type=workflow.actions.first().assign_document_type, - assign_correspondent=workflow.actions.first().assign_correspondent, - assign_storage_path=workflow.actions.first().assign_storage_path, - assign_owner=workflow.actions.first().assign_owner, - ) - template.assign_tags.set(workflow.actions.first().assign_tags.all()) - template.assign_view_users.set(workflow.actions.first().assign_view_users.all()) - template.assign_view_groups.set( - workflow.actions.first().assign_view_groups.all(), - ) - template.assign_change_users.set( - workflow.actions.first().assign_change_users.all(), - ) - template.assign_change_groups.set( - workflow.actions.first().assign_change_groups.all(), - ) - template.assign_custom_fields.set( - workflow.actions.first().assign_custom_fields.all(), - ) - template.save() - - -def delete_consumption_template_content_type(apps, schema_editor): - with transaction.atomic(): - apps.get_model("contenttypes", "ContentType").objects.filter( - app_label="documents", - model="consumptiontemplate", - ).delete() - - -def undelete_consumption_template_content_type(apps, schema_editor): - apps.get_model("contenttypes", "ContentType").objects.create( - app_label="documents", - model="consumptiontemplate", - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ("documents", "1043_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.CreateModel( - name="Workflow", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - ("order", models.IntegerField(default=0, verbose_name="order")), - ( - "enabled", - models.BooleanField(default=True, verbose_name="enabled"), - ), - ], - ), - migrations.CreateModel( - name="WorkflowAction", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[(1, "Assignment")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - ( - "assign_title", - models.CharField( - blank=True, - help_text="Assign a document title, can include some placeholders, see documentation.", - max_length=256, - null=True, - verbose_name="assign title", - ), - ), - ( - "assign_change_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant change permissions to these groups", - ), - ), - ( - "assign_change_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant change permissions to these users", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - ( - "assign_custom_fields", - models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="assign these custom fields", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - ( - "assign_owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="assign this owner", - ), - ), - ( - "assign_storage_path", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - ( - "assign_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ( - "assign_view_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant view permissions to these groups", - ), - ), - ( - "assign_view_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant view permissions to these users", - ), - ), - ], - options={ - "verbose_name": "workflow action", - "verbose_name_plural": "workflow actions", - }, - ), - migrations.CreateModel( - name="WorkflowTrigger", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - ], - default=1, - verbose_name="Workflow Trigger Type", - ), - ), - ( - "sources", - multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - ], - default="1,2,3", - max_length=5, - ), - ), - ( - "filter_path", - models.CharField( - blank=True, - help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter path", - ), - ), - ( - "filter_filename", - models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter filename", - ), - ), - ( - "filter_mailrule", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="paperless_mail.mailrule", - verbose_name="filter documents from this mail rule", - ), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - ], - default=0, - verbose_name="matching algorithm", - ), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ( - "filter_has_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="has these tag(s)", - ), - ), - ( - "filter_has_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="has this document type", - ), - ), - ( - "filter_has_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="has this correspondent", - ), - ), - ], - options={ - "verbose_name": "workflow trigger", - "verbose_name_plural": "workflow triggers", - }, - ), - migrations.RunPython( - add_workflow_permissions, - remove_workflow_permissions, - ), - migrations.AddField( - model_name="workflow", - name="actions", - field=models.ManyToManyField( - related_name="workflows", - to="documents.workflowaction", - verbose_name="actions", - ), - ), - migrations.AddField( - model_name="workflow", - name="triggers", - field=models.ManyToManyField( - related_name="workflows", - to="documents.workflowtrigger", - verbose_name="triggers", - ), - ), - migrations.RunPython( - migrate_consumption_templates, - unmigrate_consumption_templates, - ), - migrations.DeleteModel("ConsumptionTemplate"), - migrations.RunPython( - delete_consumption_template_content_type, - undelete_consumption_template_content_type, - ), - ] diff --git a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py b/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py deleted file mode 100644 index 597fbb7f9..000000000 --- a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.10 on 2024-02-22 03:52 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1044_workflow_workflowaction_workflowtrigger_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_monetary", - field=models.CharField(max_length=128, null=True), - ), - ] diff --git a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py b/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py deleted file mode 100644 index 2987e4812..000000000 --- a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py +++ /dev/null @@ -1,331 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 19:39 - -import django.core.validators -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1045_alter_customfieldinstance_value_monetary"), - ("documents", "1046_workflowaction_remove_all_correspondents_and_more"), - ("documents", "1047_savedview_display_mode_and_more"), - ("documents", "1048_alter_savedviewfilterrule_rule_type"), - ("documents", "1049_document_deleted_at_document_restored_at"), - ] - - dependencies = [ - ("documents", "1044_workflow_workflowaction_workflowtrigger_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_monetary", - field=models.CharField(max_length=128, null=True), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_correspondents", - field=models.BooleanField( - default=False, - verbose_name="remove all correspondents", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_custom_fields", - field=models.BooleanField( - default=False, - verbose_name="remove all custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_document_types", - field=models.BooleanField( - default=False, - verbose_name="remove all document types", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_owners", - field=models.BooleanField(default=False, verbose_name="remove all owners"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_permissions", - field=models.BooleanField( - default=False, - verbose_name="remove all permissions", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_storage_paths", - field=models.BooleanField( - default=False, - verbose_name="remove all storage paths", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_tags", - field=models.BooleanField(default=False, verbose_name="remove all tags"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove change permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove change permissions for these users", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.correspondent", - verbose_name="remove these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="remove these custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_document_types", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.documenttype", - verbose_name="remove these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_owners", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove these owner(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.storagepath", - verbose_name="remove these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="remove these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove view permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove view permissions for these users", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[(1, "Assignment"), (2, "Removal")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_mode", - field=models.CharField( - blank=True, - choices=[ - ("table", "Table"), - ("smallCards", "Small Cards"), - ("largeCards", "Large Cards"), - ], - max_length=128, - null=True, - verbose_name="View display mode", - ), - ), - migrations.AddField( - model_name="savedview", - name="page_size", - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="View page size", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_fields", - field=models.JSONField( - blank=True, - null=True, - verbose_name="Document display fields", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - ], - verbose_name="rule type", - ), - ), - migrations.AddField( - model_name="document", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="document", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py b/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py deleted file mode 100644 index 3ab010a3c..000000000 --- a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py +++ /dev/null @@ -1,222 +0,0 @@ -# Generated by Django 4.2.10 on 2024-02-21 21:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1045_alter_customfieldinstance_value_monetary"), - ] - - operations = [ - migrations.AddField( - model_name="workflowaction", - name="remove_all_correspondents", - field=models.BooleanField( - default=False, - verbose_name="remove all correspondents", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_custom_fields", - field=models.BooleanField( - default=False, - verbose_name="remove all custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_document_types", - field=models.BooleanField( - default=False, - verbose_name="remove all document types", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_owners", - field=models.BooleanField(default=False, verbose_name="remove all owners"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_permissions", - field=models.BooleanField( - default=False, - verbose_name="remove all permissions", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_storage_paths", - field=models.BooleanField( - default=False, - verbose_name="remove all storage paths", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_tags", - field=models.BooleanField(default=False, verbose_name="remove all tags"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove change permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove change permissions for these users", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.correspondent", - verbose_name="remove these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="remove these custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_document_types", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.documenttype", - verbose_name="remove these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_owners", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove these owner(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.storagepath", - verbose_name="remove these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="remove these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove view permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove view permissions for these users", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[(1, "Assignment"), (2, "Removal")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - ] diff --git a/src/documents/migrations/1047_savedview_display_mode_and_more.py b/src/documents/migrations/1047_savedview_display_mode_and_more.py deleted file mode 100644 index 904f86bb1..000000000 --- a/src/documents/migrations/1047_savedview_display_mode_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-16 18:35 - -import django.core.validators -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1046_workflowaction_remove_all_correspondents_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="savedview", - name="display_mode", - field=models.CharField( - blank=True, - choices=[ - ("table", "Table"), - ("smallCards", "Small Cards"), - ("largeCards", "Large Cards"), - ], - max_length=128, - null=True, - verbose_name="View display mode", - ), - ), - migrations.AddField( - model_name="savedview", - name="page_size", - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="View page size", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_fields", - field=models.JSONField( - blank=True, - null=True, - verbose_name="Document display fields", - ), - ), - ] diff --git a/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index 904ad242c..000000000 --- a/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-24 04:58 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1047_savedview_display_mode_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1049_document_deleted_at_document_restored_at.py b/src/documents/migrations/1049_document_deleted_at_document_restored_at.py deleted file mode 100644 index 39fb41353..000000000 --- a/src/documents/migrations/1049_document_deleted_at_document_restored_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-23 07:56 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1048_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="document", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1050_customfield_extra_data_and_more.py b/src/documents/migrations/1050_customfield_extra_data_and_more.py deleted file mode 100644 index 0c6a77ccc..000000000 --- a/src/documents/migrations/1050_customfield_extra_data_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-04 01:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1049_document_deleted_at_document_restored_at"), - ] - - operations = [ - migrations.AddField( - model_name="customfield", - name="extra_data", - field=models.JSONField( - blank=True, - help_text="Extra data for the custom field, such as select options", - null=True, - verbose_name="extra data", - ), - ), - migrations.AddField( - model_name="customfieldinstance", - name="value_select", - field=models.PositiveSmallIntegerField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ("select", "Select"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py b/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py deleted file mode 100644 index e8f0bb97c..000000000 --- a/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-09 16:39 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1050_customfield_extra_data_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1052_document_transaction_id.py b/src/documents/migrations/1052_document_transaction_id.py deleted file mode 100644 index 5eb8e2ef9..000000000 --- a/src/documents/migrations/1052_document_transaction_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.15 on 2024-08-20 02:41 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1051_alter_correspondent_owner_alter_document_owner_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1053_document_page_count.py b/src/documents/migrations/1053_document_page_count.py deleted file mode 100644 index 3a8bc5d79..000000000 --- a/src/documents/migrations/1053_document_page_count.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-28 04:42 - -from pathlib import Path - -import pikepdf -from django.conf import settings -from django.core.validators import MinValueValidator -from django.db import migrations -from django.db import models -from django.utils.termcolors import colorize as colourise - - -def source_path(self): - if self.filename: - fname = str(self.filename) - - return Path(settings.ORIGINALS_DIR / fname).resolve() - - -def add_number_of_pages_to_page_count(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - if not Document.objects.all().exists(): - return - - for doc in Document.objects.filter(mime_type="application/pdf"): - print( - " {} {} {}".format( - colourise("*", fg="green"), - colourise("Calculating number of pages for", fg="white"), - colourise(doc.filename, fg="cyan"), - ), - ) - - try: - with pikepdf.Pdf.open(source_path(doc)) as pdf: - if pdf.pages is not None: - doc.page_count = len(pdf.pages) - doc.save() - except Exception as e: # pragma: no cover - print(f"Error retrieving number of pages for {doc.filename}: {e}") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1052_document_transaction_id"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="page_count", - field=models.PositiveIntegerField( - blank=False, - help_text="The number of pages of the document.", - null=True, - unique=False, - validators=[MinValueValidator(1)], - verbose_name="page count", - db_index=False, - ), - ), - migrations.RunPython( - add_number_of_pages_to_page_count, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py b/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py deleted file mode 100644 index 92d45de33..000000000 --- a/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-29 16:26 - -import django.db.models.functions.comparison -import django.db.models.functions.text -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1053_document_page_count"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="value_monetary_amount", - field=models.GeneratedField( - db_persist=True, - expression=models.Case( - models.When( - then=django.db.models.functions.comparison.Cast( - django.db.models.functions.text.Substr("value_monetary", 1), - output_field=models.DecimalField( - decimal_places=2, - max_digits=65, - ), - ), - value_monetary__regex="^\\d+", - ), - default=django.db.models.functions.comparison.Cast( - django.db.models.functions.text.Substr("value_monetary", 4), - output_field=models.DecimalField( - decimal_places=2, - max_digits=65, - ), - ), - output_field=models.DecimalField(decimal_places=2, max_digits=65), - ), - output_field=models.DecimalField(decimal_places=2, max_digits=65), - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - (42, "custom fields query"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1055_alter_storagepath_path.py b/src/documents/migrations/1055_alter_storagepath_path.py deleted file mode 100644 index 1421bf824..000000000 --- a/src/documents/migrations/1055_alter_storagepath_path.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-03 14:47 - -from django.conf import settings -from django.db import migrations -from django.db import models -from django.db import transaction -from filelock import FileLock - -from documents.templating.utils import convert_format_str_to_template_format - - -def convert_from_format_to_template(apps, schema_editor): - StoragePath = apps.get_model("documents", "StoragePath") - - with transaction.atomic(), FileLock(settings.MEDIA_LOCK): - for storage_path in StoragePath.objects.all(): - storage_path.path = convert_format_str_to_template_format(storage_path.path) - storage_path.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1054_customfieldinstance_value_monetary_amount_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="storagepath", - name="path", - field=models.TextField(verbose_name="path"), - ), - migrations.RunPython( - convert_from_format_to_template, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py b/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py deleted file mode 100644 index eba1e4281..000000000 --- a/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-28 01:55 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1055_alter_storagepath_path"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1057_paperlesstask_owner.py b/src/documents/migrations/1057_paperlesstask_owner.py deleted file mode 100644 index e9f108d3a..000000000 --- a/src/documents/migrations/1057_paperlesstask_owner.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-04 21:56 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1056_customfieldinstance_deleted_at_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="paperlesstask", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py b/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py deleted file mode 100644 index 05d38578a..000000000 --- a/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py +++ /dev/null @@ -1,143 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-05 05:19 - -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1057_paperlesstask_owner"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="schedule_date_custom_field", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.customfield", - verbose_name="schedule date custom field", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_date_field", - field=models.CharField( - choices=[ - ("added", "Added"), - ("created", "Created"), - ("modified", "Modified"), - ("custom_field", "Custom Field"), - ], - default="added", - help_text="The field to check for a schedule trigger.", - max_length=20, - verbose_name="schedule date field", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_is_recurring", - field=models.BooleanField( - default=False, - help_text="If the schedule should be recurring.", - verbose_name="schedule is recurring", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_offset_days", - field=models.PositiveIntegerField( - default=0, - help_text="The number of days to offset the schedule trigger by.", - verbose_name="schedule offset days", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_recurring_interval_days", - field=models.PositiveIntegerField( - default=1, - help_text="The number of days between recurring schedule triggers.", - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="schedule recurring delay in days", - ), - ), - migrations.AlterField( - model_name="workflowtrigger", - name="type", - field=models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - (4, "Scheduled"), - ], - default=1, - verbose_name="Workflow Trigger Type", - ), - ), - migrations.CreateModel( - name="WorkflowRun", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - (4, "Scheduled"), - ], - null=True, - verbose_name="workflow trigger type", - ), - ), - ( - "run_at", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="date run", - ), - ), - ( - "document", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="workflow_runs", - to="documents.document", - verbose_name="document", - ), - ), - ( - "workflow", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="runs", - to="documents.workflow", - verbose_name="workflow", - ), - ), - ], - options={ - "verbose_name": "workflow run", - "verbose_name_plural": "workflow runs", - }, - ), - ] diff --git a/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py b/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py deleted file mode 100644 index d94470285..000000000 --- a/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py +++ /dev/null @@ -1,154 +0,0 @@ -# Generated by Django 5.1.3 on 2024-11-26 04:07 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="WorkflowActionEmail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "subject", - models.CharField( - help_text="The subject of the email, can include some placeholders, see documentation.", - max_length=256, - verbose_name="email subject", - ), - ), - ( - "body", - models.TextField( - help_text="The body (message) of the email, can include some placeholders, see documentation.", - verbose_name="email body", - ), - ), - ( - "to", - models.TextField( - help_text="The destination email addresses, comma separated.", - verbose_name="emails to", - ), - ), - ( - "include_document", - models.BooleanField( - default=False, - verbose_name="include document in email", - ), - ), - ], - ), - migrations.CreateModel( - name="WorkflowActionWebhook", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "url", - models.URLField( - help_text="The destination URL for the notification.", - verbose_name="webhook url", - ), - ), - ( - "use_params", - models.BooleanField(default=True, verbose_name="use parameters"), - ), - ( - "params", - models.JSONField( - blank=True, - help_text="The parameters to send with the webhook URL if body not used.", - null=True, - verbose_name="webhook parameters", - ), - ), - ( - "body", - models.TextField( - blank=True, - help_text="The body to send with the webhook URL if parameters not used.", - null=True, - verbose_name="webhook body", - ), - ), - ( - "headers", - models.JSONField( - blank=True, - help_text="The headers to send with the webhook URL.", - null=True, - verbose_name="webhook headers", - ), - ), - ( - "include_document", - models.BooleanField( - default=False, - verbose_name="include document in webhook", - ), - ), - ], - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[ - (1, "Assignment"), - (2, "Removal"), - (3, "Email"), - (4, "Webhook"), - ], - default=1, - verbose_name="Workflow Action Type", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="email", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="action", - to="documents.workflowactionemail", - verbose_name="email", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="webhook", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="action", - to="documents.workflowactionwebhook", - verbose_name="webhook", - ), - ), - ] diff --git a/src/documents/migrations/1060_alter_customfieldinstance_value_select.py b/src/documents/migrations/1060_alter_customfieldinstance_value_select.py deleted file mode 100644 index 21f3f8b41..000000000 --- a/src/documents/migrations/1060_alter_customfieldinstance_value_select.py +++ /dev/null @@ -1,79 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-13 05:14 - -from django.db import migrations -from django.db import models -from django.db import transaction -from django.utils.crypto import get_random_string - - -def migrate_customfield_selects(apps, schema_editor): - """ - Migrate the custom field selects from a simple list of strings to a list of dictionaries with - label and id. Then update all instances of the custom field to use the new format. - """ - CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance") - CustomField = apps.get_model("documents", "CustomField") - - with transaction.atomic(): - for custom_field in CustomField.objects.filter( - data_type="select", - ): # CustomField.FieldDataType.SELECT - old_select_options = custom_field.extra_data["select_options"] - custom_field.extra_data["select_options"] = [ - {"id": get_random_string(16), "label": value} - for value in old_select_options - ] - custom_field.save() - - for instance in CustomFieldInstance.objects.filter(field=custom_field): - if instance.value_select: - instance.value_select = custom_field.extra_data["select_options"][ - int(instance.value_select) - ]["id"] - instance.save() - - -def reverse_migrate_customfield_selects(apps, schema_editor): - """ - Reverse the migration of the custom field selects from a list of dictionaries with label and id - to a simple list of strings. Then update all instances of the custom field to use the old format, - which is just the index of the selected option. - """ - CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance") - CustomField = apps.get_model("documents", "CustomField") - - with transaction.atomic(): - for custom_field in CustomField.objects.all(): - if custom_field.data_type == "select": # CustomField.FieldDataType.SELECT - old_select_options = custom_field.extra_data["select_options"] - custom_field.extra_data["select_options"] = [ - option["label"] - for option in custom_field.extra_data["select_options"] - ] - custom_field.save() - - for instance in CustomFieldInstance.objects.filter(field=custom_field): - instance.value_select = next( - index - for index, option in enumerate(old_select_options) - if option.get("id") == instance.value_select - ) - instance.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1059_workflowactionemail_workflowactionwebhook_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_select", - field=models.CharField(max_length=16, null=True), - ), - migrations.RunPython( - migrate_customfield_selects, - reverse_migrate_customfield_selects, - ), - ] diff --git a/src/documents/migrations/1061_workflowactionwebhook_as_json.py b/src/documents/migrations/1061_workflowactionwebhook_as_json.py deleted file mode 100644 index f1945cfc1..000000000 --- a/src/documents/migrations/1061_workflowactionwebhook_as_json.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-18 19:35 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1060_alter_customfieldinstance_value_select"), - ] - - operations = [ - migrations.AddField( - model_name="workflowactionwebhook", - name="as_json", - field=models.BooleanField(default=False, verbose_name="send as JSON"), - ), - ] diff --git a/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index c5a6bb90e..000000000 --- a/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,70 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-06 05:54 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1061_workflowactionwebhook_as_json"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - (42, "custom fields query"), - (43, "created to"), - (44, "created from"), - (45, "added to"), - (46, "added from"), - (47, "mime type is"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py b/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py deleted file mode 100644 index aeedbd6a0..000000000 --- a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by Django 5.1.6 on 2025-02-21 16:34 - -import multiselectfield.db.fields -from django.db import migrations -from django.db import models - - -# WebUI source was added, so all existing APIUpload sources should be updated to include WebUI -def update_workflow_sources(apps, schema_editor): - WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger") - for trigger in WorkflowTrigger.objects.all(): - sources = list(trigger.sources) - if 2 in sources: - sources.append(4) - trigger.sources = sources - trigger.save() - - -def make_existing_tasks_consume_auto(apps, schema_editor): - PaperlessTask = apps.get_model("documents", "PaperlessTask") - PaperlessTask.objects.all().update(type="auto_task", task_name="consume_file") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1062_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AddField( - model_name="paperlesstask", - name="type", - field=models.CharField( - choices=[ - ("auto_task", "Auto Task"), - ("scheduled_task", "Scheduled Task"), - ("manual_task", "Manual Task"), - ], - default="auto_task", - help_text="The type of task that was run", - max_length=30, - verbose_name="Task Type", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - choices=[ - ("consume_file", "Consume File"), - ("train_classifier", "Train Classifier"), - ("check_sanity", "Check Sanity"), - ("index_optimize", "Index Optimize"), - ], - help_text="Name of the task that was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.RunPython( - code=make_existing_tasks_consume_auto, - reverse_code=migrations.RunPython.noop, - ), - migrations.AlterField( - model_name="workflowactionwebhook", - name="url", - field=models.CharField( - help_text="The destination URL for the notification.", - max_length=256, - verbose_name="webhook url", - ), - ), - migrations.AlterField( - model_name="workflowtrigger", - name="sources", - field=multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - (4, "Web UI"), - ], - default="1,2,3,4", - max_length=7, - ), - ), - migrations.RunPython( - code=update_workflow_sources, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1064_delete_log.py b/src/documents/migrations/1064_delete_log.py deleted file mode 100644 index ec0830a91..000000000 --- a/src/documents/migrations/1064_delete_log.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 5.1.6 on 2025-02-28 15:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), - ] - - operations = [ - migrations.DeleteModel( - name="Log", - ), - ] diff --git a/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py b/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py deleted file mode 100644 index 35fae02be..000000000 --- a/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-01 18:10 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1064_delete_log"), - ] - - operations = [ - migrations.AddField( - model_name="workflowaction", - name="assign_custom_fields_values", - field=models.JSONField( - blank=True, - help_text="Optional values to assign to the custom fields.", - null=True, - verbose_name="custom field values", - default=dict, - ), - ), - ] diff --git a/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py b/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py deleted file mode 100644 index eaf23ad64..000000000 --- a/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.7 on 2025-04-15 19:18 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1065_workflowaction_assign_custom_fields_values"), - ] - - operations = [ - migrations.AlterField( - model_name="workflowtrigger", - name="schedule_offset_days", - field=models.IntegerField( - default=0, - help_text="The number of days to offset the schedule trigger by.", - verbose_name="schedule offset days", - ), - ), - ] diff --git a/src/documents/migrations/1067_alter_document_created.py b/src/documents/migrations/1067_alter_document_created.py deleted file mode 100644 index 0f96bce3d..000000000 --- a/src/documents/migrations/1067_alter_document_created.py +++ /dev/null @@ -1,76 +0,0 @@ -# Generated by Django 5.1.7 on 2025-04-04 01:08 - - -import datetime - -from django.db import migrations -from django.db import models -from django.utils.timezone import localtime - - -def migrate_date(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - # Batch to avoid loading all objects into memory at once, - # which would be problematic for large datasets. - batch_size = 500 - updates = [] - total_updated = 0 - total_checked = 0 - - for doc in Document.objects.only("id", "created").iterator(chunk_size=batch_size): - total_checked += 1 - if doc.created: - doc.created_date = localtime(doc.created).date() - updates.append(doc) - - if len(updates) >= batch_size: - Document.objects.bulk_update(updates, ["created_date"]) - total_updated += len(updates) - print( - f"[1067_alter_document_created] {total_updated} of {total_checked} processed...", - ) - updates.clear() - - if updates: - Document.objects.bulk_update(updates, ["created_date"]) - total_updated += len(updates) - print( - f"[1067_alter_document_created] {total_updated} of {total_checked} processed...", - ) - - if total_checked > 0: - print(f"[1067_alter_document_created] completed for {total_checked} documents.") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1066_alter_workflowtrigger_schedule_offset_days"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="created_date", - field=models.DateField(null=True), - ), - migrations.RunPython(migrate_date, reverse_code=migrations.RunPython.noop), - migrations.RemoveField( - model_name="document", - name="created", - ), - migrations.RenameField( - model_name="document", - old_name="created_date", - new_name="created", - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateField( - db_index=True, - default=datetime.datetime.today, - verbose_name="created", - ), - ), - ] diff --git a/src/documents/migrations/1068_alter_document_created.py b/src/documents/migrations/1068_alter_document_created.py deleted file mode 100644 index b673f6584..000000000 --- a/src/documents/migrations/1068_alter_document_created.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.8 on 2025-05-23 05:50 - -import datetime - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1067_alter_document_created"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="created", - field=models.DateField( - db_index=True, - default=datetime.date.today, - verbose_name="created", - ), - ), - ] diff --git a/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py b/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py deleted file mode 100644 index 47db2fd91..000000000 --- a/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-11 17:29 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1068_alter_document_created"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="has this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_title", - field=models.TextField( - blank=True, - help_text="Assign a document title, must be a Jinja2 template, see documentation.", - null=True, - verbose_name="assign title", - ), - ), - ] diff --git a/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py b/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py deleted file mode 100644 index 69c77d29a..000000000 --- a/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-13 17:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1069_workflowtrigger_filter_has_storage_path_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="value_long_text", - field=models.TextField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ("select", "Select"), - ("longtext", "Long Text"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py b/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py deleted file mode 100644 index 3e097620e..000000000 --- a/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-12 18:42 - -import django.core.validators -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1070_customfieldinstance_value_long_text_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="tag", - name="tn_ancestors_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Ancestors count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_ancestors_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Ancestors pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_children_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Children count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_children_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Children pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_depth", - field=models.PositiveIntegerField( - default=0, - editable=False, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(10), - ], - verbose_name="Depth", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_descendants_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Descendants count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_descendants_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Descendants pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_index", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Index", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_level", - field=models.PositiveIntegerField( - default=1, - editable=False, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(10), - ], - verbose_name="Level", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_order", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Order", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_parent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="tn_children", - to="documents.tag", - verbose_name="Parent", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_priority", - field=models.PositiveIntegerField( - default=0, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(9999999999), - ], - verbose_name="Priority", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_siblings_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Siblings count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_siblings_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Siblings pks", - ), - ), - ] diff --git a/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py b/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py deleted file mode 100644 index 1a22f6b4f..000000000 --- a/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py +++ /dev/null @@ -1,73 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-07 18:52 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="filter_custom_field_query", - field=models.TextField( - blank=True, - help_text="JSON-encoded custom field query expression.", - null=True, - verbose_name="filter custom field query", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_all_tags", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_all", - to="documents.tag", - verbose_name="has all of these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_correspondent", - to="documents.correspondent", - verbose_name="does not have these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_document_types", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_document_type", - to="documents.documenttype", - verbose_name="does not have these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_storage_path", - to="documents.storagepath", - verbose_name="does not have these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_tags", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not", - to="documents.tag", - verbose_name="does not have these tag(s)", - ), - ), - ] diff --git a/src/documents/migrations/1073_migrate_workflow_title_jinja.py b/src/documents/migrations/1073_migrate_workflow_title_jinja.py deleted file mode 100644 index 9d80a277f..000000000 --- a/src/documents/migrations/1073_migrate_workflow_title_jinja.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.2.5 on 2025-08-27 22:02 -import logging - -from django.db import migrations -from django.db import models - -from documents.templating.utils import convert_format_str_to_template_format - -logger = logging.getLogger("paperless.migrations") - - -def convert_from_format_to_template(apps, schema_editor): - WorkflowAction = apps.get_model("documents", "WorkflowAction") - - batch_size = 500 - actions_to_update = [] - - queryset = ( - WorkflowAction.objects.filter(assign_title__isnull=False) - .exclude(assign_title="") - .only("id", "assign_title") - ) - - for action in queryset: - action.assign_title = convert_format_str_to_template_format( - action.assign_title, - ) - logger.debug( - "Converted WorkflowAction id %d title to template format: %s", - action.id, - action.assign_title, - ) - actions_to_update.append(action) - - if actions_to_update: - WorkflowAction.objects.bulk_update( - actions_to_update, - ["assign_title"], - batch_size=batch_size, - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1072_workflowtrigger_filter_custom_field_query_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="workflowaction", - name="assign_title", - field=models.TextField( - blank=True, - help_text="Assign a document title, must be a Jinja2 template, see documentation.", - null=True, - verbose_name="assign title", - ), - ), - migrations.RunPython( - convert_from_format_to_template, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py b/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py deleted file mode 100644 index 4381eabb1..000000000 --- a/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 15:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1073_migrate_workflow_title_jinja"), - ] - - operations = [ - migrations.AddField( - model_name="workflowrun", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowrun", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowrun", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1075_workflowaction_order.py b/src/documents/migrations/1075_workflowaction_order.py deleted file mode 100644 index f7101bf7e..000000000 --- a/src/documents/migrations/1075_workflowaction_order.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.7 on 2026-01-14 16:53 - -from django.db import migrations -from django.db import models -from django.db.models import F - - -def populate_action_order(apps, schema_editor): - WorkflowAction = apps.get_model("documents", "WorkflowAction") - WorkflowAction.objects.all().update(order=F("id")) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="workflowaction", - name="order", - field=models.PositiveIntegerField(default=0, verbose_name="order"), - ), - migrations.RunPython( - populate_action_order, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1076_alter_paperlesstask_task_name.py b/src/documents/migrations/1076_alter_paperlesstask_task_name.py deleted file mode 100644 index bb5406255..000000000 --- a/src/documents/migrations/1076_alter_paperlesstask_task_name.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.1.8 on 2025-04-30 02:38 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1075_workflowaction_order"), - ] - - operations = [ - migrations.AlterField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - choices=[ - ("consume_file", "Consume File"), - ("train_classifier", "Train Classifier"), - ("check_sanity", "Check Sanity"), - ("index_optimize", "Index Optimize"), - ("llmindex_update", "LLM Index Update"), - ], - help_text="Name of the task that was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - ] diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py index 1700366d8..8e29c53d2 100644 --- a/src/documents/tests/test_api_status.py +++ b/src/documents/tests/test_api_status.py @@ -1,4 +1,6 @@ import os +import shutil +import tempfile from pathlib import Path from unittest import mock @@ -16,9 +18,19 @@ class TestSystemStatus(APITestCase): ENDPOINT = "/api/status/" def setUp(self): + super().setUp() self.user = User.objects.create_superuser( username="temp_admin", ) + self.tmp_dir = Path(tempfile.mkdtemp()) + self.override = override_settings(MEDIA_ROOT=self.tmp_dir) + self.override.enable() + + def tearDown(self): + super().tearDown() + + self.override.disable() + shutil.rmtree(self.tmp_dir) def test_system_status(self): """ diff --git a/src/documents/tests/test_management_superuser.py b/src/documents/tests/test_management_superuser.py index 01f03c8e1..343d5f568 100644 --- a/src/documents/tests/test_management_superuser.py +++ b/src/documents/tests/test_management_superuser.py @@ -33,8 +33,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): # just the consumer user which is created # during migration, and AnonymousUser - self.assertEqual(User.objects.count(), 2) - self.assertTrue(User.objects.filter(username="consumer").exists()) + self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.filter(is_superuser=True).count(), 0) self.assertEqual( out, @@ -54,7 +53,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): # count is 3 as there's the consumer # user already created during migration, and AnonymousUser user: User = User.objects.get_by_natural_key("admin") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "root@localhost") self.assertEqual(out, 'Created superuser "admin" with provided password.\n') @@ -71,7 +70,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) with self.assertRaises(User.DoesNotExist): User.objects.get_by_natural_key("admin") self.assertEqual( @@ -92,7 +91,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) user: User = User.objects.get_by_natural_key("admin") self.assertTrue(user.check_password("password")) self.assertEqual(out, "Did not create superuser, a user admin already exists\n") @@ -111,7 +110,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) user: User = User.objects.get_by_natural_key("admin") self.assertTrue(user.check_password("password")) self.assertFalse(user.is_superuser) @@ -150,7 +149,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): ) user: User = User.objects.get_by_natural_key("admin") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "hello@world.com") self.assertEqual(user.username, "admin") @@ -174,7 +173,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): ) user: User = User.objects.get_by_natural_key("super") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "hello@world.com") self.assertEqual(user.username, "super") diff --git a/src/documents/tests/test_migration_archive_files.py b/src/documents/tests/test_migration_archive_files.py deleted file mode 100644 index a2e8a5f8f..000000000 --- a/src/documents/tests/test_migration_archive_files.py +++ /dev/null @@ -1,574 +0,0 @@ -import hashlib -import importlib -import shutil -from pathlib import Path -from unittest import mock - -import pytest -from django.conf import settings -from django.test import override_settings - -from documents.parsers import ParseError -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import FileSystemAssertsMixin -from documents.tests.utils import TestMigrations - -STORAGE_TYPE_GPG = "gpg" - -migration_1012_obj = importlib.import_module( - "documents.migrations.1012_fix_archive_files", -) - - -def archive_name_from_filename(filename: Path) -> Path: - return Path(filename.stem + ".pdf") - - -def archive_path_old(self) -> Path: - if self.filename: - fname = archive_name_from_filename(Path(self.filename)) - else: - fname = Path(f"{self.pk:07}.pdf") - - return Path(settings.ARCHIVE_DIR) / fname - - -def archive_path_new(doc): - if doc.archive_filename is not None: - return Path(settings.ARCHIVE_DIR) / str(doc.archive_filename) - else: - return None - - -def source_path(doc): - if doc.filename: - fname = str(doc.filename) - else: - fname = f"{doc.pk:07}{doc.file_type}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover - - return Path(settings.ORIGINALS_DIR) / fname - - -def thumbnail_path(doc): - file_name = f"{doc.pk:07}.png" - if doc.storage_type == STORAGE_TYPE_GPG: - file_name += ".gpg" - - return Path(settings.THUMBNAIL_DIR) / file_name - - -def make_test_document( - document_class, - title: str, - mime_type: str, - original: str, - original_filename: str, - archive: str | None = None, - archive_filename: str | None = None, -): - doc = document_class() - doc.filename = original_filename - doc.title = title - doc.mime_type = mime_type - doc.content = "the content, does not matter for this test" - doc.save() - - shutil.copy2(original, source_path(doc)) - with Path(original).open("rb") as f: - doc.checksum = hashlib.md5(f.read()).hexdigest() - - if archive: - if archive_filename: - doc.archive_filename = archive_filename - shutil.copy2(archive, archive_path_new(doc)) - else: - shutil.copy2(archive, archive_path_old(doc)) - - with Path(archive).open("rb") as f: - doc.archive_checksum = hashlib.md5(f.read()).hexdigest() - - doc.save() - - Path(thumbnail_path(doc)).touch() - - return doc - - -simple_jpg = Path(__file__).parent / "samples" / "simple.jpg" -simple_pdf = Path(__file__).parent / "samples" / "simple.pdf" -simple_pdf2 = ( - Path(__file__).parent / "samples" / "documents" / "originals" / "0000002.pdf" -) -simple_pdf3 = ( - Path(__file__).parent / "samples" / "documents" / "originals" / "0000003.pdf" -) -simple_txt = Path(__file__).parent / "samples" / "simple.txt" -simple_png = Path(__file__).parent / "samples" / "simple-noalpha.png" -simple_png2 = Path(__file__).parent / "examples" / "no-text.png" - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFiles(DirectoriesMixin, FileSystemAssertsMixin, TestMigrations): - migrate_from = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - migrate_to = "1012_fix_archive_files" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - - self.unrelated = make_test_document( - Document, - "unrelated", - "application/pdf", - simple_pdf3, - "unrelated.pdf", - simple_pdf, - ) - self.no_text = make_test_document( - Document, - "no-text", - "image/png", - simple_png2, - "no-text.png", - simple_pdf, - ) - self.doc_no_archive = make_test_document( - Document, - "no_archive", - "text/plain", - simple_txt, - "no_archive.txt", - ) - self.clash1 = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - ) - self.clash2 = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - ) - self.clash3 = make_test_document( - Document, - "clash", - "image/png", - simple_png, - "clash.png", - simple_pdf, - ) - self.clash4 = make_test_document( - Document, - "clash.png", - "application/pdf", - simple_pdf2, - "clash.png.pdf", - simple_pdf2, - ) - - self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash2)) - self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash3)) - self.assertNotEqual( - archive_path_old(self.clash1), - archive_path_old(self.clash4), - ) - - def testArchiveFilesMigrated(self): - Document = self.apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - if doc.archive_checksum: - self.assertIsNotNone(doc.archive_filename) - self.assertIsFile(archive_path_new(doc)) - else: - self.assertIsNone(doc.archive_filename) - - with Path(source_path(doc)).open("rb") as f: - original_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(original_checksum, doc.checksum) - - if doc.archive_checksum: - self.assertIsFile(archive_path_new(doc)) - with archive_path_new(doc).open("rb") as f: - archive_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(archive_checksum, doc.archive_checksum) - - self.assertEqual( - Document.objects.filter(archive_checksum__isnull=False).count(), - 6, - ) - - def test_filenames(self): - Document = self.apps.get_model("documents", "Document") - self.assertEqual( - Document.objects.get(id=self.unrelated.id).archive_filename, - "unrelated.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.no_text.id).archive_filename, - "no-text.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.doc_no_archive.id).archive_filename, - None, - ) - self.assertEqual( - Document.objects.get(id=self.clash1.id).archive_filename, - f"{self.clash1.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash2.id).archive_filename, - f"{self.clash2.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash3.id).archive_filename, - f"{self.clash3.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash4.id).archive_filename, - "clash.png.pdf", - ) - - -@override_settings(FILENAME_FORMAT="{correspondent}/{title}") -class TestMigrateArchiveFilesWithFilenameFormat(TestMigrateArchiveFiles): - def test_filenames(self): - Document = self.apps.get_model("documents", "Document") - self.assertEqual( - Document.objects.get(id=self.unrelated.id).archive_filename, - "unrelated.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.no_text.id).archive_filename, - "no-text.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.doc_no_archive.id).archive_filename, - None, - ) - self.assertEqual( - Document.objects.get(id=self.clash1.id).archive_filename, - "none/clash.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash2.id).archive_filename, - "none/clash_01.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash3.id).archive_filename, - "none/clash_02.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash4.id).archive_filename, - "clash.png.pdf", - ) - - -def fake_parse_wrapper(parser, path, mime_type, file_name): - parser.archive_path = None - parser.text = "the text" - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesErrors(DirectoriesMixin, TestMigrations): - migrate_from = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - migrate_to = "1012_fix_archive_files" - auto_migrate = False - - @pytest.mark.skip(reason="Fails with migration tearDown util. Needs investigation.") - def test_archive_missing(self): - Document = self.apps.get_model("documents", "Document") - - doc = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - ) - archive_path_old(doc).unlink() - - self.assertRaisesMessage( - ValueError, - "does not exist at: ", - self.performMigration, - ) - - @pytest.mark.skip(reason="Fails with migration tearDown util. Needs investigation.") - def test_parser_missing(self): - Document = self.apps.get_model("documents", "Document") - - make_test_document( - Document, - "document", - "invalid/typesss768", - simple_png, - "document.png", - simple_pdf, - ) - make_test_document( - Document, - "document", - "invalid/typesss768", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - self.assertRaisesMessage( - ValueError, - "no parsers are available", - self.performMigration, - ) - - @mock.patch(f"{__name__}.migration_1012_obj.parse_wrapper") - def test_parser_error(self, m): - m.side_effect = ParseError() - Document = self.apps.get_model("documents", "Document") - - doc1 = make_test_document( - Document, - "document", - "image/png", - simple_png, - "document.png", - simple_pdf, - ) - doc2 = make_test_document( - Document, - "document", - "application/pdf", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - self.assertIsNotNone(doc1.archive_checksum) - self.assertIsNotNone(doc2.archive_checksum) - - with self.assertLogs() as capture: - self.performMigration() - - self.assertEqual(m.call_count, 6) - - self.assertEqual( - len( - list( - filter( - lambda log: "Parse error, will try again in 5 seconds" in log, - capture.output, - ), - ), - ), - 4, - ) - - self.assertEqual( - len( - list( - filter( - lambda log: "Unable to regenerate archive document for ID:" - in log, - capture.output, - ), - ), - ), - 2, - ) - - Document = self.apps.get_model("documents", "Document") - - doc1 = Document.objects.get(id=doc1.id) - doc2 = Document.objects.get(id=doc2.id) - - self.assertIsNone(doc1.archive_checksum) - self.assertIsNone(doc2.archive_checksum) - self.assertIsNone(doc1.archive_filename) - self.assertIsNone(doc2.archive_filename) - - @mock.patch(f"{__name__}.migration_1012_obj.parse_wrapper") - def test_parser_no_archive(self, m): - m.side_effect = fake_parse_wrapper - - Document = self.apps.get_model("documents", "Document") - - doc1 = make_test_document( - Document, - "document", - "image/png", - simple_png, - "document.png", - simple_pdf, - ) - doc2 = make_test_document( - Document, - "document", - "application/pdf", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - with self.assertLogs() as capture: - self.performMigration() - - self.assertEqual( - len( - list( - filter( - lambda log: "Parser did not return an archive document for document" - in log, - capture.output, - ), - ), - ), - 2, - ) - - Document = self.apps.get_model("documents", "Document") - - doc1 = Document.objects.get(id=doc1.id) - doc2 = Document.objects.get(id=doc2.id) - - self.assertIsNone(doc1.archive_checksum) - self.assertIsNone(doc2.archive_checksum) - self.assertIsNone(doc1.archive_filename) - self.assertIsNone(doc2.archive_filename) - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesBackwards( - DirectoriesMixin, - FileSystemAssertsMixin, - TestMigrations, -): - migrate_from = "1012_fix_archive_files" - migrate_to = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - - make_test_document( - Document, - "unrelated", - "application/pdf", - simple_pdf2, - "unrelated.txt", - simple_pdf2, - "unrelated.pdf", - ) - make_test_document( - Document, - "no_archive", - "text/plain", - simple_txt, - "no_archive.txt", - ) - make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_02.pdf", - ) - - def testArchiveFilesReverted(self): - Document = self.apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - if doc.archive_checksum: - self.assertIsFile(archive_path_old(doc)) - with Path(source_path(doc)).open("rb") as f: - original_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(original_checksum, doc.checksum) - - if doc.archive_checksum: - self.assertIsFile(archive_path_old(doc)) - with archive_path_old(doc).open("rb") as f: - archive_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(archive_checksum, doc.archive_checksum) - - self.assertEqual( - Document.objects.filter(archive_checksum__isnull=False).count(), - 2, - ) - - -@override_settings(FILENAME_FORMAT="{correspondent}/{title}") -class TestMigrateArchiveFilesBackwardsWithFilenameFormat( - TestMigrateArchiveFilesBackwards, -): - pass - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesBackwardsErrors(DirectoriesMixin, TestMigrations): - migrate_from = "1012_fix_archive_files" - migrate_to = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - auto_migrate = False - - def test_filename_clash(self): - Document = self.apps.get_model("documents", "Document") - - self.clashA = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - "clash_02.pdf", - ) - self.clashB = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_01.pdf", - ) - - self.assertRaisesMessage( - ValueError, - "would clash with another archive filename", - self.performMigration, - ) - - def test_filename_exists(self): - Document = self.apps.get_model("documents", "Document") - - self.clashA = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - "clash.pdf", - ) - self.clashB = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_01.pdf", - ) - - self.assertRaisesMessage( - ValueError, - "file already exists.", - self.performMigration, - ) diff --git a/src/documents/tests/test_migration_consumption_templates.py b/src/documents/tests/test_migration_consumption_templates.py deleted file mode 100644 index 917007116..000000000 --- a/src/documents/tests/test_migration_consumption_templates.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.contrib.auth import get_user_model - -from documents.tests.utils import TestMigrations - - -class TestMigrateConsumptionTemplate(TestMigrations): - migrate_from = "1038_sharelink" - migrate_to = "1039_consumptiontemplate" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_users_with_add_documents_get_add_consumptiontemplate(self): - permission = self.Permission.objects.get(codename="add_consumptiontemplate") - self.assertTrue(self.user.has_perm(f"documents.{permission.codename}")) - self.assertTrue(permission in self.group.permissions.all()) - - -class TestReverseMigrateConsumptionTemplate(TestMigrations): - migrate_from = "1039_consumptiontemplate" - migrate_to = "1038_sharelink" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.filter( - codename="add_consumptiontemplate", - ).first() - if permission is not None: - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_remove_consumptiontemplate_permissions(self): - permission = self.Permission.objects.filter( - codename="add_consumptiontemplate", - ).first() - # can be None ? now that CTs removed - if permission is not None: - self.assertFalse(self.user.has_perm(f"documents.{permission.codename}")) - self.assertFalse(permission in self.group.permissions.all()) diff --git a/src/documents/tests/test_migration_created.py b/src/documents/tests/test_migration_created.py deleted file mode 100644 index 89e97cbe1..000000000 --- a/src/documents/tests/test_migration_created.py +++ /dev/null @@ -1,33 +0,0 @@ -from datetime import date -from datetime import datetime -from datetime import timedelta - -from django.utils.timezone import make_aware -from pytz import UTC - -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateDocumentCreated(DirectoriesMixin, TestMigrations): - migrate_from = "1066_alter_workflowtrigger_schedule_offset_days" - migrate_to = "1067_alter_document_created" - - def setUpBeforeMigration(self, apps): - # create 600 documents - for i in range(600): - Document = apps.get_model("documents", "Document") - naive = datetime(2023, 10, 1, 12, 0, 0) + timedelta(days=i) - Document.objects.create( - title=f"test{i}", - mime_type="application/pdf", - filename=f"file{i}.pdf", - created=make_aware(naive, timezone=UTC), - checksum=i, - ) - - def testDocumentCreatedMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=1) - self.assertEqual(doc.created, date(2023, 10, 1)) diff --git a/src/documents/tests/test_migration_custom_field_selects.py b/src/documents/tests/test_migration_custom_field_selects.py deleted file mode 100644 index 59004bf21..000000000 --- a/src/documents/tests/test_migration_custom_field_selects.py +++ /dev/null @@ -1,87 +0,0 @@ -from unittest.mock import ANY - -from documents.tests.utils import TestMigrations - - -class TestMigrateCustomFieldSelects(TestMigrations): - migrate_from = "1059_workflowactionemail_workflowactionwebhook_and_more" - migrate_to = "1060_alter_customfieldinstance_value_select" - - def setUpBeforeMigration(self, apps): - CustomField = apps.get_model("documents.CustomField") - self.old_format = CustomField.objects.create( - name="cf1", - data_type="select", - extra_data={"select_options": ["Option 1", "Option 2", "Option 3"]}, - ) - Document = apps.get_model("documents.Document") - doc = Document.objects.create(title="doc1") - CustomFieldInstance = apps.get_model("documents.CustomFieldInstance") - self.old_instance = CustomFieldInstance.objects.create( - field=self.old_format, - value_select=0, - document=doc, - ) - - def test_migrate_old_to_new_select_fields(self): - self.old_format.refresh_from_db() - self.old_instance.refresh_from_db() - - self.assertEqual( - self.old_format.extra_data["select_options"], - [ - {"label": "Option 1", "id": ANY}, - {"label": "Option 2", "id": ANY}, - {"label": "Option 3", "id": ANY}, - ], - ) - - self.assertEqual( - self.old_instance.value_select, - self.old_format.extra_data["select_options"][0]["id"], - ) - - -class TestMigrationCustomFieldSelectsReverse(TestMigrations): - migrate_from = "1060_alter_customfieldinstance_value_select" - migrate_to = "1059_workflowactionemail_workflowactionwebhook_and_more" - - def setUpBeforeMigration(self, apps): - CustomField = apps.get_model("documents.CustomField") - self.new_format = CustomField.objects.create( - name="cf1", - data_type="select", - extra_data={ - "select_options": [ - {"label": "Option 1", "id": "id1"}, - {"label": "Option 2", "id": "id2"}, - {"label": "Option 3", "id": "id3"}, - ], - }, - ) - Document = apps.get_model("documents.Document") - doc = Document.objects.create(title="doc1") - CustomFieldInstance = apps.get_model("documents.CustomFieldInstance") - self.new_instance = CustomFieldInstance.objects.create( - field=self.new_format, - value_select="id1", - document=doc, - ) - - def test_migrate_new_to_old_select_fields(self): - self.new_format.refresh_from_db() - self.new_instance.refresh_from_db() - - self.assertEqual( - self.new_format.extra_data["select_options"], - [ - "Option 1", - "Option 2", - "Option 3", - ], - ) - - self.assertEqual( - self.new_instance.value_select, - 0, - ) diff --git a/src/documents/tests/test_migration_customfields.py b/src/documents/tests/test_migration_customfields.py deleted file mode 100644 index 79308bceb..000000000 --- a/src/documents/tests/test_migration_customfields.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib.auth import get_user_model - -from documents.tests.utils import TestMigrations - - -class TestMigrateCustomFields(TestMigrations): - migrate_from = "1039_consumptiontemplate" - migrate_to = "1040_customfield_customfieldinstance_and_more" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_users_with_add_documents_get_add_customfields(self): - permission = self.Permission.objects.get(codename="add_customfield") - self.assertTrue(self.user.has_perm(f"documents.{permission.codename}")) - self.assertTrue(permission in self.group.permissions.all()) - - -class TestReverseMigrateCustomFields(TestMigrations): - migrate_from = "1040_customfield_customfieldinstance_and_more" - migrate_to = "1039_consumptiontemplate" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_customfield") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_remove_consumptiontemplate_permissions(self): - permission = self.Permission.objects.get(codename="add_customfield") - self.assertFalse(self.user.has_perm(f"documents.{permission.codename}")) - self.assertFalse(permission in self.group.permissions.all()) diff --git a/src/documents/tests/test_migration_document_pages_count.py b/src/documents/tests/test_migration_document_pages_count.py deleted file mode 100644 index e8f297acb..000000000 --- a/src/documents/tests/test_migration_document_pages_count.py +++ /dev/null @@ -1,59 +0,0 @@ -import shutil -from pathlib import Path - -from django.conf import settings - -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -def source_path_before(self) -> Path: - if self.filename: - fname = str(self.filename) - - return Path(settings.ORIGINALS_DIR) / fname - - -class TestMigrateDocumentPageCount(DirectoriesMixin, TestMigrations): - migrate_from = "1052_document_transaction_id" - migrate_to = "1053_document_page_count" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test1", - mime_type="application/pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_before(doc), - ) - - def testDocumentPageCountMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.page_count, 1) - - -class TestMigrateDocumentPageCountBackwards(TestMigrations): - migrate_from = "1053_document_page_count" - migrate_to = "1052_document_transaction_id" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test1", - mime_type="application/pdf", - filename="file1.pdf", - page_count=8, - ) - self.doc_id = doc.id - - def test_remove_number_of_pages_to_page_count(self): - Document = self.apps.get_model("documents", "Document") - self.assertFalse( - "page_count" in [field.name for field in Document._meta.get_fields()], - ) diff --git a/src/documents/tests/test_migration_encrypted_webp_conversion.py b/src/documents/tests/test_migration_encrypted_webp_conversion.py deleted file mode 100644 index 0660df368..000000000 --- a/src/documents/tests/test_migration_encrypted_webp_conversion.py +++ /dev/null @@ -1,283 +0,0 @@ -import importlib -import shutil -import tempfile -from collections.abc import Callable -from collections.abc import Iterable -from pathlib import Path -from unittest import mock - -from django.test import override_settings - -from documents.tests.utils import TestMigrations - -# https://github.com/python/cpython/issues/100950 -migration_1037_obj = importlib.import_module( - "documents.migrations.1037_webp_encrypted_thumbnail_conversion", -) - - -@override_settings(PASSPHRASE="test") -@mock.patch( - f"{__name__}.migration_1037_obj.multiprocessing.pool.Pool.map", -) -@mock.patch(f"{__name__}.migration_1037_obj.run_convert") -class TestMigrateToEncrytpedWebPThumbnails(TestMigrations): - migrate_from = ( - "1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type" - ) - migrate_to = "1037_webp_encrypted_thumbnail_conversion" - auto_migrate = False - - def pretend_convert_output(self, *args, **kwargs): - """ - Pretends to do the conversion, by copying the input file - to the output file - """ - shutil.copy2( - Path(kwargs["input_file"].rstrip("[0]")), - Path(kwargs["output_file"]), - ) - - def pretend_map(self, func: Callable, iterable: Iterable): - """ - Pretends to be the map of a multiprocessing.Pool, but secretly does - everything in series - """ - for item in iterable: - func(item) - - def create_dummy_thumbnails( - self, - thumb_dir: Path, - ext: str, - count: int, - start_count: int = 0, - ): - """ - Helper to create a certain count of files of given extension in a given directory - """ - for idx in range(count): - (Path(thumb_dir) / Path(f"{start_count + idx:07}.{ext}")).touch() - # Triple check expected files exist - self.assert_file_count_by_extension(ext, thumb_dir, count) - - def create_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp", count, start_count) - - def create_encrypted_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy encrypted WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp.gpg", count, start_count) - - def create_png_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy PNG thumbnail file in the given directory, based on - the database Document - """ - - self.create_dummy_thumbnails(thumb_dir, "png", count, start_count) - - def create_encrypted_png_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy encrypted PNG thumbnail file in the given directory, based on - the database Document - """ - - self.create_dummy_thumbnails(thumb_dir, "png.gpg", count, start_count) - - def assert_file_count_by_extension( - self, - ext: str, - dir: str | Path, - expected_count: int, - ): - """ - Helper to assert a certain count of given extension files in given directory - """ - if not isinstance(dir, Path): - dir = Path(dir) - matching_files = list(dir.glob(f"*.{ext}")) - self.assertEqual(len(matching_files), expected_count) - - def assert_encrypted_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of excrypted PNG extension files in given directory - """ - self.assert_file_count_by_extension("png.gpg", dir, expected_count) - - def assert_encrypted_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of encrypted WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp.gpg", dir, expected_count) - - def assert_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp", dir, expected_count) - - def assert_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of PNG extension files in given directory - """ - self.assert_file_count_by_extension("png", dir, expected_count) - - def setUp(self): - self.thumbnail_dir = Path(tempfile.mkdtemp()).resolve() - - return super().setUp() - - def tearDown(self) -> None: - shutil.rmtree(self.thumbnail_dir) - - return super().tearDown() - - def test_do_nothing_if_converted( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted document exists with existing encrypted WebP thumbnail path - WHEN: - - Migration is attempted - THEN: - - Nothing is converted - """ - map_mock.side_effect = self.pretend_map - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_webp_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - run_convert_mock.assert_not_called() - - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_thumbnails( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted documents exist with PNG thumbnail - WHEN: - - Migration is attempted - THEN: - - Thumbnails are converted to webp & re-encrypted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_png_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_errors_out( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted document exists with PNG thumbnail - WHEN: - - Migration is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = OSError - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_png_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_encrypted_png_file_count(self.thumbnail_dir, 3) - - def test_convert_mixed( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Documents exist with PNG, encrypted PNG and WebP thumbnails - WHEN: - - Migration is attempted - THEN: - - Only encrypted PNG thumbnails are converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_files(self.thumbnail_dir, 3) - self.create_encrypted_png_thumbnail_files( - self.thumbnail_dir, - 3, - start_count=3, - ) - self.create_webp_thumbnail_files(self.thumbnail_dir, 2, start_count=6) - self.create_encrypted_webp_thumbnail_files( - self.thumbnail_dir, - 3, - start_count=8, - ) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 3) - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 6) - self.assert_webp_file_count(self.thumbnail_dir, 2) - self.assert_encrypted_png_file_count(self.thumbnail_dir, 0) diff --git a/src/documents/tests/test_migration_mime_type.py b/src/documents/tests/test_migration_mime_type.py deleted file mode 100644 index 7805799fe..000000000 --- a/src/documents/tests/test_migration_mime_type.py +++ /dev/null @@ -1,108 +0,0 @@ -import shutil -from pathlib import Path - -from django.conf import settings -from django.test import override_settings - -from documents.parsers import get_default_file_extension -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - -STORAGE_TYPE_UNENCRYPTED = "unencrypted" -STORAGE_TYPE_GPG = "gpg" - - -def source_path_before(self): - if self.filename: - fname = str(self.filename) - else: - fname = f"{self.pk:07}.{self.file_type}" - if self.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" - - return Path(settings.ORIGINALS_DIR) / fname - - -def file_type_after(self): - return get_default_file_extension(self.mime_type) - - -def source_path_after(doc): - if doc.filename: - fname = str(doc.filename) - else: - fname = f"{doc.pk:07}{file_type_after(doc)}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover - - return Path(settings.ORIGINALS_DIR) / fname - - -@override_settings(PASSPHRASE="test") -class TestMigrateMimeType(DirectoriesMixin, TestMigrations): - migrate_from = "1002_auto_20201111_1105" - migrate_to = "1003_mime_types" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test", - file_type="pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_before(doc), - ) - - doc2 = Document.objects.create( - checksum="B", - file_type="pdf", - storage_type=STORAGE_TYPE_GPG, - ) - self.doc2_id = doc2.id - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "originals" - / "0000004.pdf.gpg" - ), - source_path_before(doc2), - ) - - def testMimeTypesMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.mime_type, "application/pdf") - - doc2 = Document.objects.get(id=self.doc2_id) - self.assertEqual(doc2.mime_type, "application/pdf") - - -@override_settings(PASSPHRASE="test") -class TestMigrateMimeTypeBackwards(DirectoriesMixin, TestMigrations): - migrate_from = "1003_mime_types" - migrate_to = "1002_auto_20201111_1105" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_after(doc), - ) - - def testMimeTypesReverted(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.file_type, "pdf") diff --git a/src/documents/tests/test_migration_remove_null_characters.py b/src/documents/tests/test_migration_remove_null_characters.py deleted file mode 100644 index c47bc80ca..000000000 --- a/src/documents/tests/test_migration_remove_null_characters.py +++ /dev/null @@ -1,15 +0,0 @@ -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateNullCharacters(DirectoriesMixin, TestMigrations): - migrate_from = "1014_auto_20210228_1614" - migrate_to = "1015_remove_null_characters" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - self.doc = Document.objects.create(content="aaa\0bbb") - - def testMimeTypesMigrated(self): - Document = self.apps.get_model("documents", "Document") - self.assertNotIn("\0", Document.objects.get(id=self.doc.id).content) diff --git a/src/documents/tests/test_migration_storage_path_template.py b/src/documents/tests/test_migration_storage_path_template.py deleted file mode 100644 index 37b87a115..000000000 --- a/src/documents/tests/test_migration_storage_path_template.py +++ /dev/null @@ -1,30 +0,0 @@ -from documents.models import StoragePath -from documents.tests.utils import TestMigrations - - -class TestMigrateStoragePathToTemplate(TestMigrations): - migrate_from = "1054_customfieldinstance_value_monetary_amount_and_more" - migrate_to = "1055_alter_storagepath_path" - - def setUpBeforeMigration(self, apps): - self.old_format = StoragePath.objects.create( - name="sp1", - path="Something/{title}", - ) - self.new_format = StoragePath.objects.create( - name="sp2", - path="{{asn}}/{{title}}", - ) - self.no_formatting = StoragePath.objects.create( - name="sp3", - path="Some/Fixed/Path", - ) - - def test_migrate_old_to_new_storage_path(self): - self.old_format.refresh_from_db() - self.new_format.refresh_from_db() - self.no_formatting.refresh_from_db() - - self.assertEqual(self.old_format.path, "Something/{{ title }}") - self.assertEqual(self.new_format.path, "{{asn}}/{{title}}") - self.assertEqual(self.no_formatting.path, "Some/Fixed/Path") diff --git a/src/documents/tests/test_migration_tag_colors.py b/src/documents/tests/test_migration_tag_colors.py deleted file mode 100644 index 0643fe883..000000000 --- a/src/documents/tests/test_migration_tag_colors.py +++ /dev/null @@ -1,36 +0,0 @@ -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateTagColor(DirectoriesMixin, TestMigrations): - migrate_from = "1012_fix_archive_files" - migrate_to = "1013_migrate_tag_colour" - - def setUpBeforeMigration(self, apps): - Tag = apps.get_model("documents", "Tag") - self.t1_id = Tag.objects.create(name="tag1").id - self.t2_id = Tag.objects.create(name="tag2", colour=1).id - self.t3_id = Tag.objects.create(name="tag3", colour=5).id - - def testMimeTypesMigrated(self): - Tag = self.apps.get_model("documents", "Tag") - self.assertEqual(Tag.objects.get(id=self.t1_id).color, "#a6cee3") - self.assertEqual(Tag.objects.get(id=self.t2_id).color, "#a6cee3") - self.assertEqual(Tag.objects.get(id=self.t3_id).color, "#fb9a99") - - -class TestMigrateTagColorBackwards(DirectoriesMixin, TestMigrations): - migrate_from = "1013_migrate_tag_colour" - migrate_to = "1012_fix_archive_files" - - def setUpBeforeMigration(self, apps): - Tag = apps.get_model("documents", "Tag") - self.t1_id = Tag.objects.create(name="tag1").id - self.t2_id = Tag.objects.create(name="tag2", color="#cab2d6").id - self.t3_id = Tag.objects.create(name="tag3", color="#123456").id - - def testMimeTypesReverted(self): - Tag = self.apps.get_model("documents", "Tag") - self.assertEqual(Tag.objects.get(id=self.t1_id).colour, 1) - self.assertEqual(Tag.objects.get(id=self.t2_id).colour, 9) - self.assertEqual(Tag.objects.get(id=self.t3_id).colour, 1) diff --git a/src/documents/tests/test_migration_webp_conversion.py b/src/documents/tests/test_migration_webp_conversion.py deleted file mode 100644 index cd148ed6f..000000000 --- a/src/documents/tests/test_migration_webp_conversion.py +++ /dev/null @@ -1,230 +0,0 @@ -import importlib -import shutil -import tempfile -from collections.abc import Callable -from collections.abc import Iterable -from pathlib import Path -from unittest import mock - -from django.test import override_settings - -from documents.tests.utils import TestMigrations - -# https://github.com/python/cpython/issues/100950 -migration_1021_obj = importlib.import_module( - "documents.migrations.1021_webp_thumbnail_conversion", -) - - -@mock.patch( - f"{__name__}.migration_1021_obj.multiprocessing.pool.Pool.map", -) -@mock.patch(f"{__name__}.migration_1021_obj.run_convert") -class TestMigrateWebPThumbnails(TestMigrations): - migrate_from = "1016_auto_20210317_1351_squashed_1020_merge_20220518_1839" - migrate_to = "1021_webp_thumbnail_conversion" - auto_migrate = False - - def pretend_convert_output(self, *args, **kwargs): - """ - Pretends to do the conversion, by copying the input file - to the output file - """ - shutil.copy2( - Path(kwargs["input_file"].rstrip("[0]")), - Path(kwargs["output_file"]), - ) - - def pretend_map(self, func: Callable, iterable: Iterable): - """ - Pretends to be the map of a multiprocessing.Pool, but secretly does - everything in series - """ - for item in iterable: - func(item) - - def create_dummy_thumbnails( - self, - thumb_dir: Path, - ext: str, - count: int, - start_count: int = 0, - ): - """ - Helper to create a certain count of files of given extension in a given directory - """ - for idx in range(count): - (Path(thumb_dir) / Path(f"{start_count + idx:07}.{ext}")).touch() - # Triple check expected files exist - self.assert_file_count_by_extension(ext, thumb_dir, count) - - def create_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp", count, start_count) - - def create_png_thumbnail_file( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy PNG thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "png", count, start_count) - - def assert_file_count_by_extension( - self, - ext: str, - dir: str | Path, - expected_count: int, - ): - """ - Helper to assert a certain count of given extension files in given directory - """ - if not isinstance(dir, Path): - dir = Path(dir) - matching_files = list(dir.glob(f"*.{ext}")) - self.assertEqual(len(matching_files), expected_count) - - def assert_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of PNG extension files in given directory - """ - self.assert_file_count_by_extension("png", dir, expected_count) - - def assert_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp", dir, expected_count) - - def setUp(self): - self.thumbnail_dir = Path(tempfile.mkdtemp()).resolve() - - return super().setUp() - - def tearDown(self) -> None: - shutil.rmtree(self.thumbnail_dir) - - return super().tearDown() - - def test_do_nothing_if_converted( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with default WebP thumbnail path - WHEN: - - Thumbnail conversion is attempted - THEN: - - Nothing is converted - """ - map_mock.side_effect = self.pretend_map - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_webp_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - run_convert_mock.assert_not_called() - - self.assert_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_single_thumbnail( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_errors_out( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = OSError - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 3) - - def test_convert_mixed( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - self.create_webp_thumbnail_files(self.thumbnail_dir, 2, start_count=3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 0) - self.assert_webp_file_count(self.thumbnail_dir, 5) diff --git a/src/documents/tests/test_migration_workflows.py b/src/documents/tests/test_migration_workflows.py deleted file mode 100644 index 60e429d68..000000000 --- a/src/documents/tests/test_migration_workflows.py +++ /dev/null @@ -1,134 +0,0 @@ -from documents.data_models import DocumentSource -from documents.tests.utils import TestMigrations - - -class TestMigrateWorkflow(TestMigrations): - migrate_from = "1043_alter_savedviewfilterrule_rule_type" - migrate_to = "1044_workflow_workflowaction_workflowtrigger_and_more" - dependencies = ( - ( - "paperless_mail", - "0029_mailrule_pdf_layout", - ), - ) - - def setUpBeforeMigration(self, apps): - User = apps.get_model("auth", "User") - Group = apps.get_model("auth", "Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - # create a CT to migrate - c = apps.get_model("documents", "Correspondent").objects.create( - name="Correspondent Name", - ) - dt = apps.get_model("documents", "DocumentType").objects.create( - name="DocType Name", - ) - t1 = apps.get_model("documents", "Tag").objects.create(name="t1") - sp = apps.get_model("documents", "StoragePath").objects.create(path="/test/") - cf1 = apps.get_model("documents", "CustomField").objects.create( - name="Custom Field 1", - data_type="string", - ) - ma = apps.get_model("paperless_mail", "MailAccount").objects.create( - name="MailAccount 1", - ) - mr = apps.get_model("paperless_mail", "MailRule").objects.create( - name="MailRule 1", - order=0, - account=ma, - ) - - user2 = User.objects.create(username="user2") - user3 = User.objects.create(username="user3") - group2 = Group.objects.create(name="group2") - - ConsumptionTemplate = apps.get_model("documents", "ConsumptionTemplate") - - ct = ConsumptionTemplate.objects.create( - name="Template 1", - order=0, - sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}", - filter_filename="*simple*", - filter_path="*/samples/*", - filter_mailrule=mr, - assign_title="Doc from {correspondent}", - assign_correspondent=c, - assign_document_type=dt, - assign_storage_path=sp, - assign_owner=user2, - ) - - ct.assign_tags.add(t1) - ct.assign_view_users.add(user3) - ct.assign_view_groups.add(group2) - ct.assign_change_users.add(user3) - ct.assign_change_groups.add(group2) - ct.assign_custom_fields.add(cf1) - ct.save() - - def test_users_with_add_documents_get_add_and_workflow_templates_get_migrated(self): - permission = self.Permission.objects.get(codename="add_workflow") - self.assertTrue(permission in self.user.user_permissions.all()) - self.assertTrue(permission in self.group.permissions.all()) - - Workflow = self.apps.get_model("documents", "Workflow") - self.assertEqual(Workflow.objects.all().count(), 1) - - -class TestReverseMigrateWorkflow(TestMigrations): - migrate_from = "1044_workflow_workflowaction_workflowtrigger_and_more" - migrate_to = "1043_alter_savedviewfilterrule_rule_type" - - def setUpBeforeMigration(self, apps): - User = apps.get_model("auth", "User") - Group = apps.get_model("auth", "Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.filter( - codename="add_workflow", - ).first() - if permission is not None: - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - Workflow = apps.get_model("documents", "Workflow") - WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger") - WorkflowAction = apps.get_model("documents", "WorkflowAction") - - trigger = WorkflowTrigger.objects.create( - type=0, - sources=[str(DocumentSource.ConsumeFolder)], - filter_path="*/path/*", - filter_filename="*file*", - ) - - action = WorkflowAction.objects.create( - assign_title="assign title", - ) - workflow = Workflow.objects.create( - name="workflow 1", - order=0, - ) - workflow.triggers.set([trigger]) - workflow.actions.set([action]) - workflow.save() - - def test_remove_workflow_permissions_and_migrate_workflows_to_consumption_templates( - self, - ): - permission = self.Permission.objects.filter( - codename="add_workflow", - ).first() - if permission is not None: - self.assertFalse(permission in self.user.user_permissions.all()) - self.assertFalse(permission in self.group.permissions.all()) - - ConsumptionTemplate = self.apps.get_model("documents", "ConsumptionTemplate") - self.assertEqual(ConsumptionTemplate.objects.all().count(), 1) diff --git a/src/paperless_mail/migrations/0001_initial.py b/src/paperless_mail/migrations/0001_initial.py index f7e717f0e..aab6a8239 100644 --- a/src/paperless_mail/migrations/0001_initial.py +++ b/src/paperless_mail/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 3.1.3 on 2020-11-15 22:54 +# Generated by Django 5.2.9 on 2026-01-20 18:46 import django.db.models.deletion +import django.utils.timezone +from django.conf import settings from django.db import migrations from django.db import models @@ -9,7 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("documents", "1002_auto_20201111_1105"), + ("documents", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -25,9 +28,23 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=256, unique=True)), - ("imap_server", models.CharField(max_length=256)), - ("imap_port", models.IntegerField(blank=True, null=True)), + ( + "name", + models.CharField(max_length=256, unique=True, verbose_name="name"), + ), + ( + "imap_server", + models.CharField(max_length=256, verbose_name="IMAP server"), + ), + ( + "imap_port", + models.IntegerField( + blank=True, + help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", + null=True, + verbose_name="IMAP port", + ), + ), ( "imap_security", models.PositiveIntegerField( @@ -37,11 +54,69 @@ class Migration(migrations.Migration): (3, "Use STARTTLS"), ], default=2, + verbose_name="IMAP security", + ), + ), + ("username", models.CharField(max_length=256, verbose_name="username")), + ("password", models.TextField(verbose_name="password")), + ( + "is_token", + models.BooleanField( + default=False, + verbose_name="Is token authentication", + ), + ), + ( + "character_set", + models.CharField( + default="UTF-8", + help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", + max_length=256, + verbose_name="character set", + ), + ), + ( + "account_type", + models.PositiveIntegerField( + choices=[(1, "IMAP"), (2, "Gmail OAuth"), (3, "Outlook OAuth")], + default=1, + verbose_name="account type", + ), + ), + ( + "refresh_token", + models.TextField( + blank=True, + help_text="The refresh token to use for token authentication e.g. with oauth2.", + null=True, + verbose_name="refresh token", + ), + ), + ( + "expiration", + models.DateTimeField( + blank=True, + help_text="The expiration date of the refresh token. ", + null=True, + verbose_name="expiration", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", ), ), - ("username", models.CharField(max_length=256)), - ("password", models.CharField(max_length=256)), ], + options={ + "verbose_name": "mail account", + "verbose_name_plural": "mail accounts", + }, ), migrations.CreateModel( name="MailRule", @@ -55,21 +130,126 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=256)), - ("folder", models.CharField(default="INBOX", max_length=256)), + ("name", models.CharField(max_length=256, verbose_name="name")), + ("order", models.IntegerField(default=0, verbose_name="order")), + ("enabled", models.BooleanField(default=True, verbose_name="enabled")), + ( + "folder", + models.CharField( + default="INBOX", + help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", + max_length=256, + verbose_name="folder", + ), + ), ( "filter_from", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter from", + ), + ), + ( + "filter_to", + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter to", + ), ), ( "filter_subject", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter subject", + ), ), ( "filter_body", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter body", + ), + ), + ( + "filter_attachment_filename_include", + models.CharField( + blank=True, + help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter attachment filename inclusive", + ), + ), + ( + "filter_attachment_filename_exclude", + models.CharField( + blank=True, + help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter attachment filename exclusive", + ), + ), + ( + "maximum_age", + models.PositiveIntegerField( + default=30, + help_text="Specified in days.", + verbose_name="maximum age", + ), + ), + ( + "attachment_type", + models.PositiveIntegerField( + choices=[ + (1, "Only process attachments."), + (2, "Process all files, including 'inline' attachments."), + ], + default=1, + help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", + verbose_name="attachment type", + ), + ), + ( + "consumption_scope", + models.PositiveIntegerField( + choices=[ + (1, "Only process attachments."), + ( + 2, + "Process full Mail (with embedded attachments in file) as .eml", + ), + ( + 3, + "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", + ), + ], + default=1, + verbose_name="consumption scope", + ), + ), + ( + "pdf_layout", + models.PositiveIntegerField( + choices=[ + (0, "System default"), + (1, "Text, then HTML"), + (2, "HTML, then text"), + (3, "HTML only"), + (4, "Text only"), + ], + default=0, + verbose_name="pdf layout", + ), ), - ("maximum_age", models.PositiveIntegerField(default=30)), ( "action", models.PositiveIntegerField( @@ -78,18 +258,23 @@ class Migration(migrations.Migration): (2, "Move to specified folder"), (3, "Mark as read, don't process read mails"), (4, "Flag the mail, don't process flagged mails"), + ( + 5, + "Tag the mail with specified tag, don't process tagged mails", + ), ], default=3, - help_text="The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.", + verbose_name="action", ), ), ( "action_parameter", models.CharField( blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", + help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", max_length=256, null=True, + verbose_name="action parameter", ), ), ( @@ -98,8 +283,10 @@ class Migration(migrations.Migration): choices=[ (1, "Use subject as title"), (2, "Use attachment filename as title"), + (3, "Do not assign title from rule"), ], default=1, + verbose_name="assign title from", ), ), ( @@ -112,6 +299,14 @@ class Migration(migrations.Migration): (4, "Use correspondent selected below"), ], default=1, + verbose_name="assign correspondent from", + ), + ), + ( + "assign_owner_from_rule", + models.BooleanField( + default=True, + verbose_name="Assign the rule owner to documents", ), ), ( @@ -120,6 +315,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name="rules", to="paperless_mail.mailaccount", + verbose_name="account", ), ), ( @@ -129,6 +325,7 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.SET_NULL, to="documents.correspondent", + verbose_name="assign this correspondent", ), ), ( @@ -138,17 +335,136 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.SET_NULL, to="documents.documenttype", + verbose_name="assign this document type", ), ), ( - "assign_tag", + "assign_tags", + models.ManyToManyField( + blank=True, + to="documents.tag", + verbose_name="assign this tag", + ), + ), + ( + "owner", models.ForeignKey( blank=True, + default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", + to=settings.AUTH_USER_MODEL, + verbose_name="owner", ), ), ], + options={ + "verbose_name": "mail rule", + "verbose_name_plural": "mail rules", + }, + ), + migrations.CreateModel( + name="ProcessedMail", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "folder", + models.CharField( + editable=False, + max_length=256, + verbose_name="folder", + ), + ), + ( + "uid", + models.CharField( + editable=False, + max_length=256, + verbose_name="uid", + ), + ), + ( + "subject", + models.CharField( + editable=False, + max_length=256, + verbose_name="subject", + ), + ), + ( + "received", + models.DateTimeField(editable=False, verbose_name="received"), + ), + ( + "processed", + models.DateTimeField( + default=django.utils.timezone.now, + editable=False, + verbose_name="processed", + ), + ), + ( + "status", + models.CharField( + editable=False, + max_length=256, + verbose_name="status", + ), + ), + ( + "error", + models.TextField( + blank=True, + editable=False, + null=True, + verbose_name="error", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ( + "rule", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + to="paperless_mail.mailrule", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="mailrule", + constraint=models.UniqueConstraint( + fields=("name", "owner"), + name="paperless_mail_mailrule_unique_name_owner", + ), + ), + migrations.AddConstraint( + model_name="mailrule", + constraint=models.UniqueConstraint( + condition=models.Q(("owner__isnull", True)), + fields=("name",), + name="paperless_mail_mailrule_name_unique", + ), ), ] diff --git a/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py b/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py deleted file mode 100644 index aad22918f..000000000 --- a/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py +++ /dev/null @@ -1,477 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:46 - -import django.db.migrations.operations.special -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("paperless_mail", "0001_initial"), - ("paperless_mail", "0002_auto_20201117_1334"), - ("paperless_mail", "0003_auto_20201118_1940"), - ("paperless_mail", "0004_mailrule_order"), - ("paperless_mail", "0005_help_texts"), - ("paperless_mail", "0006_auto_20210101_2340"), - ("paperless_mail", "0007_auto_20210106_0138"), - ("paperless_mail", "0008_auto_20210516_0940"), - ("paperless_mail", "0009_mailrule_assign_tags"), - ] - - dependencies = [ - ("documents", "1002_auto_20201111_1105"), - ("documents", "1011_auto_20210101_2340"), - ] - - operations = [ - migrations.CreateModel( - name="MailAccount", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, unique=True)), - ("imap_server", models.CharField(max_length=256)), - ("imap_port", models.IntegerField(blank=True, null=True)), - ( - "imap_security", - models.PositiveIntegerField( - choices=[ - (1, "No encryption"), - (2, "Use SSL"), - (3, "Use STARTTLS"), - ], - default=2, - ), - ), - ("username", models.CharField(max_length=256)), - ("password", models.CharField(max_length=256)), - ], - ), - migrations.CreateModel( - name="MailRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256)), - ("folder", models.CharField(default="INBOX", max_length=256)), - ( - "filter_from", - models.CharField(blank=True, max_length=256, null=True), - ), - ( - "filter_subject", - models.CharField(blank=True, max_length=256, null=True), - ), - ( - "filter_body", - models.CharField(blank=True, max_length=256, null=True), - ), - ("maximum_age", models.PositiveIntegerField(default=30)), - ( - "action", - models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - help_text="The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.", - ), - ), - ( - "action_parameter", - models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - ), - ), - ( - "assign_title_from", - models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - ), - ), - ( - "assign_correspondent_from", - models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - ), - ), - ( - "account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - ), - ), - ( - "assign_tag", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - ), - ), - ], - ), - migrations.RunPython( - code=django.db.migrations.operations.special.RunPython.noop, - reverse_code=django.db.migrations.operations.special.RunPython.noop, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True), - ), - migrations.AddField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - ), - ), - migrations.AlterModelOptions( - name="mailaccount", - options={ - "verbose_name": "mail account", - "verbose_name_plural": "mail accounts", - }, - ), - migrations.AlterModelOptions( - name="mailrule", - options={"verbose_name": "mail rule", "verbose_name_plural": "mail rules"}, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - verbose_name="IMAP port", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_security", - field=models.PositiveIntegerField( - choices=[(1, "No encryption"), (2, "Use SSL"), (3, "Use STARTTLS")], - default=2, - verbose_name="IMAP security", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_server", - field=models.CharField(max_length=256, verbose_name="IMAP server"), - ), - migrations.AlterField( - model_name="mailaccount", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=256, verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="username", - field=models.CharField(max_length=256, verbose_name="username"), - ), - migrations.AlterField( - model_name="mailrule", - name="account", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - verbose_name="account", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - verbose_name="assign correspondent from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tag", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_body", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter body", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_from", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_subject", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter subject", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - verbose_name="maximum age", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0, verbose_name="order"), - ), - migrations.AddField( - model_name="mailrule", - name="attachment_type", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - (2, "Process all files, including 'inline' attachments."), - ], - default=1, - help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", - verbose_name="attachment type", - ), - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="character_set", - field=models.CharField( - default="UTF-8", - help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", - max_length=256, - verbose_name="character set", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by dots.", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AddField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="mail_rules_multi", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0002_auto_20201117_1334.py b/src/paperless_mail/migrations/0002_auto_20201117_1334.py deleted file mode 100644 index 1f4df3f6d..000000000 --- a/src/paperless_mail/migrations/0002_auto_20201117_1334.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-17 13:34 - -from django.db import migrations -from django.db.migrations import RunPython - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0001_initial"), - ] - - operations = [RunPython(migrations.RunPython.noop, migrations.RunPython.noop)] diff --git a/src/paperless_mail/migrations/0003_auto_20201118_1940.py b/src/paperless_mail/migrations/0003_auto_20201118_1940.py deleted file mode 100644 index a1263db05..000000000 --- a/src/paperless_mail/migrations/0003_auto_20201118_1940.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-18 19:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0002_auto_20201117_1334"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True), - ), - ] diff --git a/src/paperless_mail/migrations/0005_help_texts.py b/src/paperless_mail/migrations/0005_help_texts.py deleted file mode 100644 index 8e49238e9..000000000 --- a/src/paperless_mail/migrations/0005_help_texts.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-22 10:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0004_mailrule_order"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0006_auto_20210101_2340.py b/src/paperless_mail/migrations/0006_auto_20210101_2340.py deleted file mode 100644 index 2c2ff9fa8..000000000 --- a/src/paperless_mail/migrations/0006_auto_20210101_2340.py +++ /dev/null @@ -1,217 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 23:40 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1011_auto_20210101_2340"), - ("paperless_mail", "0005_help_texts"), - ] - - operations = [ - migrations.AlterModelOptions( - name="mailaccount", - options={ - "verbose_name": "mail account", - "verbose_name_plural": "mail accounts", - }, - ), - migrations.AlterModelOptions( - name="mailrule", - options={"verbose_name": "mail rule", "verbose_name_plural": "mail rules"}, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - verbose_name="IMAP port", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_security", - field=models.PositiveIntegerField( - choices=[(1, "No encryption"), (2, "Use SSL"), (3, "Use STARTTLS")], - default=2, - verbose_name="IMAP security", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_server", - field=models.CharField(max_length=256, verbose_name="IMAP server"), - ), - migrations.AlterField( - model_name="mailaccount", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=256, verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="username", - field=models.CharField(max_length=256, verbose_name="username"), - ), - migrations.AlterField( - model_name="mailrule", - name="account", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - verbose_name="account", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - verbose_name="assign correspondent from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tag", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_body", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter body", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_from", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_subject", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter subject", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - verbose_name="maximum age", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0, verbose_name="order"), - ), - ] diff --git a/src/paperless_mail/migrations/0007_auto_20210106_0138.py b/src/paperless_mail/migrations/0007_auto_20210106_0138.py deleted file mode 100644 index c51a4aebe..000000000 --- a/src/paperless_mail/migrations/0007_auto_20210106_0138.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.1.5 on 2021-01-06 01:38 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0006_auto_20210101_2340"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="attachment_type", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - (2, "Process all files, including 'inline' attachments."), - ], - default=1, - help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", - verbose_name="attachment type", - ), - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0008_auto_20210516_0940.py b/src/paperless_mail/migrations/0008_auto_20210516_0940.py deleted file mode 100644 index b2fc062dd..000000000 --- a/src/paperless_mail/migrations/0008_auto_20210516_0940.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.3 on 2021-05-16 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0007_auto_20210106_0138"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="character_set", - field=models.CharField( - default="UTF-8", - help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", - max_length=256, - verbose_name="character set", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by dots.", - max_length=256, - verbose_name="folder", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py b/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py deleted file mode 100644 index 47fdaff12..000000000 --- a/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.0.3 on 2022-03-28 17:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0008_auto_20210516_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Mark as read, don't process read mails"), - (2, "Flag the mail, don't process flagged mails"), - (3, "Move to specified folder"), - (4, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", - max_length=256, - verbose_name="folder", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0009_mailrule_assign_tags.py b/src/paperless_mail/migrations/0009_mailrule_assign_tags.py deleted file mode 100644 index fbe359814..000000000 --- a/src/paperless_mail/migrations/0009_mailrule_assign_tags.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:00 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0008_auto_20210516_0940"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="mail_rules_multi", - to="documents.Tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0010_auto_20220311_1602.py b/src/paperless_mail/migrations/0010_auto_20220311_1602.py deleted file mode 100644 index 0511608ca..000000000 --- a/src/paperless_mail/migrations/0010_auto_20220311_1602.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:02 - -from django.db import migrations - - -def migrate_tag_to_tags(apps, schema_editor): - # Manual data migration, see - # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations - # - # Copy the assign_tag property to the new assign_tags set if it exists. - MailRule = apps.get_model("paperless_mail", "MailRule") - for mail_rule in MailRule.objects.all(): - if mail_rule.assign_tag: - mail_rule.assign_tags.add(mail_rule.assign_tag) - mail_rule.save() - - -def migrate_tags_to_tag(apps, schema_editor): - # Manual data migration, see - # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations - # - # Copy the unique value in the assign_tags set to the old assign_tag property. - # Do nothing if the tag is not unique. - MailRule = apps.get_model("paperless_mail", "MailRule") - for mail_rule in MailRule.objects.all(): - tags = mail_rule.assign_tags.all() - if len(tags) == 1: - mail_rule.assign_tag = tags[0] - mail_rule.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0009_mailrule_assign_tags"), - ] - - operations = [ - migrations.RunPython(migrate_tag_to_tags, migrate_tags_to_tag), - ] diff --git a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py b/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py deleted file mode 100644 index 16cec8710..000000000 --- a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0010_auto_20220311_1602"), - ] - - operations = [ - migrations.RemoveField( - model_name="mailrule", - name="assign_tag", - ), - ] diff --git a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py b/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py deleted file mode 100644 index c48ebf33b..000000000 --- a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py +++ /dev/null @@ -1,321 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:47 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("paperless_mail", "0011_remove_mailrule_assign_tag"), - ("paperless_mail", "0012_alter_mailrule_assign_tags"), - ("paperless_mail", "0009_alter_mailrule_action_alter_mailrule_folder"), - ("paperless_mail", "0013_merge_20220412_1051"), - ("paperless_mail", "0014_alter_mailrule_action"), - ("paperless_mail", "0015_alter_mailrule_action"), - ("paperless_mail", "0016_mailrule_consumption_scope"), - ("paperless_mail", "0017_mailaccount_owner_mailrule_owner"), - ("paperless_mail", "0018_processedmail"), - ("paperless_mail", "0019_mailrule_filter_to"), - ("paperless_mail", "0020_mailaccount_is_token"), - ("paperless_mail", "0021_alter_mailaccount_password"), - ("paperless_mail", "0022_mailrule_assign_owner_from_rule_and_more"), - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - ("paperless_mail", "0024_alter_mailrule_name_and_more"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0010_auto_20220311_1602"), - ] - - operations = [ - migrations.RemoveField( - model_name="mailrule", - name="assign_tag", - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Mark as read, don't process read mails"), - (2, "Flag the mail, don't process flagged mails"), - (3, "Move to specified folder"), - (4, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (5, "Tag the mail with specified tag, don't process tagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AddField( - model_name="mailrule", - name="consumption_scope", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - ( - 2, - "Process full Mail (with embedded attachments in file) as .eml", - ), - ( - 3, - "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", - ), - ], - default=1, - verbose_name="consumption scope", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.CreateModel( - name="ProcessedMail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "folder", - models.CharField( - editable=False, - max_length=256, - verbose_name="folder", - ), - ), - ( - "uid", - models.CharField( - editable=False, - max_length=256, - verbose_name="uid", - ), - ), - ( - "subject", - models.CharField( - editable=False, - max_length=256, - verbose_name="subject", - ), - ), - ( - "received", - models.DateTimeField(editable=False, verbose_name="received"), - ), - ( - "processed", - models.DateTimeField( - default=django.utils.timezone.now, - editable=False, - verbose_name="processed", - ), - ), - ( - "status", - models.CharField( - editable=False, - max_length=256, - verbose_name="status", - ), - ), - ( - "error", - models.TextField( - blank=True, - editable=False, - null=True, - verbose_name="error", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ( - "rule", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="paperless_mail.mailrule", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.AddField( - model_name="mailrule", - name="filter_to", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter to", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="is_token", - field=models.BooleanField( - default=False, - verbose_name="Is token authentication", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=2048, verbose_name="password"), - ), - migrations.AddField( - model_name="mailrule", - name="assign_owner_from_rule", - field=models.BooleanField( - default=True, - verbose_name="Assign the rule owner to documents", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - (3, "Do not assign title from rule"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.RenameField( - model_name="mailrule", - old_name="filter_attachment_filename", - new_name="filter_attachment_filename_include", - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename_exclude", - field=models.CharField( - blank=True, - help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename exclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_attachment_filename_include", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename inclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="paperless_mail_mailrule_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="paperless_mail_mailrule_name_unique", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py b/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py deleted file mode 100644 index 83ece3bba..000000000 --- a/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 16:21 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0011_remove_mailrule_assign_tag"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - to="documents.Tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0013_merge_20220412_1051.py b/src/paperless_mail/migrations/0013_merge_20220412_1051.py deleted file mode 100644 index 0310fd083..000000000 --- a/src/paperless_mail/migrations/0013_merge_20220412_1051.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-12 08:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0009_alter_mailrule_action_alter_mailrule_folder"), - ("paperless_mail", "0012_alter_mailrule_assign_tags"), - ] - - operations = [] diff --git a/src/paperless_mail/migrations/0014_alter_mailrule_action.py b/src/paperless_mail/migrations/0014_alter_mailrule_action.py deleted file mode 100644 index 6be3ddf69..000000000 --- a/src/paperless_mail/migrations/0014_alter_mailrule_action.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-18 22:57 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0013_merge_20220412_1051"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0015_alter_mailrule_action.py b/src/paperless_mail/migrations/0015_alter_mailrule_action.py deleted file mode 100644 index 80de9b2b1..000000000 --- a/src/paperless_mail/migrations/0015_alter_mailrule_action.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-29 13:21 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0014_alter_mailrule_action"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (5, "Tag the mail with specified tag, don't process tagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py b/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py deleted file mode 100644 index d4a0ba590..000000000 --- a/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.0.4 on 2022-07-11 22:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0015_alter_mailrule_action"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="consumption_scope", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - ( - 2, - "Process full Mail (with embedded attachments in file) as .eml", - ), - ( - 3, - "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", - ), - ], - default=1, - verbose_name="consumption scope", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py b/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py deleted file mode 100644 index 98cfef014..000000000 --- a/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.1.3 on 2022-12-06 04:48 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0016_mailrule_consumption_scope"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0018_processedmail.py b/src/paperless_mail/migrations/0018_processedmail.py deleted file mode 100644 index 3307f7579..000000000 --- a/src/paperless_mail/migrations/0018_processedmail.py +++ /dev/null @@ -1,105 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-03 18:38 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0017_mailaccount_owner_mailrule_owner"), - ] - - operations = [ - migrations.CreateModel( - name="ProcessedMail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "folder", - models.CharField( - editable=False, - max_length=256, - verbose_name="folder", - ), - ), - ( - "uid", - models.CharField( - editable=False, - max_length=256, - verbose_name="uid", - ), - ), - ( - "subject", - models.CharField( - editable=False, - max_length=256, - verbose_name="subject", - ), - ), - ( - "received", - models.DateTimeField(editable=False, verbose_name="received"), - ), - ( - "processed", - models.DateTimeField( - default=django.utils.timezone.now, - editable=False, - verbose_name="processed", - ), - ), - ( - "status", - models.CharField( - editable=False, - max_length=256, - verbose_name="status", - ), - ), - ( - "error", - models.TextField( - blank=True, - editable=False, - null=True, - verbose_name="error", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ( - "rule", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="paperless_mail.mailrule", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/src/paperless_mail/migrations/0019_mailrule_filter_to.py b/src/paperless_mail/migrations/0019_mailrule_filter_to.py deleted file mode 100644 index 8951be290..000000000 --- a/src/paperless_mail/migrations/0019_mailrule_filter_to.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-11 21:08 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0018_processedmail"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="filter_to", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter to", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0020_mailaccount_is_token.py b/src/paperless_mail/migrations/0020_mailaccount_is_token.py deleted file mode 100644 index 81ce50a19..000000000 --- a/src/paperless_mail/migrations/0020_mailaccount_is_token.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-22 17:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0019_mailrule_filter_to"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="is_token", - field=models.BooleanField( - default=False, - verbose_name="Is token authentication", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0021_alter_mailaccount_password.py b/src/paperless_mail/migrations/0021_alter_mailaccount_password.py deleted file mode 100644 index 0c012b98b..000000000 --- a/src/paperless_mail/migrations/0021_alter_mailaccount_password.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-20 15:03 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0020_mailaccount_is_token"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=2048, verbose_name="password"), - ), - ] diff --git a/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py b/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py deleted file mode 100644 index f2c59a5bf..000000000 --- a/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.1.11 on 2023-09-18 18:50 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0021_alter_mailaccount_password"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="assign_owner_from_rule", - field=models.BooleanField( - default=True, - verbose_name="Assign the rule owner to documents", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - (3, "Do not assign title from rule"), - ], - default=1, - verbose_name="assign title from", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py b/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py deleted file mode 100644 index 1a1eac790..000000000 --- a/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-04 03:06 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0022_mailrule_assign_owner_from_rule_and_more"), - ] - - operations = [ - migrations.RenameField( - model_name="mailrule", - old_name="filter_attachment_filename", - new_name="filter_attachment_filename_include", - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename_exclude", - field=models.CharField( - blank=True, - help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename exclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_attachment_filename_include", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename inclusive", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py b/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py deleted file mode 100644 index c2840d0e4..000000000 --- a/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.11 on 2024-06-05 16:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="paperless_mail_mailrule_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="paperless_mail_mailrule_name_unique", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py b/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py deleted file mode 100644 index 308ebdf15..000000000 --- a/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-09 16:39 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0024_alter_mailrule_name_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="processedmail", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0026_mailrule_enabled.py b/src/paperless_mail/migrations/0026_mailrule_enabled.py deleted file mode 100644 index c10ee698c..000000000 --- a/src/paperless_mail/migrations/0026_mailrule_enabled.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-30 15:17 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "paperless_mail", - "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="enabled", - field=models.BooleanField(default=True, verbose_name="enabled"), - ), - ] diff --git a/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py b/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py deleted file mode 100644 index 3fb1e6af2..000000000 --- a/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-05 17:12 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0026_mailrule_enabled"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=3072, verbose_name="password"), - ), - migrations.AddField( - model_name="mailaccount", - name="expiration", - field=models.DateTimeField( - blank=True, - help_text="The expiration date of the refresh token. ", - null=True, - verbose_name="expiration", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="account_type", - field=models.PositiveIntegerField( - choices=[(1, "IMAP"), (2, "Gmail OAuth"), (3, "Outlook OAuth")], - default=1, - verbose_name="account type", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="refresh_token", - field=models.CharField( - blank=True, - help_text="The refresh token to use for token authentication e.g. with oauth2.", - max_length=3072, - null=True, - verbose_name="refresh token", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py b/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py deleted file mode 100644 index 2a0279651..000000000 --- a/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-30 04:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "paperless_mail", - "0027_mailaccount_expiration_mailaccount_account_type_and_more", - ), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.TextField(verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="refresh_token", - field=models.TextField( - blank=True, - help_text="The refresh token to use for token authentication e.g. with oauth2.", - null=True, - verbose_name="refresh token", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py b/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py deleted file mode 100644 index fe7a93b71..000000000 --- a/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.3 on 2024-11-24 12:39 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0028_alter_mailaccount_password_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="pdf_layout", - field=models.PositiveIntegerField( - choices=[ - (0, "System default"), - (1, "Text, then HTML"), - (2, "HTML, then text"), - (3, "HTML only"), - (4, "Text only"), - ], - default=0, - verbose_name="pdf layout", - ), - ), - ] From d0032c18be0a4e8e4275e4f5a00e8d06fdc5f55d Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:29:54 -0800 Subject: [PATCH 56/57] Breaking: Remove support for document and thumbnail encryption (#11850) --- docker/install_management_commands.sh | 3 +- docker/rootfs/usr/local/bin/decrypt_documents | 14 --- docs/administration.md | 30 ------ docs/migration.md | 6 ++ src/documents/__init__.py | 3 +- src/documents/admin.py | 1 - src/documents/checks.py | 48 --------- src/documents/conditionals.py | 2 +- src/documents/consumer.py | 8 +- src/documents/file_handling.py | 5 - .../management/commands/decrypt_documents.py | 93 ------------------ .../management/commands/document_exporter.py | 60 +++-------- .../management/commands/document_importer.py | 2 - .../0004_remove_document_storage_type.py | 16 +++ src/documents/models.py | 24 +---- src/documents/templating/filepath.py | 1 - .../samples/documents/originals/0000004.pdf | Bin 0 -> 23578 bytes .../documents/originals/0000004.pdf.gpg | Bin 17779 -> 0 bytes .../samples/documents/thumbnails/0000004.webp | Bin 0 -> 2624 bytes .../documents/thumbnails/0000004.webp.gpg | Bin 2712 -> 0 bytes src/documents/tests/test_checks.py | 50 ---------- src/documents/tests/test_file_handling.py | 39 ++------ src/documents/tests/test_management.py | 62 ------------ .../tests/test_management_exporter.py | 10 +- src/documents/views.py | 10 +- src/paperless/db.py | 17 ---- src/paperless/settings.py | 13 --- 27 files changed, 57 insertions(+), 460 deletions(-) delete mode 100755 docker/rootfs/usr/local/bin/decrypt_documents delete mode 100644 src/documents/management/commands/decrypt_documents.py create mode 100644 src/documents/migrations/0004_remove_document_storage_type.py create mode 100644 src/documents/tests/samples/documents/originals/0000004.pdf delete mode 100644 src/documents/tests/samples/documents/originals/0000004.pdf.gpg create mode 100644 src/documents/tests/samples/documents/thumbnails/0000004.webp delete mode 100644 src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg delete mode 100644 src/paperless/db.py diff --git a/docker/install_management_commands.sh b/docker/install_management_commands.sh index be972d605..f7a175e9e 100755 --- a/docker/install_management_commands.sh +++ b/docker/install_management_commands.sh @@ -4,8 +4,7 @@ set -eu -for command in decrypt_documents \ - document_archiver \ +for command in document_archiver \ document_exporter \ document_importer \ mail_fetcher \ diff --git a/docker/rootfs/usr/local/bin/decrypt_documents b/docker/rootfs/usr/local/bin/decrypt_documents deleted file mode 100755 index 4da1549ee..000000000 --- a/docker/rootfs/usr/local/bin/decrypt_documents +++ /dev/null @@ -1,14 +0,0 @@ -#!/command/with-contenv /usr/bin/bash -# shellcheck shell=bash - -set -e - -cd "${PAPERLESS_SRC_DIR}" - -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py decrypt_documents "$@" -elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py decrypt_documents "$@" -else - echo "Unknown user." -fi diff --git a/docs/administration.md b/docs/administration.md index ddf51bf9a..2fb70a806 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -580,36 +580,6 @@ document. documents, such as encrypted PDF documents. The archiver will skip over these documents each time it sees them. -### Managing encryption {#encryption} - -!!! warning - - Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090) - because it did not really provide any additional security, the passphrase - was stored in a configuration file on the same system as the documents. - Furthermore, the entire text content of the documents is stored plain in - the database, even if your documents are encrypted. Filenames are not - encrypted as well. Finally, the web server provides transparent access to - your encrypted documents. - - Consider running paperless on an encrypted filesystem instead, which - will then at least provide security against physical hardware theft. - -#### Enabling encryption - -Enabling encryption is no longer supported. - -#### Disabling encryption - -Basic usage to disable encryption of your document store: - -(Note: If `PAPERLESS_PASSPHRASE` isn't set already, you need to specify -it here) - -``` -decrypt_documents [--passphrase SECR3TP4SSPHRA$E] -``` - ### Detecting duplicates {#fuzzy_duplicate} Paperless already catches and prevents upload of exactly matching documents, diff --git a/docs/migration.md b/docs/migration.md index 2ef850cbe..1c934e6df 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -17,3 +17,9 @@ separating the directory ignore from the file ignore. | `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking | | `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones | | _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults | + +## Encryption Support + +Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093) + +Users must decrypt their document using the `decrypt_documents` command before upgrading. diff --git a/src/documents/__init__.py b/src/documents/__init__.py index dd8c76d19..861c45185 100644 --- a/src/documents/__init__.py +++ b/src/documents/__init__.py @@ -1,5 +1,4 @@ # this is here so that django finds the checks. -from documents.checks import changed_password_check from documents.checks import parser_check -__all__ = ["changed_password_check", "parser_check"] +__all__ = ["parser_check"] diff --git a/src/documents/admin.py b/src/documents/admin.py index c6f179e2a..1ebbdc9ce 100644 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -60,7 +60,6 @@ class DocumentAdmin(GuardedModelAdmin): "added", "modified", "mime_type", - "storage_type", "filename", "checksum", "archive_filename", diff --git a/src/documents/checks.py b/src/documents/checks.py index 8f8fbf4f9..b6e9e90fc 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -1,60 +1,12 @@ -import textwrap - from django.conf import settings from django.core.checks import Error from django.core.checks import Warning from django.core.checks import register -from django.core.exceptions import FieldError -from django.db.utils import OperationalError -from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration from documents.templating.utils import convert_format_str_to_template_format -@register() -def changed_password_check(app_configs, **kwargs): - from documents.models import Document - from paperless.db import GnuPG - - try: - encrypted_doc = ( - Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - .only("pk", "storage_type") - .first() - ) - except (OperationalError, ProgrammingError, FieldError): - return [] # No documents table yet - - if encrypted_doc: - if not settings.PASSPHRASE: - return [ - Error( - "The database contains encrypted documents but no password is set.", - ), - ] - - if not GnuPG.decrypted(encrypted_doc.source_file): - return [ - Error( - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ), - ] - - return [] - - @register() def parser_check(app_configs, **kwargs): parsers = [] diff --git a/src/documents/conditionals.py b/src/documents/conditionals.py index 47d9bfe4b..b93cabf62 100644 --- a/src/documents/conditionals.py +++ b/src/documents/conditionals.py @@ -128,7 +128,7 @@ def thumbnail_last_modified(request, pk: int) -> datetime | None: Cache should be (slightly?) faster than filesystem """ try: - doc = Document.objects.only("storage_type").get(pk=pk) + doc = Document.objects.only("pk").get(pk=pk) if not doc.thumbnail_path.exists(): return None doc_key = get_thumbnail_modified_key(pk) diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 2c1cf025b..4c8c4dd28 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -497,7 +497,6 @@ class ConsumerPlugin( create_source_path_directory(document.source_path) self._write( - document.storage_type, self.unmodified_original if self.unmodified_original is not None else self.working_copy, @@ -505,7 +504,6 @@ class ConsumerPlugin( ) self._write( - document.storage_type, thumbnail, document.thumbnail_path, ) @@ -517,7 +515,6 @@ class ConsumerPlugin( ) create_source_path_directory(document.archive_path) self._write( - document.storage_type, archive_path, document.archive_path, ) @@ -637,8 +634,6 @@ class ConsumerPlugin( ) self.log.debug(f"Creation date from st_mtime: {create_date}") - storage_type = Document.STORAGE_TYPE_UNENCRYPTED - if self.metadata.filename: title = Path(self.metadata.filename).stem else: @@ -665,7 +660,6 @@ class ConsumerPlugin( checksum=hashlib.md5(file_for_checksum.read_bytes()).hexdigest(), created=create_date, modified=create_date, - storage_type=storage_type, page_count=page_count, original_filename=self.filename, ) @@ -736,7 +730,7 @@ class ConsumerPlugin( } CustomFieldInstance.objects.create(**args) # adds to document - def _write(self, storage_type, source, target): + def _write(self, source, target): with ( Path(source).open("rb") as read_file, Path(target).open("wb") as write_file, diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 48cd57311..39831016d 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -126,7 +126,6 @@ def generate_filename( doc: Document, *, counter=0, - append_gpg=True, archive_filename=False, ) -> Path: base_path: Path | None = None @@ -170,8 +169,4 @@ def generate_filename( final_filename = f"{doc.pk:07}{counter_str}{filetype_str}" full_path = Path(final_filename) - # Add GPG extension if needed - if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG: - full_path = full_path.with_suffix(full_path.suffix + ".gpg") - return full_path diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py deleted file mode 100644 index 793cac4bb..000000000 --- a/src/documents/management/commands/decrypt_documents.py +++ /dev/null @@ -1,93 +0,0 @@ -from pathlib import Path - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.core.management.base import CommandError - -from documents.models import Document -from paperless.db import GnuPG - - -class Command(BaseCommand): - help = ( - "This is how you migrate your stored documents from an encrypted " - "state to an unencrypted one (or vice-versa)" - ) - - def add_arguments(self, parser) -> None: - parser.add_argument( - "--passphrase", - help=( - "If PAPERLESS_PASSPHRASE isn't set already, you need to specify it here" - ), - ) - - def handle(self, *args, **options) -> None: - try: - self.stdout.write( - self.style.WARNING( - "\n\n" - "WARNING: This script is going to work directly on your " - "document originals, so\n" - "WARNING: you probably shouldn't run " - "this unless you've got a recent backup\n" - "WARNING: handy. It " - "*should* work without a hitch, but be safe and backup your\n" - "WARNING: stuff first.\n\n" - "Hit Ctrl+C to exit now, or Enter to " - "continue.\n\n", - ), - ) - _ = input() - except KeyboardInterrupt: - return - - passphrase = options["passphrase"] or settings.PASSPHRASE - if not passphrase: - raise CommandError( - "Passphrase not defined. Please set it with --passphrase or " - "by declaring it in your environment or your config.", - ) - - self.__gpg_to_unencrypted(passphrase) - - def __gpg_to_unencrypted(self, passphrase: str) -> None: - encrypted_files = Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - - for document in encrypted_files: - self.stdout.write(f"Decrypting {document}") - - old_paths = [document.source_path, document.thumbnail_path] - - with document.source_file as file_handle: - raw_document = GnuPG.decrypted(file_handle, passphrase) - with document.thumbnail_file as file_handle: - raw_thumb = GnuPG.decrypted(file_handle, passphrase) - - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - - ext: str = Path(document.filename).suffix - - if not ext == ".gpg": - raise CommandError( - f"Abort: encrypted file {document.source_path} does not " - f"end with .gpg", - ) - - document.filename = Path(document.filename).stem - - with document.source_path.open("wb") as f: - f.write(raw_document) - - with document.thumbnail_path.open("wb") as f: - f.write(raw_thumb) - - Document.objects.filter(id=document.id).update( - storage_type=document.storage_type, - filename=document.filename, - ) - - for path in old_paths: - path.unlink() diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index 88daeddf5..77b3b6416 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -3,7 +3,6 @@ import json import os import shutil import tempfile -import time from pathlib import Path from typing import TYPE_CHECKING @@ -56,7 +55,6 @@ from documents.settings import EXPORTER_FILE_NAME from documents.settings import EXPORTER_THUMBNAIL_NAME from documents.utils import copy_file_with_basic_stats from paperless import version -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless_mail.models import MailAccount from paperless_mail.models import MailRule @@ -316,20 +314,17 @@ class Command(CryptMixin, BaseCommand): total=len(document_manifest), disable=self.no_progress_bar, ): - # 3.1. store files unencrypted - document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED - document = document_map[document_dict["pk"]] - # 3.2. generate a unique filename + # 3.1. generate a unique filename base_name = self.generate_base_name(document) - # 3.3. write filenames into manifest + # 3.2. write filenames into manifest original_target, thumbnail_target, archive_target = ( self.generate_document_targets(document, base_name, document_dict) ) - # 3.4. write files to target folder + # 3.3. write files to target folder if not self.data_only: self.copy_document_files( document, @@ -423,7 +418,6 @@ class Command(CryptMixin, BaseCommand): base_name = generate_filename( document, counter=filename_counter, - append_gpg=False, ) else: base_name = document.get_public_filename(counter=filename_counter) @@ -482,46 +476,24 @@ class Command(CryptMixin, BaseCommand): If the document is encrypted, the files are decrypted before copying them to the target location. """ - if document.storage_type == Document.STORAGE_TYPE_GPG: - t = int(time.mktime(document.created.timetuple())) + self.check_and_copy( + document.source_path, + document.checksum, + original_target, + ) - original_target.parent.mkdir(parents=True, exist_ok=True) - with document.source_file as out_file: - original_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(original_target, times=(t, t)) + if thumbnail_target: + self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - if thumbnail_target: - thumbnail_target.parent.mkdir(parents=True, exist_ok=True) - with document.thumbnail_file as out_file: - thumbnail_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(thumbnail_target, times=(t, t)) - - if archive_target: - archive_target.parent.mkdir(parents=True, exist_ok=True) - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - with document.archive_path as out_file: - archive_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(archive_target, times=(t, t)) - else: + if archive_target: + if TYPE_CHECKING: + assert isinstance(document.archive_path, Path) self.check_and_copy( - document.source_path, - document.checksum, - original_target, + document.archive_path, + document.archive_checksum, + archive_target, ) - if thumbnail_target: - self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - - if archive_target: - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - self.check_and_copy( - document.archive_path, - document.archive_checksum, - archive_target, - ) - def check_and_write_json( self, content: list[dict] | dict, diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 3e614c6a6..ba3d793b3 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -383,8 +383,6 @@ class Command(CryptMixin, BaseCommand): else: archive_path = None - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - with FileLock(settings.MEDIA_LOCK): if Path(document.source_path).is_file(): raise FileExistsError(document.source_path) diff --git a/src/documents/migrations/0004_remove_document_storage_type.py b/src/documents/migrations/0004_remove_document_storage_type.py new file mode 100644 index 000000000..e138d5d78 --- /dev/null +++ b/src/documents/migrations/0004_remove_document_storage_type.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.9 on 2026-01-24 23:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0003_workflowaction_order"), + ] + + operations = [ + migrations.RemoveField( + model_name="document", + name="storage_type", + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 372fafaf2..88d33f1fe 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -154,13 +154,6 @@ class StoragePath(MatchingModel): class Document(SoftDeleteModel, ModelWithOwner): - STORAGE_TYPE_UNENCRYPTED = "unencrypted" - STORAGE_TYPE_GPG = "gpg" - STORAGE_TYPES = ( - (STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")), - (STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")), - ) - correspondent = models.ForeignKey( Correspondent, blank=True, @@ -250,14 +243,6 @@ class Document(SoftDeleteModel, ModelWithOwner): db_index=True, ) - storage_type = models.CharField( - _("storage type"), - max_length=11, - choices=STORAGE_TYPES, - default=STORAGE_TYPE_UNENCRYPTED, - editable=False, - ) - added = models.DateTimeField( _("added"), default=timezone.now, @@ -353,12 +338,7 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def source_path(self) -> Path: - if self.filename: - fname = str(self.filename) - else: - fname = f"{self.pk:07}{self.file_type}" - if self.storage_type == self.STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover + fname = str(self.filename) if self.filename else f"{self.pk:07}{self.file_type}" return (settings.ORIGINALS_DIR / Path(fname)).resolve() @@ -407,8 +387,6 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def thumbnail_path(self) -> Path: webp_file_name = f"{self.pk:07}.webp" - if self.storage_type == self.STORAGE_TYPE_GPG: - webp_file_name += ".gpg" webp_file_path = settings.THUMBNAIL_DIR / Path(webp_file_name) diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 805cefbdb..3647948ea 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -108,7 +108,6 @@ def create_dummy_document(): page_count=5, created=timezone.now(), modified=timezone.now(), - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, added=timezone.now(), filename="/dummy/filename.pdf", archive_filename="/dummy/archive_filename.pdf", diff --git a/src/documents/tests/samples/documents/originals/0000004.pdf b/src/documents/tests/samples/documents/originals/0000004.pdf new file mode 100644 index 0000000000000000000000000000000000000000..953bb88ab8acd1b69f9d0e2963e2e7e64933f4f7 GIT binary patch literal 23578 zcmagG19T2vP6=e+;j_ud}6#zNK7 zx8}E2Rn0kyOhH79mXVGPK-OQ}-_if8KM%k}$UtasWCh^ip_ehWGk38dWd6!gq8GEY zaWQqG7qc;RF%>a2wl^{5;{!OmIGGyS0z9%+<;MX*42T^^)DJoe?#jlfLOoCZ=sXgj zyvg{*i?dPFgm;fP1W=}i=BXJgri`Rd^_1>&-pBYT>7@n)0zHVH0BFT@Nsx@{bZ z{{+=dG~H6WGwvm7G}gfiu=&+w?bS4IM&k^LK2*}UcdRvG6%<5B?se3}LjWhfdARF# z`x61OETDwqM5-RkiACdB;2VwkC9&$J@*e@4y-aKl0fBH@k5BAMEm9c4H)DWaMCE`%fz! zczSE#Eaev}*ID zi@-Umi@0rJzp%@qx9!X#dbr?Pu@SD9eSCN*m9$G@v;9N+O4j*)VcX%TymX#6wEJ;5 zA(0x9z(U7Bt>CPEFy0PEgf7}p&(RVV?!GJMlY1_a;A{EwrRc@ zc|}Y#@rf@lBg1?B$gV>w;u?y0o)IxhviIQL9p~~r8F8+{f)v%R#&23Uya?CTCYqQy zp0jY!5g~R-q-?g?N4cp0Z5SH&QD$gZ=(48Peea13w>Ar~k*HDo$H?IgIJWgj|HuDdu>X|

    6T)g=R zmQ(aw@_IR<&w#v<2?XQw67@dqfK*!E8O0W{b6(K{Drb~S2E7D+JWuQ40YzNG+eK(J-G~(08LZ@r86feZo=t2~w{YUSG7H3SRN=$`%VNI3)sgRQep|VsVljfbt zS)2UFg(`p0idQOP!R^a?z)nGD5a|0y*CsFTM!UPK>kHTFnwC3&miw3eT3d5V5Lcuo_hDMO*9++6X_pM6 zetBDQuXp8-gm56~S2kVL&_z0hiO`KiSYM-!2}6nka}K9D0+`C;_rOS+V{DR29h`Sh z&+RT~&MpbWo;2FIdm@ggI>P!Yn_Lv4LDOK!%2gAabi$6gQFBhA3S$`bRhAF!qENl8 zMtuyC0!5)BPS+zmHJU*wdw!fQGfg4t*0Iofts()UK0bD0-P*^|uI4Y0rPt4sMOMqD z6z9mTDx~PhGvqGz4y`c`FZiuVi+ZuYFXO>)FwoQ!CFdA(A0A#H$+8+Xwd?-qx(CJa z!bh^+IL9-JDQ@y6WS4(Tp%PS4$KF_9zl3Vr-DZv2-+|E)?dDRHXHT0BD(bsJcAkCm+HUUW}mN3 zbK32zVe;*}4ofiR+tRL^?XPwjPW;cK1n5xkpdci-)?$WrcQwKy4n*_UumeVMvGXaE zUTqY_jC>Kz5v2)XWK%Zj(Fn+m^tFrL9Zp5nR_e2hp71RUWOMPlHoHXRJ+?@)v@^O# zor(zS2M9{~Ftu~QFo$9%dQh;^Gl&Lz;z)J^ycFGN?X6q3L3Ks4wObLkq2jT|72Y7g-42yN}Jj|lc^unEwKT&kwSOsp4% z?LQ!36y{a`v=|OeLd4I6LrsSHx4Yu-iQ1OH+mk?K8kZL42pnMJ&BbY0N}PV@1!!XkAZ8=x#m3S3Pg+hGa;!&@}h#t@aPYm^k7 zf@>$a@aGc)FZ=UJCmt<*J2-l!}N>jNKC>P8$@bayx( zx3Ad7VopcTAGAwar6k#1E^Y7UcV`#5EPA9G7&1!nuZ1s%%w6ryCp2h6`zRmh@}N`(n1(C$P=#s zj^u-BxCIZ?Jxmvc7yNa#SwO9fa8AHiJ-o3Zm&c)4nT$paNuEy2!Q-x9=AaWE4 zT(J@kt{n6!0QLwkljIGFT{sgJav@Qrv2T}hMT^`eI%G}Sfi8_o z)|a}k=Cj3T&O>kd%|B`^`#KFjT7%4&y(7?k+G&|Qo7yxI6VLkBaRfX-Bd4Zk0R3mL z0I>ELq8X`xls1jQ&z?eXyZ>Z(Gr z8q{YRo^rn(;GEl5maU%0etJsxbK{&<4v%dGR%a}2&Z56x=YJ1!$p;DCGCiZe&N4vr zROApE8>fzxmI8bO;7+PizXhlk<3cV1`~>V-(-@<*Y_XxrV<>Wd9tVR{VCD#9Pe%vs z$w_b!9fNhFfKkkI2RH{NB_^tK&7%e7S|a6bBXP>gcW(VUVi0##pa$tQU7gBwWLLnrF;AJaZFfZ^&M(jSP8fm?t;H1r>YT05k8PHGHV0UA3XgM`+6 zM+>aPAqK%>L}|YDeP(Y&s3p37OUaF2DKdDv$iYHg0-W;R$DaKVLcw5|0gb&5ftWEHsp=9Z7pCgTEBL3VD(k)uKK7qPbCD7hLHvTYm z<0V}bcHn(l0&T|6)kYvWEwJY`IVVfU7jjmSVqkX)obJgEQN{773b9|G-Vfml&9r81 zuUc$Vx1KmauL?5tW%*p{vS0MZZnO8izZTdJ9kR#I^O$Y-6|VQQkiYDI)n3nU|9q;P z)!&%;n5dSm(m6NoqUYWA`95}PZ-1dXqp|c${$+wtSDK)ny`dJ7yM|6#C`wXYvtsw| z1%MVc{IL*H^$NjSAk9x?F0J06F-P&j+KK#aZj^{>>WBEK} zsc4z(AI-NK<4-i_Ux_qO%}Mo4+6j8KO_u7MYDQ%>EXg}PMz@dv+C!y%lNeCo#V1!t zpKbq3uKac2Z;&i|+enbbc!+L2dXSCW`J=$rxCUh=Gr$v$kP}>lJO25gV2~Xr{@_v^ z8DUfO%L$(fM|vC0*0px)54?U2q8Do)1@Fr(>>EDJ$14W5&Na4SkmAW*CHE7E{D)g% zb&SR?vCyy7K(N#@E7!k$FH)jZa$W9NAy3KjZ?hVjmL@Zx1Ga%gaRV}E0!{6%vZO8l zMUb=nB=P;7{h{+Qh@cC$Zo+tmgU1E*t*2JFAni{+ z#`kkkeOgjV8|I2A8mp*F5`Z^Gu~D|M#@1^oy%XY$oX@(N2%WzL5i@?Xt6^B)tXpRaS?a(0ziGUjw2 z3(W+XHhRi%-Ou-?=7&k<9T~@Key52x)kB9o0l!j{e0z;&tpsb?wOPoGm@?Cmn<7ld zVupu}c14(pg=n6kpVB(n?b2Q2X)iP~>;& z5WoGKR_XnfAtmkd!wcI((?sKjQ!A@BEprU}h_`|0(C|k9uc%tzYhR+IzS-OwE}9G# zYL!%!+RB*B3v%O#juD|&G^J*QEc1o`_hn-Er}SS7Yw%CGpdXXKesbDUbh&DfEVHS7 zjn=q`o%{X-+}o;RGes6)&<`<4l@hmJZe$LRU%dffwJ2b-_%6@JFo>l=V%| zF{nUE+?8umUT_T@{OQ&xNYry&?NZ%Z-cF^sY55swx$$(f^fx20Q*tD&@ppdU47<;L?h@C?AW$2{Xh%e?lihK{R?aF_RZnZRtlHAj4$ld- zcBbD#M$%TDX?gnzl(5#fkk>)g>YIn+bOA;IL_x2x0*?H36>O9*M!DS_7=dK6ImNK z*g@lDcM#%s?Yy%$NqIKaTTI7Y4LqE^JZBYNM>(BoaThkM7mb{jZ|-|uPt-i)sXLZ# zYRA;-XbF*kqOoI#RIK*^CZ4Jtc!9>2XX_X=${vkZ&}i_u@|@=~H6euZ22>JI!ASMO zya|0W*_G*}w`y5EM%LLt%}AKT)pMoql2s@QVKOZ~O#E%nbrUmroxE5}Z|)ZI<{$Bn zlbfEm9|BCb754mZr5`Gcj`@>7{<6gk#X3}Jb zQfW$ux+J%6hzj_$&y94FKKO^j;g(Ad{V$ysaNKo#(pofG2IMNb|&-f?!e`DakLP}iCi^3JlMJp>Lo)P{U8X$3yu^BhI)!bx0nq!_BU6$_+7vI9mm|_!>V~4Ryf(l zQL^`mM*6>Q;to`9K)*dhAvADwDkOA-i~GFZ3KeWG2vm{Lt%arP)#z zt1%k0emW~qTQRIZ(z3qL@jft-#@GUkd{0efSe@cMe`(7@<1$k__bhtvOPH8`r%-os zX-tR=`CuFy&tq{_T#oeGyZ_-kOX|d>Y?7+geOA_Q*7Zz66Cyz~cVPSQ3(97GIErpE8 z;qLc?hEFD=xwTrQb(LCDYYjL;QuP+AIV!5m^3*VF; zzTNdzO?bU|N9s=Hn$TCQE-2m?n&liqR||XVxQjg+TZv*>s+^gGBXw6?F^s&Syz=%b zRf3i}|GF4ofg5R+55OuU+XGDOJ{~56RK*|ZfvY7MISB=@4hVD=^k_Lza~Ab*DZ#q} zbOfaGRRdrQvEKkXB5F)qqwk5j!*4PHQKJxQWj$mj;O)BgdSve)?vb5x0Rax*)B>=S z3Z1AKq}eL5Q8hw%Pyj#3sZ|~+e};0X=qnr0ww8No2DqB!yflJf#HajuL~V(GZhd+D zs+{fAx46g4Nmwue1S2U6Q16@)UmDoFYK&6na<(Xu4ih#@WJizPO38v!DSSw@@#%xO+n2yz>(9QW-z41g$XrQCL|HE_33HB&0%yz zgeE1H6bUQqF!T?^DTf~=UWS}=!3je%JH;|PA3jc$!oCJ+20W2751y#BeWv7)(#~=^ zi*_a5k$@fx#9V?flASzdPHge+Ty+8JWJv0HjO?&-)B28u4b2!Hd`XJ~DOy&#*zPgX zg%uWj1<{?U7nd)TDt_dA;l|#m#5e4$+-;@A@ko}m*wER;IpqWwCN}iUY@R@>EUEl? z%EMy^8oa@XGGeM4cBgclaQTqZZdHn`z&fnqbf>xs5hma8)nmyRGLi(#Y#LPfm?3gw z%XxG-GGSv(Mp$hd8c~wKfNkUgOZj0LrrhxSk|oy%9YYb&1B=q#q%a4w2yZTAXldrc zl`!HY%3u`vp*=`4zo9)z7MAQ-uNxYIIQcR~mdTJlLl@j4G=i**#G7D4IrA8p7_vPH zX_?U3a?@|B7_wz3+8vwyVF*hh$@9^MW`7hK9-}NyNFu@$2kBL9-h49qhelZ{ z`ui=`^5z%Ar~QpYF;Z$oEilI73PSOdaYU`U^kQL^WMI=YC7l8%^6`iU&Pb_}BMIfx zog4;&9`k0e%Z^_ulgAXXXKEliaB8WZ%kG90P{c`cN>N8=sjS`w2L=@{qIP?U^n=al zLz1EJLMqfG9}%5kmFW0Wl8F;a8)TJhI+xPx5X^DKpBXbpX^t=#XjOxMaK<{QM4ZTC zUXnHu${f^pl{g$unQgB&F-lZ(iyW;BU(a%gT2^z&QTdKCu1Qyilc+@w8a9e#vPZf=q8a$lQmS?Zxu`7}|c*b1{$9kARi)vdCOtrZci z$#N`_nf8CB)KyXJmHjvQN0uhG>|4dyUjwWa6Qtfel1oKF zt#8qDUP&a_rC-}S%B>@IRwI*jwifCTKj)s4?VzGt=%MfhMQi3PpE znoUOGJ5mW+I-&NJ!HMW;aW|J%*t0Hk>77iDQNtrY4aG`IKGMWxoXKP=Rb!p_$;XM# z((nCydp4Ay7Kr=ld*XumIQaR8?mI#I~>+bGAhhT4eKo$h-rFk3le zYhh3DKJJuzx*S97(1GX(JG6JtOy$9IKRvKETVU5sIbOuCK9S7$`92tLvtSk`kLWsl z0#05n_KS}Aj86GM`o;4}j$)49kAjX+j^uNfP_g;Ic%NC*H}%)g-Uc6>g+J@xkA(Me zz1(U(vOm|)IIpu`aDNK(o72kefix?0$7`hPdN><8?(_xPJ#Y_q_rL8)>y_=09Z5&u zx@?^km91%J&`)UQ8Dlw_>8@xz7FiUP7ZV9O%CH%JJIoZ?I<3e&Gwy$ zN6B`j^NKmG~8g8PwVYxug%@e1_HRJ8#2gg5Eif$@&< z7QIbB{R;bilFy#N+s#rj+@)P%?PB0!W#f!azp0_hxnXYlGLDOtB5xp-Z!Bo z0B64Q^tRcXnR(H$S`uKVc+{;I>^(RpctUk|iNX39X=3qt-+v3F54Y}mhH0t0K+6UX zAJwZGV8xsYx0j+Kqe5^$AUT?hldD2f5Rg`miWZ0+ zA&3vkXHAMtOv)*60!l^9OKKEDLn>K@>Z+pQtI*@N{Rx`KsBZf|&}x4Pv%g##8wV%L zfAeab|K`kd~Q&nUIl*ot==AgHe}W(#6om(pb>W+{ToU z0YES4Y;0=hLde0%2%!Jl*xv*#6C)ddUf9qN24>TIAKJM)~)`lx^RUeU}qE^hEQiYg& z8?T_1rpx)T5&ouHp)$5mFQtQ6+dKkuhM?<2IyR2%O;QM}P0 zAMiFJ^8smg!^5P^PqB!cmocYMlBg5(5&5c5kkI0p83JoTP{GjLlwQHm z>8~@e{jGpr$<*22)yde@nUL|HA^=%a6HCLtKK2*!Y+uc=b290EIf;|2vCBV8I{!7J zxRbrB!&hgNzvjWv&e`E_oyMN@!pdLH=4NSZsw6H%FG=`S>PtgFFZ|WxuO^)d|Iz9H zbSsvBcTkRy`CoDbM%I7y?f^$hD zpoBccbVHLGaZ4+MDHyl-0{(z-f1Ggd*CDZ!)nyI;PgU(H_%;L<>gTEUkH@ahGAQ4V zx-KArWzfm{n8*n3?LyE)>FB7Juy*+L8TOI2OaDvUpCS{z{*WEl@*79|uq2;5J$@PY z1hI}k=lBhvfjlAtK7+o0K)yl@27~t0I!)6*UBZ4MaXt{B`0NJt?~(?Qd|W1*Ai|va zGfsnc^gs>-86ZOJ`tSE*1Ap*SP4ZJ+Kq(i<&bOGyPB^s1PxGAD z=~w^U{Mwy63^6kPUIvDA4RPU0SEyKvN;5n z5;E(~AD97zya!eT7{>q-*Z+1K{~DKL7!u5%5D}zL0Cn0Q6OrFR0U!a2MF_bc0HOdk zCIDUz!kQ1y5`aDp!37z63JPlAvE2#H0B+U;q5(o}0B3`6^Ai$&8_@w|vIo2c#C;o> zCjjG$Phte)DF6r&Di9tSz&rBojj&7t5zYwSDH;@-5Q#LvBpyUb5L*H=gAm3f2sa)w zHqe-m0M%be0Zh){$N`WIL6(o290X8+t`vYP2Y1cKXYp^yN6H43&Bto=SDS`d?G>E{ z=IS+_25%EYI0pZoPoM+7)Z@wkW#>=C04vr*%K*6WXJmlC2z)gF;Q>PEMfCtH?KNhA z$@MQYfS(9D+6LDEs_h~6fZOzM-p2Dl;PY4g3BeaQwGHzGVC!MO0(}G1Ho$oU;oe4A z2KP1~2qeT55~73xH6-E@@s~ivDipXwL^BFbCqxfNGYaD%qzi}K4S6C0mViPI6(PbV zg_jgkiiahI85IV^BNu~I2>pqNC&7Wz;iLNe+98}A{L}oDBfVAJ}|@BpF+#TxQMY(7{T0qFgrn4;$TEVNJ!xpBDlmz zj$jF)BO)dwH;B*SLL!X2EQPR&A?2dkf-Jo&(YY$h)+=yhdo(&~>p-vCATFLT`d+M24h*2vOvCR8q9jU=vX%V$b+D z2_jNBs(@s%F_M({N(tFwcqIbMXcl2DV%r3G35sHzS(KBYJ8=l2{`iCu4HFP1tc=hJ zkwfA*BAA515o8n2dTh-wo+zF;8_{1R^hE6m-$#J=;r1DB*qm@V1G0tq#P~!fNigDT z6UIl-_Qm(PZn&LLo^e{EKqyH8QN;O_ln|)Qk$8hKyW-FV7L=07G*ZANfwKZ9`FDy# zl+MYlWAF#zPNJTLJ_^9)1}Yriu+ieA#7YY46G==R0`*avKFi=;FXV; zQ=jZIPNno`Sw2zhbw7-waTUi1CaF3tlGD@5n=u z+k%j3mPS%7^y+XmQR{;HX)z1tMzmG@^I$h&?}G0$g2xOls9f+l(Q?A(1-H|~GlR#- zE@W-!8~%1e%>~@k8^?TitapfBsJyYh{=UJ!fw|)J`CIv0Mf`w7RGmso9 zWQG+Q=rk0m2-MMN!&1g*KPxq8fAB4WSw}SWa~W>bZ7u`ZP_hAV;&=mp_Z%9*{2W|X zvO)WW?+w2edEVn@{8|^ZjC6+S2J#B^3igWI9{fE_x4*{N&%n>n-4O~GUA#}xk%of~ zmtPKyA|PpyYLCUCje{f`d=mXA+_~R#55%FM#X<*!4m>S{JCb@JXGnG1$}y$|eO*+C zy@tRY)E$OB);rugI;)>{dvjOKLEmA^vD=~D12b+g=?d0^jfY(iQNCY#5ADjz!?uHH z6V^V+eV~1(^BVYx!3UNPDKB)Y_h>Kb+VsiF2eb=oEB3PQanF9+e&=$R;=19<^Nsie z?}Oxn+85CtsR@Tm$(GnhuOUkbo!{QB;Dk7`Yz$ zIoL=Dt02@Nbz10{@D33IN;sH17`l&T2g@+vD)pz>lhiWl8}L<9|pBa=xcpi2HB9tE>=;3ukd$? zR1vJCU`<$++$gRpCYr@O0k#ai%Wf2T5qOb#5nLq_Hi6BMF(Y=4e;#G2XVHxKA<0IV zm0&wMX+rM=#2Jk%%_lTTe3TS5dhq3fo=)7YSv&&x#Bm7(iRPp9BlaUz`?dSDH+0~V zL<;4oU(ZCM!Nj79#TE*8RA#8!k<5cp$O6!11ByzNE6E^ZR8nXq0kc9U#dk^&R7J_q zW5#1VrV%#*HzDM0iJh41+zre2OuGp@K58`*U7r__B7w8wPm!muT9jGS`fk zh?k(3FyLMR0|SF?14IKv^LTRwa|VN-1`Y-e=2ym0#^J^irX!}5MwG_o<`!nhCdbAt zqYOg~qgT;}2|J+uT>a?%d?cX>eItD%vlTYD}vDd77-}cytwqy`dawEnDdP#dody9LDM@>dd2KNW{2T=!5 z2a_j~2VJr&k}FaxA}WF^;w-~0vrmxbc;>82SxYZvxF?{@h?z;4F&Y>eSQ?lb;2WqK z@EWKa=$$d0d7Qa!6%UgpR3~C4&L`BpScnf!CUj-^4zsCA5~&neF|^nh%fJC&Yh?T7!@I%9fZ zfM$|nq-wTc$ZFbT>}q~*9%CSD^4o}aQfo|Wc(u_1!_RdVt%$K!S+E*Y~hL_8hk5;zo{miyZ zwvKun85|rO!yQH(${jHsG#ojcpB#c62b@hDP#uz;=A9dzT%2K@q8)j5JtpfWu0xEo zPh2$W;qTxSvg}kZ81Z%gqLQS8rBkJI#^c9B$5Y1Rs1vC(sKct#t0GTv)hU)#YqWI`T5}yccU}gXM&Vkn z*Jaju){WKy>ssrj>ksR>>w@cb>z3=!>m}>=>$dB>>+G$UPE!u_4z&(`M<3%sI|Vlm zH{n+!*A`cpx69Wpw_rB|SH(BDdsI6WN0tXCJFTP6aps{%Xj9*&5OR6-Xy`%d+391{ z1JtvYqL%`eqS%AkbJ>%c=hXVwM%M<^=G4a72VW9x;cjVn(ff2iCA^{fy7?0MV)?@P8vAnl%6}Mq zaD2di2z>xP#6ILcfIrsW;-2)c{I)-bKGlJ!dkA|-2+8v)^QjGpwrRGh5vd$Rw#$1g z1H1y*0`vk510X@Bz%~ItVBXM0pl6|-0Fi((7%P}e7%spIlo%8_%pr6x`VSNvlruD3 zbRR+viWZs{x)zdP^g@6m%uFz9h)W1fh)(cZNMW#2$W6#t@Fc($3Jz)mDHB}_Q48sX zsF7X<(F_S4F%?}6gAM%`#se}unjGpR8XuVs#X9*qH4pKdf`{Hy&Q<7D4U}B4WU#!5 zl8EYn#DLs@GN}luAgTC%u|%~*l|=1G_=s@A5%HyHSHxN5u4slRxJZfUf~c$ro(Muz zXcT!AJ{m0&FNv4pUF9*}jOC1ap>?4)OS)Kr~{*fL{y3CY^23PBf1uX zZTdqzo4mLDtJoR}NqAi(bmVBHWkf~Ls#fw=vXJD$wB6Lic%>AkRHyix)W~G16fQbdss)+_5>{eXs(U3XRV!(1 zB3b(1w6sLkY%-LY%uTpVYDU*nJij&2+{&8ir;?7-QGcr$Kt2@08>O<3w=aJ6d#rbpXC^ zdeC%0d$4gJyq~lmHRdpL|3$sxSd#5QFBIOMsP-*QH)Xfy4a}GC~P->_xqsdAljfTSut`D za$aI|;;+P%#CE0fS)>w>5~&jO621~{sRk+gWVYmol+iJrG32p7W2Ix0V*z8(W1eHC zV@E08$@Wz8)bv!f)R&crsvIfRS=crmH4W(>b6Sfs-4wbMK5CaS=b6i<=5qs6_%Bnm914Fl_lkP>Q##E z`E2=a^0soeqPEg_)sw|^#WneM`F6DzRrp1@)xKJtik;HmMZRl&mwc~!$%W7i(iT<{ zR_<5o*Cml5Q7uv^QrjyWa*ctG!N4Y9^|jt*Sz)ovtjr|OoXix=l+1k2RL!i;RA;@k znzXF3u(LL7)MylGOm2#9%5DT{LT-w+RJCfd(p_k)Vl8VbY^si#F`Gf3QCnnNben~r zmz$kjs+qN$!=LUdd)M)neQk!93zHR>5tkj78CIoGp%5)oD3dD_J_?;Io}|v9=kjhd z(Y4dH)V0;M-so>rY}0HjbDcVQINn^$S@>S|s*F%FTukw!tl0d=Q87kwTCsY*deKr1 zd-W?mM zlIo0V%f&y7YKvjj#y@UccGrCv2K_Q@LTa9AtgE}Oi)iLFQ#PKnWVT^8 zzMV8%LpL9{q_(ZHJ~hCx$g2Ki#Hg%CD%K-Jh?!(PB&LGP_ts! za}lsnxWVpS%-g{GTd#j}b#s_+lW(hQ@gwn__Lbs+`hgps6jBe;6OsfHBa9&oHViq; zEDSgd5&f-W`pMAA$w|Y> zjLDM8>dB>?vg|D``?hVJ79A5ESe!jj@$R6m+_eGp*`|Tv$MlP^pn&*wG+1)+hyBD+rjfV?m^ui-Ia~?_n+R` z-d)}~-f6FRkDiZH`0+@%ee`a9=#6k-x*#Qh7C{pA;NMH!U!h$ zbR3^{cXs;a;cM|W?@PU+JgU6cJYKyCuIV25E_t_kPu_>T$HTV!Pm7L?|LmQhoF-i= zU(W40FCCRkh0>3uZK$s<^(+mt_puK*k2TM^&$~}PHW2I&pHWUKNv-C6iv+~mph*f}8fVqe6>lHC=K50Jdwpou% zk5!KyA*%w50`oK<10@6RPnIj8ZH4Wmo~E96!Xd&s!t;D#K_bBnK{P>(AUkkgm=|1F z$ObTJh*L;zNO76wl@MC4dX(D+D zJZYV{ZiM&kP=%8RXkMeRAg1$=Qqj+%kyS}Ej}z#EKZO8kLQnPj~|W`j$4oK zF->5_qjjPsqt#`ECL+>H(@z=WjMa=Ej5CbMjP8wfjf;$Ljd6@#jVX+`1}_F?h9ib7 zhdGBrh7X1ohVBP8;=J*i@#JGMVwa-b!x3VpVryeBBfD|BF}qU)q!48kup2TOGAE={rkCehq+uqer)Z=sr+BbCGCER6B$Fl=C+x?gW+vyTB+ciz@SnKO z)|yDwOVq2?8#{?O={bFOvcKuyKiKCN$BYffo8jK|B7KqQO&B5p6)%nZQR& zhgzE)oV=Tynp}`}eg=xp+Q;uYw zkY9-ST3?~mVifSRTLI!RPL&@A|ipRDnf-UV*lukD;TX_KwBQc;9bgrv!@xy##GV zpPA9ZUxhP;A%(wDl2IB_yeQV?NwYazcl#pNDD-lc=}PI*>DeeBXym@pQC(2t)BEan zI4ABD?@R6_ZBi)9rRH_`+A6VMJijWP#PeuB_$@Mm;6Rkk~}K~C&eqBBqc4)Eq$5NMfVpZ9H<2}w z5s{B6`H4kJb+e^RjW-si#s>xm4P*AI1K$$9b$*Mh9H>mJoUHU#9ar^Uuvq{sEG`f& zcrKV%L009d`c)AvhR+`_cr21GB`@gIWR!dA!&q@zr&ym`^;_RsXIX_=>(ARQ&MZmJ zV=X-|mMp0*-7m;4eXkszzg@CkY@Z)q$gAwu;Me>7<@*S`h84ohW-c^^%k6wIF`a>7 z&W3Z0Ly9ef?SsRFZH4oKV}`vrwL1yOS>QeOWjnE1S*dA2n5nXyi@XtRE{mOZ1h#xwm^>{%4DEM(1Sb8RPSUtxJ-pJDsFDrJFY z%WA>9p1G*CvAV@I_tF$IO}=V2eYlajs5ZB_#kTfv4{{B19=HRZ2Xlg9MGvdv;k2X8 z-KzUzqh+I(x1qD^-Ro87H7?2pgN!~?7q%na`TA@y3qxOL{lefZ{^aKw)CtGp%)Dcj zaowIgg>{)xnXb8sx#CgC*dYaK@6Qznns#rnxDFxy71EP8VudF4Vew64YrM)jp6pSw&AukZvh1NIIlQqc_DdC zdH1QJ+$r@db&(oOz3bLQ&x47SUh1h@n|r2*v3sq@&`0!#U-z+(U8g~>9xp7f>o19~ z-(I|4&0g4EM&FU&OWyQRB?eFRDFZB*uD!mp?tlk!8t2WdLbl;f-F!SLs5uOnC zVOii|5jwD+oMX;2$5@B09hanPTy?YB9uBy6xX#Ccji8QSXm=!PP4n**l80=T~EMGqo{0d*ygiv_m8XfN=`;K>VX~+{j)@@WyUXcJmZe3ey_Wr|IhXg#`rY}- z!EpF+U>qF&EB~bT`kTkwbr{w}^horz%(TqML`cRfGlBVa!)^n5gJA=O8Mk?v*;1ZD z?g&qvC-VE(3OALsLcyQM#HZ@>=ZWRo@?b8YY%1@9$Jnp*W8z(NwSjpmG>`Uo0B09x zC+Ak{dh229b!&>}8ISFz4WCPwMVGU$)yJuG+fn`0dG0VDz4zwh+sdsz{jy$pr-OIG z%kD(f4o0oM5dE`0cE_Ec=bh==_$xg>9U?j@#t(F23_1Ec8XnE%uiJE~gOu^AWOdit zP(4~&p{Tbgh^UXKVe~b03Uq$@gw*0Ro3A}=CUUw7UQ~S~F`25t=L(^f!g45#p$!Yjz{$_~#4|j-r z>}BHSLPz_x-8Ju>-gf8CYtJM2C4E<(Pxp1ei{MVsUf4cD6M>zthE{+&n0mBYpgKUE zOTARRtCp$eTW#Bq(jU2+c$&I;!qpKq8Py@RIXb7B-u1uU6V8i<#HZpk39|TN{d8VM zo=ERUAC%9^BITF(Xnj^+CmvU}oqLV~r||L!zJGj#Tu0qtEYVx)Px`jLAG~w9>H2I~ z@p^UIy#INLy5Fen(7*47@S%NLy}qAWy2O#e`G#YMbAl6tV~kU^1+`V&wc;D~s&;RC zwYKYi+`jFebXl+!)$Q)*_wv4k5KFMi@8i4w-g33K6Bm;Q%b()g_964S@^m{nwIJJ9 zdRMw()@OEq`1|*g_6ThP?QQi)bz=2L^^tb_n$H@EEf;>DZj)}m z&x4PT?oT;fsOzb{H(VfiDj*FR5CvUOe+M8S)W=UyBM<(S|4CExS6cB`p~b<%@sIB! z{EY(`|B6BW#=aCC|IpOPySg~Ix=8**w(+I$5LZ#w{}Px8GSdI^#FwDM#PY8~OVZ>^ z*I{X9Y3fAj{3ZDyloSSAbO=wkTaIv)*LnSbd*SpL6uu4u@sLg8_qVM>ZHG+HN-5 z-70OZJP^Q+Wg9H8WoLh_5Ba40LeC7?3+z@^U0JFkkZj;`=gggZ2ObWbvAVs-#TNQ+ z4jVsMMqBbZnlIvk^8L?m3R#2$<)ePht~XBfW%2eQjqV=a-v|56>d~h~UzHGM}o6`M$-`Q)OB%M>$vMkCt z<-84#cJ?*i`iFL${UjiAjN^Ae$Q2Qo{^w-39Vkixya{K%h`5C9FBWTwF0Royql-|6 z1i;+e#>Y0>A@9@jDf&BI50ZG6q-#G8qT%@DR6mOh$WbQgavKHY` zwWF1SwNdtP7ewh0-}I%?&1{A*gNWH-GN{L^|0Nt0^Z9TN{s!T&V>s}$9V}&nFAndM zWHtPlF0j~1pVsX%ktbv1Vl9**XIyPFwXOELbDlZkeHul#Ng|M>PEMQ?F2r!q@ZBb>=-QXM zw_+Id1LUPe=cfaa4SF?|!>e{Gfr?Jw@7CG0#mJN#qx)1MM(d2MknUxOpr+Sr(UV{g zA)XX_LWX#?2(xLPS#jzlJE0pab;0ZUN(a2_Dw$*C=qncpw&=|!ibsJUd#AEWg#PyX zA0*zbR{0+Tb~SaU3jE^7^H)*a{`KxP;`GzyI}nqY7UD1p3w0oRD3Q(CRC3X`pD?bR z2qvmjzYnxq_Fj{4l$9}NPM(0CT<3?m2BLs+c<}+x#kCxhS^YQ%XwF?`(ad89S;dHaR*h} z!uE_G==djoJpAyfL|3Jm3+b;pvcUu|`b(s1Q?~@Is~g&c$8cA3G`= zd#*mVbkEXU$K@ie+?p6Y&(ItbW_D!cME?G@NqypkQ%{d`fg7H# zorK$Gm<&3}s}f45<`(v0p5cIka+DYiXhGhgxQGeLpZX@cVYWK5SDNefWnMsK17n>c z3Qr2Bm(o~R#QV|}@Zm&T#yxR&uwa8JQu)WZSuHRbuTKPONTAWd@&dS{;dFtoT=p`P zv@*2MIHJcyHMHLC!#0;#;PAyoZ}%gu-AL-5vz<~ABaXZHHmp-Cx{dOx6{i=1q7lhW zfOERFb(jk#vQ&oJn87TCb>zz{qnGFU<3Q$e+EMpB;mml@`e+|#O1HC5=JvC$_NtkJ ztX21YIFomir9{O2KF?EhIPiyBYEJ)>jhj?iRR`T5dHT&I@eC>II|-s0vM!i%6$KHe z&32#AAnK~vj-ccq>cr~hjqk)rO9x3|(a|#WKa0+O=T*D6C=svWaJCh0Od+og%nUr@ zXlN=udi0hit<(~*Y1F)F;Jm}J`>XDBemjAOTn(eJGETy(#R!3+=IvokZvhgk25X%r z*iS1k2HVuS-u1BBd;R$26(ozjbXGk7(JlXQG7yQKn!~1Htm79@WHw%Wo*S7ip^zKS z5CUN=0WX)a_Q&{uL=+{~n}GJGusW(#^8`Tjb9~=IH(Iv-_lk*K*V+|xq;ap0iHfe) zHtFDSWO#Wc&Yi{##~V7a#FI+>J=49)!n^Gfv=MZ(^rk%^@;E;Uo-|7@^4REDoibVZ z_Ry+!W%t<8?z|k6Pgo6>E)*^7$pPwVIC$_fG9|(o_=t2>g`=Amw_CI)US*W}@9YQ% zpGGB>v}K-nQYnB_x`jEzt#B5Yqqc;w8ZeTl(&@ypzhsN_E^uLLKj<98yQK6j&{OS= zS)l*F0(HGOg{Zg2(5mXTjZxki91j|d+?xi;q`hrboC!5cwf`LN{;O+A26GJQL3ZKC zXrKUQkA14&E%`e`igS;80ZiGFt4JRw46yoM@%W^zS7Gjl%_HG^dCnw zv*wh5sVEq#WKV=7r>LAoWxH+7f#yCSn`Hw@?PKt|P}rzTg2X-X95@E_Nr3v6QvHtPM!uNu32dk+fK&g$tL~GoMtJ`GG2@dyu2Wv1m_d zR!A09i_|wOoWob9VNB+DEME8m+kySmCKuVEViD;SCv`_^T4(um{TL0Sen@m{m=c8a zIee&R>@G(RS5+0{6@sc+E-I*;7L&nGz7t^qRGPmsT94*?Xg(lIeB} z`j{lDD7hv~tR3xS>~bY^d0$C@rehnZG?(J2>5s8>V|GQ} z78|xLXc9V>g>A|E?k3j-1`$)R9Zyy15BE|dL6NM;cn@&mu2UL8mkFJ8Dt*SBb@8~z(Q<`&Rb;Dq5C zWy(U0bhKnX$){+JTBZ|tE(Sf;&rQeAp5AJz9A&r0D*_}W(2S&e^o1^Wy*R7?DEDSS zX$eXluYAR&%`0DEUNlw0_-I$ znM2gWKxv`OLVcUbLGW67u3&Nezg~TYNIH)ktS3s{9rFNEXgNjB0?cDG2#ac7p3~Mu z5%Xj*&IF>1$F<^3iaCxON(>m~p_(|v;J{0BeN+3%88EANyL&(Q9XGIKr`F2*NKkhs zJ=kKHZRt=sAWTW(tPDnl-q3~ruDE7h5+<=|%4u5p4X*G$2%Z^Q+t)B*M3L1v7lMOj zRZ4L7Y}MM1cpeWA)DUJ=7Zv^50v?WP%?|8%)kQd$oMSQlVFtxH1G&aCl!Eltf8G!W>dyvW6v1v@N+~g=I>QXp$Fq0_G$#qijHj?$Fa%X70_`F>cYFw zYS$QodL~xyvZRWjvX>~W&cyY2%DyKi$NwCC1Ykc7M+a_X+dYB@-LbvfAlXQ0_=V2Oi2 ztH9=%tjh9)Wp?St1J7R|s@4P3L`GoXO?zW4gmsKDX&IQsb&qpKrkfSIDgYV;A|XQCRFJstt?;`^RXks3eb-I%JLrMJdYw{9x_xS8|`f`Fd7I= z<(v1oEa)L-0v6d+6{*qzpTYRqaE&T&Foe&AU}^59R?-e_i8!N-Kaw-h#`@AY#81Q) zT^okUpUrtM{=~b0<^l$=+fp5F{kR^?I~IEac*x%*lXj>dSN9JDJ?GPC41|GWY5~)) z7=K}00e4*mhjC(wNT7OEyBR+=6A}1TyWEc9`B4t-*ST$GYz|_$g0~A(;55S#y23BkS=}=5{L8BI4 zmmn>c{2*dgjPndL+0kCQFIW5IPO~u|Ry6**q0u%4V*;{2=ScN#%u4sFOrF&u98N5N zq3M|!%J(o-gepQzZ>AQo21xvz)at1&Q>r+sb=zYa$zmJDhoKPdws8$L0&^0EC0YKW z50e&mgT2*5jfqTr1zIG{TD~bqrAyBf`zsIN=31c%9IKH(9KhNm0CNF|!Zz;L!W>_R znvv%m``!%<$er4U!AMtaMEm^4Gw@Ej#U)GwB|!(qsvt0yL4y27Bb`}eK^fYW#$V~4 zWGG_gX3MT#g7O(1LC$RIAW&smBzMkxkwGIbnHJ{hjchgdC^%`DyJN+?N>LJP+xIYY z&j$zJgPo*@;q3GID-gc4QI_N5Vqyas1x8CHEb_v#B_cqLyE#&8&JOTr^`7I&U0>Ki zfS{oee%$%uQEVr&LMr|8DNgP5Hi+|A^mDfp<&7e-E?QcQTRtvSf6ei10p216r2Q%C zh^d(MPRrG^dKk5Zw?nUP7|GN72{HJguUzTW&oiX?W@Xq z*>ZfCuO^{x8y65CbdM3z79BoG)oA-yroBc^R9O1x;qcsGP4*$E08%E<&WaAm&awY- zE&=&pbKMFT=xV@`)>&nFO*%065k4K(C!2r&!~YA%N&71&7<(zOPxX@ z>(8s8-*{*!9DFC-jpjw-PTFKW3@!0es-7_ZdgHAe;voH-SaP}03Ke6ZjMWX_4F9>G z6op}{-3dwh%%<14)8*OUxiIIY$%Y%@ZVLp&=EBs!Nl;r{#JL{e-()?S*v*i?r`u|s z^&2tTN>i59o~r#Saxs|^*iqtT6+~a3cPvUN3QLx)p{Uz9wB3WGI~nPaw8@X(;gOKN ztZW$Z`~EAO)(%oa6FJNf6ymilews^f8GCBOuL*WCH_^JSe^d>IPPzuZrn zZ_1Av6UFf|&pO_mdCQHID|BOSAwsFAv!zKgCtMUbIYLo=-RQ1-j#JF?2-B(~EKRkY zMN^nuB(`Z<$L~NqQ68asz>o|iR7?N&7Cdd^+#o7wr6jgsXY|TmAW9+AC1btX@2f@r zCUcSym43d|3L9WXp=5sqyN%I{9aUjwiEt%|Y+#DjCLH2%xRT`3iUGBzS=;LD(CKV$ zxF0gc?F;xKt+ZYr`TvDqU2hkY3deW~liV_dam7XweU?cC`IR8fEtayikC{;Kz)95W zd-tq((W%Tlr+t-vuYQ@O=b|H7{B?v853zVl1DTGH(dC`|6dy)n!Kp=_Ei&dHBn)BO zM40s@#5Bh&nS>*|lzr-e_Z58ZLD}T+-l^@}yo+#K*cDFvo(7X_-r*z3|uD3Hu1H_KMODiQkEw39?(q*kX&wHDkOb(t6nH>qR&c16;`-Q9DDtZ>6f z52(3rxnrT800WQ$(3jk%&H10V%nHAYNsvIU3Vzn}(6~JxF4Dn;AI}nxPdHMbZboY+ zk2%AktuvlrFS_jgYKi?b8uME=btCiDArVC@s$zi3ZRCr&QaX>B8~JrbZU9@H$RH`n zK@UR|rCVgwX)TATuf}>C_Hb96*(Vnp9b5CDj#_|;ixInoxO@U8C$~u|>5qw{OF7pH zA67o4_iu%+NnHtZ3JyGC8xAd8uc?&kZ1a$$lf6ir-E+qjU00+&Zu^8{|Ji1ocy?ag zd}yv^FM{O=xV0XVVCtUm)gLgw6y+ZU`dlb`>so@%rPwJ+U(;^p!q>Xqg zJf^$Maf_ZQGi~R8{T|ZT-NICHIpGr3%EPqzR3Vanq!!C?F-NZpDTE*m6lUAV^7-PN*t#!jdzM?9`h!v?BBy!VLZ#vC0ENEDm9u8jRkv z2Ld_Rr{8|!1OV1(y~8?4QTVC>V)>+4;iY*YuzHpmVa;l@#i*1-BwmO7YUr-Kgls9F zpFK_a$aexc=|R>ix|{(C`PUwvdTS1RM}1VI2I7~OA5pc3cOxVYHshOt6?Ao*OxR?t z*e19XXkL8<^1+Q|ElU2m_aQCu2kSzmwPRk)lUaM{X$HHTyy58jeoAX{&qdDYUn2=j zOo;&#t%2r4FL%>`YMf*Xs@+*`I-^JHo!!3NuDp-&plmDN)l4mdxR+QEh#@z$)qq+} z9Hmu&iaQ1o0uGx4w5=dSKAOFlozO-OSB+_|9Lwaa6$*mL9{ShkXggz!6J8G{;uP*+ z?F@5W$TuLV^Q*5kpR#Fp;>XVTc`G=th1t@sKk(T+ds@)aiise)fT z2muT4hDmCl-AJ<;eonXoP(mh!&f7!?Y&7`5^(A?0k9aQAep?lrMWx*oJT1Co!qRW| zdmfr#7nn1CQ{UXm&1jf*x}F+KM(!}QSwL2c|MJ_8roUhYKBPo`ZeOoe)L-^y_i5P^ z7cb+?k*lyDEWF#>BpQj9Hwkp># zvgccnQ6YW%wB}*kQ%MJ##u!Hv;NklG);Cbe%w)!xiOJ~2lNj_WHs4P72GV=}u^-_$ zjIY=08edcIRjOrU1g_q3Qof`BeZ=tqX>_eI_W_g53b7P}jeg1YaEPNX*UL zKTP)Ad4G-es2!(pkA+{H>_*UTUWWaU1n_(PWZ2-KJ)QC)lf&%MV$T9wm4H^c;g2$< z%H*b5bD}@X3XRicXjaE#9##@y{GhZj6#bq{Zh6=?vu%0eza4A zh~9Z#jEn^UlifFsi)?|0g6l!!K-QkO)0x=1orp9FCq8p|HR4z~^57++VCCH$h}I}+ z^T%q|G4tL+Gm}Ks($rm7nyt|I!=sn1+GH^~f`}}_XtmD<8z} z#TptO!!4js5*oGeaa(U%aus0G7rf!K(pUg5lxqj|^@Q;=TR4nFP7OgWf(Ot1|2;3-UbavWmV~cXk#3skEM!5ktW+St$cl2 z15zHdvL>f48FeFg7ZF3t(V>M&$2vsJ2EicJ46sc3x|K`qPjT(imnrQ}rQ$s2Lt3D+ zj(wA}>LlxF`iVu>KvuSN*e|DG;wPVO=M*?cix(nIUKh%dCA8SQerH0A8;6Gwy5 zEDgy;&w?8acr)6RT9PZ2)g+$3j{e;iFOxw&@-o+D;AB`pSuDttg9n_HE^LWa5?r1i z5NN&mD*O{67L_)P_fw~A%DWWX3iq(Hq7Jfsl1JcVVX}mdR_vc03t%!+YvirGaeL0j zjjBVD&9o0bF0{iv{BLe0^98$0RU)>ykD8T4GyEMKCX!3cvuF z)XA*0C=eqer`gG`karxSI3AXF+IQ;91?mSUxqY~&n3b@YZkIOlYON4w1d9-NxFjMk zto5M-LIC(!xC^@7_w0?!9*mJ{Lac(Bs@O0fyPUw-2!v{OBjeYW7}12v;32%wQ2<-` z&j0|qttm|*r5o+uV`6^hERYyi8;tv6h~C@hBFfyNQ3YAJjl7SuJg^>n#5@`I)A z^uMzte9_uQP8apRPr*>^|1dvLI}P7qB%^V_4Z^1P_0!CWd~0XOH&;OVb+|{9!R2j$ zO!C=Sl*5wV>)z%I%+twKZ&sGPa1+zQOm*kP?&}2W8L*NozP7qu3<}#?AQsWo5?six z(17$oXQ3&>+_kh>|4VdTPto@8InO2ImUQ?tD^pE5%N@wvbl}l*D&-s@^D=YYw1GeM zMbh$sV+uSP8+PbjiCC{r`6sc}!|n`6&U=AmkN08E1ZglA*DlP7>61duBb$DuNytGI&(V!T}TXLH-Gh2D~^@ zypZ`t$t&kP@tcF|i+U>=qDrm6fA}a2h#kOjnu}iq=q`|$x|FO10yMU@yv7B+QN&Dt(-(0tW&rekCEFCG0_U<#bNJjuf-F|{bRsE!~K zP#(D@a}r6Nj2E%&&s*`P{IOWpX_PT?3_N5PFGoz+`hzYhkM;X$MUM-cw{9k!l*#Sa zuK_!1M8H3m8Ft~aB?jy}fPP+<4cj%^)YQS}JZzD2bSfKh zkU2aK?Q5CR!f-c>iL2*ux5WK^+}w36?;HZ2#wcoHg#JK(zr9Ygnr*iJVtm2F^p^3j zi{4K6w|tD1os&^9WaV5_hZIAM!{lGVPe7Ez zxJR=4<-s#U@I4o`dUPS#k7nR104D<#u1uwFKDV})AHVN&h7*omg^F0njp1ThTD zKWAGIfHhlX>(CM@&ZxQWd5iWF7ekaFb+pV~BsA5!7!FIbRXDpyZcL;zYVJx72i%`2 zG`Elv2M@1x>$qq;ZS2cZnwgf0pkVPWS6~YUm0FJ42Kky;|F7OZ%8YN#yxoK>9qETF z@%HgCF&I|yavjrZi>ZTy*MZs(xxxl&T+md9&L~3c@GMC5jb>`C_I(F%?OHo`kaiC z_244p=uOQnotY48yxlX=xS9%w6gZt{=ooQZ42lRW6tmMynZU&=tr9DHcgA(i|zt?S$#kPRm0LSEU336Kuk*Yjij}Xvq)(szyEYAw?)IGg?f33aGaW|y*uRN9)^=Ddq5Z(rP@)xil~5E4IsmvdFRB%X=r0NOfuZI$4lq>`+jqkORM7FN z282h_lcdwyrerg^8Ki$6oswCV8>e;fX*cJJB&lESz;~od6;S-tKoa`8v>)DeJTB%} z5BeKD_n%Xbmf`J`Q~`PePE_<2<65d@s~|_Sl3fUm6B2uY&$jQuX$L79EUvR+DVqBd zoSKiu_+@_d|MNNOZ-J8~fpP5XWex`ic;CPh%DY)fqN04nb;I)oqv(!pQL|UmP8^Jg zKkb$xXzFnQ_6d#x&@?#n$eCYwET#@36qQflFmym)9eE;+6@Z7u;`huaX$J7=te|C4 z7Su`>c-pn(w`cIl9b~Y59s7lws_39pH;ILvcD@Y3=h8js;cRL|$JJo|P$L}aL+#as zlBN^)iBcsJ{HBmIu6=-5Rx zTR2?JJ|}1gDRme+Jo2Te_SYirg>zdP}tW zSdCs5%QC$9SuS1VTXYFG&7RYXiv=aj+($OUl^L}GP(C+Mn+pyp?GN>!Mqwv|M|h^+ zm`^`6)p7s zW-TJl|B@dtps*1&aex^Z3_>o@X{5{$#zTVffBl7(>cTqxD-iQ-xV?!Lcn3%6so{>F z!K$ps*!%POwn9YKCefC4C8Np4FJ;mg?GXaAHS!S99Q7t+XD%00tScG=P`b=6WJw*Q zAHgo=KOn-Bg%c#YP|;;@KSz(RgoX)lytvqt+}SSKVV5Tl3-OUunugAI=4qAD(_pD3 zV^5?`M;gO>9Lv|5!)$2@YK=db#mp{`99rs6I}5tfh@iz4RTC_71rK{kEYld^L6rr) zr-mIE*rQM@jQmU|t^#f{Hci7Fph}}|uwGJ|3j3kQ4$p_!xof2`8iJrxrGzcBc%-J{ zM8F7ux+~D)gVkVqO;hBr?A2C0$QiUby;-bUpz~GwmZgqBa66YrxqUt7bkt8H8O&AL z!_v4QXK=zVOCh7?*>ofmV?2Rfy6&2a=3OR%#7t?;>Z$Fz*2euQk8l-ZZsUn;?D3VUWC_#DN;kW4 z%9D0OKNaQpdz7N}&E}uy^frtNj*93Uz^h;=7uuWPV6^-S`rT0_Wg&<;osc^8pBdqW z*gz(sish1$9{_1C_T^b$T*0Ej*}SJoUE3v>#UsBg(2O7-o5~0_|Q*1WfXw;)ar?$gJ z0S*zQ3V^DcD@X7nt_aU3SCv+d?|WGd_P`zK90$H)i_5f8+t^I~7lo^-(u{3ZP$oGz zz$}>S^kdm|0+wD1(-^;eHnzidX)vNRpJIq38*VFqW9qw{RFe`Sw8Kqvjs6kQTr|hj zg|e1-rM5FpuZ`;bj)3k)r`%ex6cFiv^JMfM*R3Gxt+0QX%^w6x>)gL7mr%pn&n(<6 zUYUN;b&WRt6?Esp$d-$mZ3SUmmWt`5A~h|rfx3Q%_GQ2Ms5w+ z#lq`mCJ1)F*{v&zFO`e9D4qi8L`AdC*i0RZKq+ybf-bQIr8QVH#^v+z&j!W=JNM0U zg_R^N2*7kN3yTTOYE&t+e$h@nS|zwl*tK+nF)QWE%kn1XzMLsX(J4klVSlJ#&oO`K zqv);CkwBI}RlBw3!1*r1DoE9w~hyn3wdHs zl9yYY(Y!JDEm8dOs5;f=I?YxOEqKUv*W4U=Fbs-4glM*g|at z*jD319pOY^q~$`myVwD~d(**zM&J9=IXt@kJ%b~mU(ICszrO1@Y?G6QGukbIY-An6 zu&utwWC*PYP>a9^bld-7!_bcVAzNXFfh!Fu&r%7x5}_=egu>I;VdJs86$RNIFQkP=4h7{&{5u>WjrNbCo!ZC)TM zSR1WFuv~y)h*?|y`+=2A27%C}JW18Cw`Van9}|T$?RBR%L|%U@j@BH2+bu6OC?dfC zB+vL{gBa)8N~M-Lzy%G(#6s8!327%Xr9FMGoFgEi_~DiJiHB#}-3IZD>+)r1my*g9N9HKhyW!dwP# zX*Zf>>E^?DCXltT0Sz4A8QpO%&*_g8hF@|CQrpP}PY}QnH@aetNU*8k{%p;M&`g{$ zes>2Q@;Wf&wOr$5nQepZ0y#|d!xB_L;@t&9E3vM$H|iIV)YvCO@<8M<{C*@$U^#Bi%|73nO3Ag8>StS*~WOp1&N|yA&HLvT1lFY2DC@Ki{ttCBw z75Kd)pjODZ3X!-^dGmiW59UcW+&}?}TlU_V7h4UsTElpk7g-imitA9AU?9khoA(8_ z^3+Shz##k>#3of|m{=UHFKO|+G}flwqr^g)Z!c$FAVsiWA-LV`G?2Co(h0sC9jYmY zPEyfsO5Yx#B;^wsy9tCMR9(mk7#Fr%$#(QH$&Ms1r}Ind*j9`Dx~sUe?IZfZa`J>yL)ColN% zb+II2(d|4#9&uyk#R>iBVP2IE0+^gTet55AUEF_iu|wG?QyZRDU8T2d%~|u{+8-)3 z2X~XC_`I@?i+Aq2p&j!HqSv#3v%T<{^6g6&^ABMK)U)7kt*et$0v+iL0^j43ywV>n=x56l>;^hE>28*^iB)c zS1e6J8LTo%5qS(CF?s0*$z^Vh%}i7o#3*j&_KTD>xht;6cdM(HZ)1C>+zQL9KD{|uQ=yR7>k`u1u(k8W+cW> zVRu%mo2%_Hjc5|D(u&#=>NQ*-JDKJfi7vt6<3!vNS!WG-Th`Qj=w2yq6#LjBv{ev5}BN(6<# z5jx7I+ZK-dM*Gh1_;T2yT_z4^Jv@GI!VH0Q6#y(Bh~op$x8fx9O(~XSX6*MP=eON; z(rM+xhBL=zm#PA4wI|~ev_ECR1oml7)Q?d|?NTs5E3tP*K*2z{_nB6A%l4xtMsk_D zpC~x+z`Vp=IPO+geHSd;_3ls2!(H}68A&T{Ff=!Lw_TDbb)(HKK&V?OlaA(!^Nky@ z9GLUyNi2Cu)e(nW@2Zz$J93J-XfC%Wu$|=9xpn2Ag0l@mdq&C9i8w%IjG%;0nY~;U z6Vnu>h4)zSm}Gq#;QEWNw-lGN6V+oH)IzYZm(XN+*smMyA?NfxR^f4{C$5$)C%dx+ zSin)pClLgy(0()0`mF}NTc@gef?Un1EbfL{#C~da3-)<#aaCS0h?FkgDB^Q7>00zU z$zdRLFEC}R%8m^m*#m!jm(aKc?5=Ms72VrvkmuK@M6)@FngyC|h5FulbbA4VFFn*6 zk?=vdm1rosCn46{opux4?RnLS!FDSyVu*DQGEdO!OTsf1mz{5-yjtzUT*Sp)Y=R2i z%wsZE)_}|-BiJ>?goY3h3yAMB)w8Wlj3`VjJ#$?fIDf(2DezRI-OD0)1xP2LE}LPO8Vb4c{DEGA!%@A^YENo@xU%pR%;GAn`q>8- z>P2OH(N=>0eYzp8BmDFIQbsvr_rs6vv2H*hw;6#e&sJNyI?!G=<3u0G6p5&aKQYz} zu%KDZ=}z*(Cf+PhzUdz_oLMPKE(%!*xaRJVc#zo}xT;gNRKDl+S$q^xA;?`HYqJ(U zqY;C4W=rh`!Joa|S8GtMX3rZ1q7r#ZuG0KuA<1KPx|2c8(a(q|wL0`&)L;9V-szBj zdy6yyVZFLKcjaoL^yS7_-a=2TH*YvOsGO8f8VLiv(fLJ zFirYf1R_E}(H)x5%=g8t0g*rtoyffi2#XV6mbkfi+`8n5o37)&YCg>3#1Er#uOi?@ z%g)p1s=CPOfP`OF-S8Mh;VseKDB;ok%__HXzfHkv!70u%j>m++yrDWkEmecv?bNZ9 zL8F%^zm$q0SM(q&mwd;JmYe|$TJiW>V67J2^iIt${^MS1%tXELY_(u zkKkQ)`Vf*ucoA9A;6d7KLz!C&7R4S+k~xN?2_PurfvcHK#;o31W)Ij=X&}z zFykE4;}DMmOZQYbMcPc&DB^@3B>GU}GZ*&q!yXZqE=Pc;Jwg8{@Shco5#j(=o=J|F z&!wwxy-s8SkLy>V2uS2mJ=*lxiDGohLhPDm&vCBY#TRk|Mx}CdtbEN=Y@Gne4N+>y z{zVwsO-Av<#jJ)gd?(p}x8{q;;jDK4Y6>u}OZohQ8`q~+9+8$ikO`&kTqWiSE%wnTt-2&M4u@vPsQ}z~9mGF@22kRhXM0EEq_l z5Jy~BI+05VOx*`nP(nYo&&0=a3QxC4RHNriXS_~TT?aInu})L!>!n4Jt;_v{!g2+9d(P(PH36>b%)GAuLp6#tXRbeIoIap+!gTc)z_ z4I2g4MEv`%>2+*=J$e|u^bRo2eJ*Am*9co$TqU&ErEz`oh6oD5XT%e;sy} zzq)e@wG5NEG6681gt6FE{f)NaPl(t@7EZs!x^m7;FomNL1hl-9DWm~stv=y`$@?Ot zdwTdE*88&_apK6G0aV1!=t8rcc(}w@#A}sy3}Z69D`EGw?ghS0;^+VQVzYw4je;i3 zaG*YpUoeOeSS^IpWK~{2u(ampvTn^fwSzKzT9(4h8{+#k7OU0zuK5fikT~TL_{lX# z%G(*Mf%-L(dKq>aY|dRCK`eh~L$FU`WyHN&H8tGoECs^)_uf%n!oX_+2zbNKBD?_~ zJP3{<^|u2l{(!GaY(`zJV$d`~eD*eQG_u24N=>RAzL&+C>Kn`5*uB>&DXJ9k<)wHN zw4huWn=@(vrYTT-#=cj>2{&o7V09+e+i0q|_690?6SxdeiRQWxF7)HNV^sN!@(j&G z3ru0g$`nI+5;9wh3=XK+X#y_6D zSa%aeP3q~HW}uLJWu{HibGZCHK{Z$fYZYzsLwZ^T-)&H{EWgVG!{C3e11=tpX#!YW zpQ4SB{O2T=S5?&lZaNk>U$Rs}2bV-2f4pk+wLuai-GH#%E`hN)l&C`ZAsvOfE#S*v z+2v2S&lg1xdI%;s_%ibnvdZEttd3&d{INbiJASBVA0`8F}ARWUjH> zv?pRy$zL$??De}SsS&;YSJ81AlkUoF{@1~0>v9L3{pP|IO5kLFduG!6e$oTz+}F?7 z=0D>lqHJKfW1e0w`5V=0sCYD!P-XojfrR)r{|c{W3yl!o=0gSGfak{^G!% z>^(Z|{cw(16uxY%&%viU=hf^d$x{Ue+VD91717$OWMSh6YJna;n2*swmX4U3KXw9A^~rGq&xA~jxM})w z?+Q~^$4{X;exFG4Q%+6L5noXrO3bP1n8L)AAhDXxb5|%&AqP>yyKXdRd6?T3T+h}~ zWHiz;QC}_d{e;dADaMKSyJf7oN(uHfciO0Dw8;N31*3b z*Q(xs971R*Wx|&r&5v>hV(V~j^EF+@ciV`yct)C3BuyXeQM$M$>piyd@p!>9sT22%3wn?C?I6N5@iBXl@6($bI;Li|$7AKw&EkN2G&=aRV2^5d5*Rp!N+#<#8{uN4azkLxLh}Ip9o>tC zY)V49$Jg*FY*ozBz0hV?=FCv--sGulJrObb_L=!wG#J zZlCu`T={btQ5XLGJq?MGmhWoY{cz7@Op~x;bcae(gGv`9!b+z&i=y3mcAl5?S#w~J z{TP5>U!lo}6wuR3phW%79nMi(43hDL;XiVC75sQDAYOsdjGwpT@bmibEOl*f<9$7o zL%Tz=@{IRW4Pjtv0JC{sX6sPaC1*{}B z7Qt9VZFPVaP!ID!p}s7{BJgwFx=`RLcx;yV?pr{+l!@s{+Tlm*zJ~95u5`ST@8^4g zN9KYiJpL^?j5ZgXTj~C%LGP8*aSP|H8aThbqv>z0;m#k%F9ggA) zLP$9z#~X%4B{~g#+#^5o;_$>YZ>F8$?Q{5vhj&VCe z{;{Iv%cu_#O=hI9ov%X$)Ugd&;sDCMs=i!#z7??4nx&_2x!#;V%n?goyV$RhT}zDu zWf?TITX8%L6pk0kc1Mkd0e}qt|LCU>zN)ptG9l|01KHs13>KL<8EhzU=CdTw^D|tS zZ9HoT0;7qe$Nr@@ufeed`a*ASYrc>|8Yh!Of%oWUfbO;Bo~MgA;dd?Z-E0P(_&pAS zXH=_Mm@M?aS}>E-k?s<)W#b#B?9|D zS`hB66%``l!46PGoShh;faXM!uzy(*kCBK?G5*DHMZ^p9Pl{`5P%L5K*@{rdgxr>L zH`H?z0+1iW*CdtAoVWkyL;VX5JQi3jB4zMilp>X!JC14D^zl74kcq2G%F^QBeH|PW zj1A>-*Q>q2e|V)fU{y<7rqqIYh^FUGgwOa5WizsCI?h|{B5y8r6BbwcWtwW8FogWm zY_%O!qvQN;R4E1pMZZoiaso0H6~n6YIWXG64SAGoxC&K|q|#AS%nZZs`Pfj!{o|*9 z^dPhbw6N78OFRIlRsid-+9jkM7hlg+PVSh+0#KibF+;Ls^qyR}AqOc~-t8v@y4g~& z*+Vt;yvgJiv=);J2$hhP(EiIPd9HhFuPc$24!+bfawB+~25mR_;D5$DcEPJIC0=xi z{H>S{P#ueSM#Ubty1}v?x!9HuWb& z%;D&X)?U5`z@z~|iy0e=3fXvb5j5sJ16HMuc<}{SSM$yNUnDI>@uWiq*ni&pf5rvh zf~unCF^>)pnA_1ap`Nb{24$UxLJ{U2d)g%g^uB6S_(t;b4EB2Q12a^}6v=$Lf1Ol% zy@xq>m*i)dDi&iFx9g}carmA@uC@WuBIrlwkbXKy;GA$x}yq$vgVcnpxu zOS9}tdhDf5-p{0Uk!%eO=BG~AihXJr_KFKUi+1F2C2j4*k>3Fnvqz^ec;jkW5E_kv zO$R?1<;8s=_B(V&QB3As;#&nzc>q##;fjag|iQRxV3ff(a6*V79r%2FYEifswH( z8m)%`QyeLhL?YaPP!WD@lQC%Qjiq8m`N&YE-ZWr>8Ca~aX2({H_!~U*s>v-UXpm%b zmdB`QRTsZ;Kapj8tv47R^UGPwqVB7u*yaIb1)KT=sv+vdErTA72?c9nt1d#d4(8!p z4t7L;{s4}?9`xwd64h47b*maoa{vc>$zL}@{J;KvEa}(6+*H>67OuBM?ao;S_e&+8 z*uIu$@=z~(s92BjkkcFLC_FQ0*2 zaD|(WQK+$BFKd#GZK-{h`DH$T7M&VVaxr9VgWui)z8c-VJ*Sk^$+DDCG0magyMU!* zUC{qvDOpLXQ(d*hM+*yCCE}(%o>5{TK}L)p&$(&KOqA75vYnY&|pth0ft%} zBZD`3h*k|5SG$5jFRhv$6U`bG%%CkliCTQrtqF#ZCwq!!Q#V*@-b+>Y>oMwseo>ql zjan;_+jO7)rjB@h_Mov)3s7fH!tRApCIJ> z?EWx|mrdPToJrpgqlJ`hl#r3uw>#N$J^paH_}H1VOc$CdMOZPm8^lF=4)jS`Dh!Y0 zrZ;_nZT4aGdYLQ)xBH4#!bFJxUTv;cZikvpKMYFCLOcQ7_!YP+5l0-wIo-;~@_lC! zS$B_Uj~md7zELO2YULU+9XD}hNq|0TD0;HB-I;^TokrMizWdXVVB2a z8DnIe(B`Nt&A%Xa@<^)jJv+|8p3^Q5qQoGR=c5aPqudTepobA7-AeRPcu~Z^ElIE} z3yEw;7;LKl&BFAA^iJ>pyfo&N^d22XmE7Mdp0J@7YOc zU_SYb2|gF}aaWb}Wnzdj>hD(8BEml~f^WB9Kn~$$x@zj-MQ*qd-LLcgf{|7aJm?}J K)LVU4SZMNPYqQP( diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp b/src/documents/tests/samples/documents/thumbnails/0000004.webp new file mode 100644 index 0000000000000000000000000000000000000000..a7ff623b24a24c1490dec1742efb28ce7da545d1 GIT binary patch literal 2624 zcmV-G3cvMINk&FE3IG6CMM6+kP&gng3IG7`KLDKpD)a%406vjOp-UyCqoJiWn24|v z32AQObfFvbd-1RJp4C6aep2<=`Fq-~SUo}JSMqgTjulSz% zUf4Ns^#b*m>VNIOzxe6uiQvCr59Z(1y)N&WpeN~HR$ub`n0;yNBmCq0xBTH$)_p+u zH5g^;>IcE7!!K7*J`F}0db)w|YB0;y)DDcpile-!BGTR)&60>d2p)3sf6InmuAqDw z(zfHF^ZC$xro3zu6@{+aHz-mtsTgJcd<38+QQ@mdETJM%iOZvQy!xIj3qkyA${-N5 zj83zqC02XQNxa@6kuO{uWYOk37rSKg`K1qE#g|5wtU+|~?81Gud0~<+;GL1UlnMNa z^YX;l)-rD8ie|?>j8_ku>96IT4%vmuYq}*fiMEo@CUpRSgTUXBz948*b@7D4G?+{oB8)00C3a`crLiAJMIT;)F0Q4@pTg= zk8TKVpSj9BlOD^U_Vtd@u7Vcfp9Z50yIcE7!!K7*J`F}0db)w|YB0;y z)DMGEhF-3qd>V{0^>qW_)M1yas2>KS47C6N{{LS<022BlC{>70i&@QiW1W$cX@RHh z&$~wT&cZZ5ZzWxr&}HBKxV?IK33{DDzuXf!8n}3C7gB94(TK^{}EA}4n>7Ny_jMijR%-xoD0JK1< zyK!Jib*~}pltq>L8HnpYIzUk?U7LhyFOmM&;|Oaxa~Z3HP-s^zLrsn#NN`#;_GUTb z#_n!>RiP(Hvb9K2Hk@AT!spjPHv9q)i9Uf)E6V$WuUbKH@;b%pn^qQ7wH%ZNW}0)w z^)wQ`^WXd5QE_pS%vl$vBN~*Kf|1vnY61ukCkU=c=WuddH#0*TXMPx(hDV$KU$9?t zL`#!I*2q>euUU5S`lpk!qAd5ZRhy-VzxSa_Ni>LBnx;L79obH{#=Ek9gBFrH58KRz z#=)6S;p~6=11R3l$2Pvo7&{`sQQfb zOLbS48SXz(_@ThOkBlq6zj!KZl-FN}lQ4XA7HyfGEi7%ZjhRS~Q`_)}eG)=3yC?4s z5(*>Wv?@8<=+_CfU@&q}TNL*l8N~_>A-c8;(=aXtM9-r7ad)ImdcAw6dxW zHOgelATJ~5IScJi`6*Za&Z2wv_?9Di&mZ>0`}6iUi?H2MoxkW)5giHsT|DilWJSYF z6)stGfpvDp@j115D$=z6n z1NQQ%yGt(wgViY8yNi(~DXe2*aR9`R(HSqKSx8cxwEyZf94ZJ1NTyw(#NX9w{y#?he7v5k8d`_t1RdIrvWe+Xj& z9oEoqKHr<;{>AnSd zogP_2+qlLnz5AZ>&`>GF34=)WG26*ow0t*dkIC}WW??y8K)T0YOP$#88XSJis;)y+ z*WZZ0A=;4oECTVqLUYyE7D7`>x%e2G zo{0(_rv;S~AWM5T-2JDuSS0Dv{=VU1e%Y|=c&jeRFr1`TRNa3DQpjy( zw=NLvPru9rt~Z*{vndHkP6F3aDyrsm_h59Zi?FIim z(it9Y+jd%l9+(yb00VJ@&f>q+jT|$+?Z9-iqY#k~@(HG}3kJ3P z!6}u8ApsDt)v-C_yDz+g%y_M-JyR z9c!iH=&>aHv6tVv#Ajj3|M=^^QZUl{D>D9!Q_^0L9!LSF1|&vuvX9vCa(JWBs8wiiWY2ZMY7 z=Fg#yHB6Uauc{ARHobWbgz?8I@ewfUyN_n#_Hce4J*GGX1dge6M@dLdOnp^IwwL1% z{$)m7+JGHYw_OY2m1jCU4_>*%di}Z&5YdW`bFhv0jBvu*{T2##(O7KYCLTpg3R5KX zFG=K{F4X%&n8MI7PsvqQvSUE(+d>69)d5kDpLU4(nI&Du=l;!@7H7BZ0r3|ZvA^vG z_SP<+yflz%bRnTrAWxtgHF+~K(}?AU9FTy*Ux3ZPk7Mbb2lUfcp$H5nn@|2}8Ajvx zfp0~o9C*82Qrid4sxr?fhFjr?FOf9*Ou&}io*@Xhb|8=^-OKRnG#j<&uUQ{nirzGM z%c@AWw^JRP|7d|%)Ey4>(LYcw%hwYkcXnfJq`W64oBzs-Fuw`^h}H)4BY7n|!WYX0 z*qOc&(G$0+_JSNHOUL!#{jBH~3PCqOqswuP-fu1a{XN5XCZ_n^l&yQ8X>q3eQ@04? z&3h(EFCzTACB=gUy=weuxqRZlXe%;B@sVnZD#7eSth1XrSgHT+rY{TrtQBa?BFZu8 in}?oeZhY4Q)o&wyi3@~zh-WNDh`8wL1V8`)0002IHZUas literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg b/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg deleted file mode 100644 index 3abc69d360a4c61dba6b12e6c833bdf6b441d8f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2712 zcmV;J3TO3<4Fm}T0-;;@IO|&ujsMc?0VS)=twtf`xvyXfcqt3jXqTw`8{(PsHlqKp zvQ<;i!TOdU2bcw#F!E(Q{3@R^u)Y3-1h2kRO5MLj#bAru`yRu1G5vDd7PU!coJf$& zTxKEi9D{7jbK}m#R!5(e0%xx+*0RX3RrG}d=Q5iaf|6gcnVa}(Xx;Ks2&5DyY$cjF z8_?s8N%6p6OrEM-l%l@KH-xkD@h+$dQqZRrq+i-!E-oB6}jkBv!JeE82u3Ch`GU zHj8F*Fgu{N5=>)__tjeku(lVGtpx(RJ_Nx{^)R9fyQMJTSQ!3K^GmdUU&5WP?B=nY-C zvbUQS;2kW^Yn4ID=j7ts==&yg|r!_R?hMKkyA6ww3EEiyRrw9vDz=@mPK7hV2&sE3`*;d zLSO|aprA{H-2n^+w^UiSzfK`evE~|b_<;mwlf_k5@!M6$h&O>Mx;Qzs?FPIYtNSai z(28VwW{7v-;yircu>ON&>m)Sw6jR^Dj9t(IRD2*xUXz_NdL9VI5E#gtj$tiw#g2{h zARnmicyVuuet!n(ZGhqHT9>o2OY6pxGhP<<8^+2oi^~3Vr_jZ<&za6s-idEYSxL>< zXLqSE??R$UoB%7nt>Pg6BVOI;w)!Ef zt1=3{NWrEGPfT-{+`o1f*3yB^t7U8+%<1%~^cAgJGz4<@m1}_MMDlcOxif3Ve1C2J zsUlW?6<1US2(>WU%4rpsBK5#?e40j8$ucA1IxbZ;TlJgr-2HEtbB=w(GdK9IMh#Fh zFqH7G)gX>4y0p~T;Tk~6EYTIxYvB;F%POrqNB?kGHY}v2e1(MNoIH2%GRjC8{@hbdLH%Jv zUSUQ;AcxN}*G;dHQ^T_=yPkrX<$qW((PCcArQAMVDgkWhW0&5u z-Ul=tZ{1F<)uv3%11uzx-&=tnw=V;-%Qp1b2H*Dxg@k9UHfN2yw1!;bRSVwqJer$m zX;nqV_Gi`6d`p81+V_R;B47$wDs92=OXW{rKnxF>f&0_kBM3Y76~n#8`2q zQ`zzmDkr*ATK@QHx6L&74mW%#u;Hv+h)Ql(_hqWYT0I9ubRfY}gSJgt`E*?Y>FHA= zfVhoB=!1Oj8J^*x7T9c6I~sVmf|OJt$aq!mG6_fuYZDJ^_(OEs>N@M!JQoJs=iD49 zqb7vc;yt{Ve3XVFT}88CXzdZTf^7|wnP0Yd=S|v3Rqr?6*gIX`N+2vY&ND)i5fCcr zJC%Yzd>`w-tqU<|W>#u%`y0RMNS$QFR;0-Y=u|0GjE8g03^Y#`ca29@-dLPH$@khQ zqzdZdMl_e>XFOHgB*K4W%tWreelKo7GTo24PaDOFDE{(hYE@n&#F6CL$8m2awzmJO zQC?7zcWaLPVcPTx-~s&{`HwB)>=f35S9AEg%z6-YsZILDM%iV{E`W++)jaE?(9O`^39Va}nI` z6IWnJ>9EHR5iu8PYEUNX6n(7G7Ok#HmroLISz1D2ZA&^Y?T<9QaYy1m9+XQeOpxUr zj3=vj0P8dhtbq7l_E>Zp8|?;=zZ7cYZ`wP7wwHKOud1P5888IrCkVG5q`sXK@L%ny z2{{~A7+q7hO6(EuD9bl?L==H^h!g;yw1w!z%x_Au%pst~#~@4Ki>Hd7|bHnq-zi_8C! zO%XY}qI*2vGF!R1Gr;4}V^HTX;r=_7^SBstf?W?iC4!2LD}b58TS()Ap%XshA4y^g zBn|YKBr^b!QE6%_2l?;v6f=GevixCI)@>_k>N;I}}{>C|g(zU@{y9Q>T zx*QtRIG(m_PWdDhIz@7NG`DVRfPr=* zi=s}T3rW4^j!Ir{2Wbk{b;m23!ARENWpotEO8D!OloX%2 zsl&t+#XczG0c1sC22TI#%}}k}+2Cr|nM>H9-l_2nG~R*KgRxv473195Kln&Tf1(|! z@ZTGuzP1zUC|{J9AkUJts$;6cv>%VB0uE{%WaS+hF+%=WLhhySU; zPx+!RGNuu*lm$EHiBxkQ);u8lRcH5{-k*YZmHf_8+ztH0hhgS9gx z(21r9KB)V6nN?<12(!@Dus5tl-t-NyCxz_O?5{Pszx<8m9HyeKkT{!3&{kOmj;~LlnSE|4HWSO@)R<2k5(F*&X9e;&hKX zl>Rf@Aci=l8*EBC9ti1)zls61z^IhP-&97Q?FLFctq-a3$Fefg`AWA};d&H|HN*7p z_c*#dMKt&+T#7N`Nr1P2MO26O!7mUT^K%4+_dm5I6Xa(EyVVPa>yWLt5OR-u7r|U4 S|3^~Dece>pl*7~{#c}PaRZyY; diff --git a/src/documents/tests/test_checks.py b/src/documents/tests/test_checks.py index 4af05746f..304074e37 100644 --- a/src/documents/tests/test_checks.py +++ b/src/documents/tests/test_checks.py @@ -1,4 +1,3 @@ -import textwrap from unittest import mock from django.core.checks import Error @@ -6,60 +5,11 @@ from django.core.checks import Warning from django.test import TestCase from django.test import override_settings -from documents.checks import changed_password_check from documents.checks import filename_format_check from documents.checks import parser_check -from documents.models import Document -from documents.tests.factories import DocumentFactory class TestDocumentChecks(TestCase): - def test_changed_password_check_empty_db(self): - self.assertListEqual(changed_password_check(None), []) - - def test_changed_password_check_no_encryption(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED) - self.assertListEqual(changed_password_check(None), []) - - def test_encrypted_missing_passphrase(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - msgs = changed_password_check(None) - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - "The database contains encrypted documents but no password is set.", - ) - - @override_settings( - PASSPHRASE="test", - ) - @mock.patch("paperless.db.GnuPG.decrypted") - @mock.patch("documents.models.Document.source_file") - def test_encrypted_decrypt_fails(self, mock_decrypted, mock_source_file): - mock_decrypted.return_value = None - mock_source_file.return_value = b"" - - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - - msgs = changed_password_check(None) - - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ) - def test_parser_check(self): self.assertEqual(parser_check(None), []) diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index befc7050f..f6764d3f8 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -34,22 +34,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_generate_source_filename(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() self.assertEqual(generate_filename(document), Path(f"{document.pk:07d}.pdf")) - document.storage_type = Document.STORAGE_TYPE_GPG - self.assertEqual( - generate_filename(document), - Path(f"{document.pk:07d}.pdf.gpg"), - ) - @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() # Test default source_path @@ -63,11 +55,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Ensure that filename is properly generated self.assertEqual(document.filename, Path("none/none.pdf")) - # Enable encryption and check again - document.storage_type = Document.STORAGE_TYPE_GPG - document.filename = generate_filename(document) - self.assertEqual(document.filename, Path("none/none.pdf.gpg")) - document.save() # test that creating dirs for the source_path creates the correct directory @@ -87,14 +74,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): settings.ORIGINALS_DIR / "none", ) self.assertIsFile( - settings.ORIGINALS_DIR / "test" / "test.pdf.gpg", + settings.ORIGINALS_DIR / "test" / "test.pdf", ) @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming_missing_permissions(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -128,14 +115,13 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_file_renaming_database_error(self): Document.objects.create( mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, checksum="AAAAA", ) document = Document() document.mime_type = "application/pdf" document.checksum = "BBBBB" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -170,7 +156,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -196,7 +182,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_trash_dir(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -221,7 +207,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Create an identical document and ensure it is trashed under a new name document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.filename = generate_filename(document) document.save() @@ -235,7 +221,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_nofile(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.delete() @@ -245,7 +231,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_directory_not_empty(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -362,7 +348,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_nested_directory_cleanup(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -390,7 +376,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -403,7 +388,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -429,7 +413,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -438,7 +421,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -1258,7 +1240,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): title="doc1", mime_type="application/pdf", ) - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -1732,7 +1714,6 @@ class TestPathDateLocalization: document = DocumentFactory.create( title="My Document", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, created=self.TEST_DATE, # 2023-10-26 (which is a Thursday) ) with override_settings(FILENAME_FORMAT=filename_format): diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 014f5d673..e1b88633c 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -1,7 +1,5 @@ import filecmp -import hashlib import shutil -import tempfile from io import StringIO from pathlib import Path from unittest import mock @@ -96,66 +94,6 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): self.assertEqual(doc2.archive_filename, "document_01.pdf") -class TestDecryptDocuments(FileSystemAssertsMixin, TestCase): - @mock.patch("documents.management.commands.decrypt_documents.input") - def test_decrypt(self, m): - media_dir = tempfile.mkdtemp() - originals_dir = Path(media_dir) / "documents" / "originals" - thumb_dir = Path(media_dir) / "documents" / "thumbnails" - originals_dir.mkdir(parents=True, exist_ok=True) - thumb_dir.mkdir(parents=True, exist_ok=True) - - with override_settings( - ORIGINALS_DIR=originals_dir, - THUMBNAIL_DIR=thumb_dir, - PASSPHRASE="test", - FILENAME_FORMAT=None, - ): - doc = Document.objects.create( - checksum="82186aaa94f0b98697d704b90fd1c072", - title="wow", - filename="0000004.pdf.gpg", - mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, - ) - - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "originals" - / "0000004.pdf.gpg" - ), - originals_dir / "0000004.pdf.gpg", - ) - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "thumbnails" - / "0000004.webp.gpg" - ), - thumb_dir / f"{doc.id:07}.webp.gpg", - ) - - call_command("decrypt_documents") - - doc.refresh_from_db() - - self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) - self.assertEqual(doc.filename, "0000004.pdf") - self.assertIsFile(Path(originals_dir) / "0000004.pdf") - self.assertIsFile(doc.source_path) - self.assertIsFile(Path(thumb_dir) / f"{doc.id:07}.webp") - self.assertIsFile(doc.thumbnail_path) - - with doc.source_file as f: - checksum: str = hashlib.md5(f.read()).hexdigest() - self.assertEqual(checksum, doc.checksum) - - class TestMakeIndex(TestCase): @mock.patch("documents.management.commands.document_index.index_reindex") def test_reindex(self, m): diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index b01b8d47e..81262779a 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -86,9 +86,8 @@ class TestExportImport( content="Content", checksum="82186aaa94f0b98697d704b90fd1c072", title="wow_dec", - filename="0000004.pdf.gpg", + filename="0000004.pdf", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, ) self.note = Note.objects.create( @@ -242,11 +241,6 @@ class TestExportImport( checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element["fields"]["checksum"]) - self.assertEqual( - element["fields"]["storage_type"], - Document.STORAGE_TYPE_UNENCRYPTED, - ) - if document_exporter.EXPORTER_ARCHIVE_NAME in element: fname = ( self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME] @@ -436,7 +430,7 @@ class TestExportImport( Document.objects.create( checksum="AAAAAAAAAAAAAAAAA", title="wow", - filename="0000004.pdf", + filename="0000010.pdf", mime_type="application/pdf", ) self.assertRaises(FileNotFoundError, call_command, "document_exporter", target) diff --git a/src/documents/views.py b/src/documents/views.py index 730a6dc1a..96b1f50b0 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -195,7 +195,6 @@ from paperless import version from paperless.celery import app as celery_app from paperless.config import AIConfig from paperless.config import GeneralConfig -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless.serialisers import GroupSerializer from paperless.serialisers import UserSerializer @@ -1071,10 +1070,8 @@ class DocumentViewSet( doc, ): return HttpResponseForbidden("Insufficient permissions") - if doc.storage_type == Document.STORAGE_TYPE_GPG: - handle = GnuPG.decrypted(doc.thumbnail_file) - else: - handle = doc.thumbnail_file + + handle = doc.thumbnail_file return HttpResponse(handle, content_type="image/webp") except (FileNotFoundError, Document.DoesNotExist): @@ -2824,9 +2821,6 @@ def serve_file(*, doc: Document, use_archive: bool, disposition: str): if mime_type in {"application/csv", "text/csv"} and disposition == "inline": mime_type = "text/plain" - if doc.storage_type == Document.STORAGE_TYPE_GPG: - file_handle = GnuPG.decrypted(file_handle) - response = HttpResponse(file_handle, content_type=mime_type) # Firefox is not able to handle unicode characters in filename field # RFC 5987 addresses this issue diff --git a/src/paperless/db.py b/src/paperless/db.py deleted file mode 100644 index 286ccb094..000000000 --- a/src/paperless/db.py +++ /dev/null @@ -1,17 +0,0 @@ -import gnupg -from django.conf import settings - - -class GnuPG: - """ - A handy singleton to use when handling encrypted files. - """ - - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - @classmethod - def decrypted(cls, file_handle, passphrase=None): - if not passphrase: - passphrase = settings.PASSPHRASE - - return cls.gpg.decrypt_file(file_handle, passphrase=passphrase).data diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 024c7f076..30ee213d1 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -1203,19 +1203,6 @@ EMAIL_PARSE_DEFAULT_LAYOUT = __get_int( 1, # MailRule.PdfLayout.TEXT_HTML but that can't be imported here ) -# Pre-2.x versions of Paperless stored your documents locally with GPG -# encryption, but that is no longer the default. This behaviour is still -# available, but it must be explicitly enabled by setting -# `PAPERLESS_PASSPHRASE` in your environment or config file. The default is to -# store these files unencrypted. -# -# Translation: -# * If you're a new user, you can safely ignore this setting. -# * If you're upgrading from 1.x, this must be set, OR you can run -# `./manage.py change_storage_type gpg unencrypted` to decrypt your files, -# after which you can unset this value. -PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE") - # Trigger a script after every successful document consumption? PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT") POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT") From cf89d81b9e2dd2f8ba62dff2dc147e5555fb3263 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 03:31:55 +0000 Subject: [PATCH 57/57] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 566 ++++++++++++------------- 1 file changed, 277 insertions(+), 289 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 4843c5ccc..7e4bf0abf 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-15 23:01+0000\n" +"POT-Creation-Date: 2026-01-25 03:30+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -57,31 +57,31 @@ msgstr "" msgid "Custom field not found" msgstr "" -#: documents/models.py:38 documents/models.py:769 +#: documents/models.py:38 documents/models.py:747 msgid "owner" msgstr "" -#: documents/models.py:55 documents/models.py:984 +#: documents/models.py:55 documents/models.py:962 msgid "None" msgstr "" -#: documents/models.py:56 documents/models.py:985 +#: documents/models.py:56 documents/models.py:963 msgid "Any word" msgstr "" -#: documents/models.py:57 documents/models.py:986 +#: documents/models.py:57 documents/models.py:964 msgid "All words" msgstr "" -#: documents/models.py:58 documents/models.py:987 +#: documents/models.py:58 documents/models.py:965 msgid "Exact match" msgstr "" -#: documents/models.py:59 documents/models.py:988 +#: documents/models.py:59 documents/models.py:966 msgid "Regular expression" msgstr "" -#: documents/models.py:60 documents/models.py:989 +#: documents/models.py:60 documents/models.py:967 msgid "Fuzzy word" msgstr "" @@ -89,24 +89,24 @@ msgstr "" msgid "Automatic" msgstr "" -#: documents/models.py:64 documents/models.py:456 documents/models.py:1529 +#: documents/models.py:64 documents/models.py:434 documents/models.py:1507 #: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" -#: documents/models.py:66 documents/models.py:1053 +#: documents/models.py:66 documents/models.py:1031 msgid "match" msgstr "" -#: documents/models.py:69 documents/models.py:1056 +#: documents/models.py:69 documents/models.py:1034 msgid "matching algorithm" msgstr "" -#: documents/models.py:74 documents/models.py:1061 +#: documents/models.py:74 documents/models.py:1039 msgid "is insensitive" msgstr "" -#: documents/models.py:97 documents/models.py:170 +#: documents/models.py:97 documents/models.py:163 msgid "correspondent" msgstr "" @@ -132,7 +132,7 @@ msgstr "" msgid "tag" msgstr "" -#: documents/models.py:117 documents/models.py:208 +#: documents/models.py:117 documents/models.py:201 msgid "tags" msgstr "" @@ -144,7 +144,7 @@ msgstr "" msgid "Cannot set parent to a descendant." msgstr "" -#: documents/models.py:142 documents/models.py:190 +#: documents/models.py:142 documents/models.py:183 msgid "document type" msgstr "" @@ -156,7 +156,7 @@ msgstr "" msgid "path" msgstr "" -#: documents/models.py:152 documents/models.py:179 +#: documents/models.py:152 documents/models.py:172 msgid "storage path" msgstr "" @@ -164,1063 +164,1051 @@ msgstr "" msgid "storage paths" msgstr "" -#: documents/models.py:160 -msgid "Unencrypted" -msgstr "" - -#: documents/models.py:161 -msgid "Encrypted with GNU Privacy Guard" -msgstr "" - -#: documents/models.py:182 +#: documents/models.py:175 msgid "title" msgstr "" -#: documents/models.py:194 documents/models.py:683 +#: documents/models.py:187 documents/models.py:661 msgid "content" msgstr "" -#: documents/models.py:197 +#: documents/models.py:190 msgid "" "The raw, text-only data of the document. This field is primarily used for " "searching." msgstr "" -#: documents/models.py:202 +#: documents/models.py:195 msgid "mime type" msgstr "" -#: documents/models.py:212 +#: documents/models.py:205 msgid "checksum" msgstr "" -#: documents/models.py:216 +#: documents/models.py:209 msgid "The checksum of the original document." msgstr "" -#: documents/models.py:220 +#: documents/models.py:213 msgid "archive checksum" msgstr "" -#: documents/models.py:225 +#: documents/models.py:218 msgid "The checksum of the archived document." msgstr "" -#: documents/models.py:229 +#: documents/models.py:222 msgid "page count" msgstr "" -#: documents/models.py:236 +#: documents/models.py:229 msgid "The number of pages of the document." msgstr "" -#: documents/models.py:241 documents/models.py:689 documents/models.py:727 -#: documents/models.py:799 documents/models.py:858 +#: documents/models.py:234 documents/models.py:667 documents/models.py:705 +#: documents/models.py:777 documents/models.py:836 msgid "created" msgstr "" -#: documents/models.py:247 +#: documents/models.py:240 msgid "modified" msgstr "" -#: documents/models.py:254 -msgid "storage type" -msgstr "" - -#: documents/models.py:262 +#: documents/models.py:247 msgid "added" msgstr "" -#: documents/models.py:269 +#: documents/models.py:254 msgid "filename" msgstr "" -#: documents/models.py:275 +#: documents/models.py:260 msgid "Current filename in storage" msgstr "" -#: documents/models.py:279 +#: documents/models.py:264 msgid "archive filename" msgstr "" -#: documents/models.py:285 +#: documents/models.py:270 msgid "Current archive filename in storage" msgstr "" -#: documents/models.py:289 +#: documents/models.py:274 msgid "original filename" msgstr "" -#: documents/models.py:295 +#: documents/models.py:280 msgid "The original name of the file when it was uploaded" msgstr "" -#: documents/models.py:302 +#: documents/models.py:287 msgid "archive serial number" msgstr "" -#: documents/models.py:312 +#: documents/models.py:297 msgid "The position of this document in your physical document archive." msgstr "" -#: documents/models.py:318 documents/models.py:700 documents/models.py:754 -#: documents/models.py:1572 +#: documents/models.py:303 documents/models.py:678 documents/models.py:732 +#: documents/models.py:1550 msgid "document" msgstr "" -#: documents/models.py:319 +#: documents/models.py:304 msgid "documents" msgstr "" -#: documents/models.py:437 +#: documents/models.py:415 msgid "Table" msgstr "" -#: documents/models.py:438 +#: documents/models.py:416 msgid "Small Cards" msgstr "" -#: documents/models.py:439 +#: documents/models.py:417 msgid "Large Cards" msgstr "" -#: documents/models.py:442 +#: documents/models.py:420 msgid "Title" msgstr "" -#: documents/models.py:443 documents/models.py:1005 +#: documents/models.py:421 documents/models.py:983 msgid "Created" msgstr "" -#: documents/models.py:444 documents/models.py:1004 +#: documents/models.py:422 documents/models.py:982 msgid "Added" msgstr "" -#: documents/models.py:445 +#: documents/models.py:423 msgid "Tags" msgstr "" -#: documents/models.py:446 +#: documents/models.py:424 msgid "Correspondent" msgstr "" -#: documents/models.py:447 +#: documents/models.py:425 msgid "Document Type" msgstr "" -#: documents/models.py:448 +#: documents/models.py:426 msgid "Storage Path" msgstr "" -#: documents/models.py:449 +#: documents/models.py:427 msgid "Note" msgstr "" -#: documents/models.py:450 +#: documents/models.py:428 msgid "Owner" msgstr "" -#: documents/models.py:451 +#: documents/models.py:429 msgid "Shared" msgstr "" -#: documents/models.py:452 +#: documents/models.py:430 msgid "ASN" msgstr "" -#: documents/models.py:453 +#: documents/models.py:431 msgid "Pages" msgstr "" -#: documents/models.py:459 +#: documents/models.py:437 msgid "show on dashboard" msgstr "" -#: documents/models.py:462 +#: documents/models.py:440 msgid "show in sidebar" msgstr "" -#: documents/models.py:466 +#: documents/models.py:444 msgid "sort field" msgstr "" -#: documents/models.py:471 +#: documents/models.py:449 msgid "sort reverse" msgstr "" -#: documents/models.py:474 +#: documents/models.py:452 msgid "View page size" msgstr "" -#: documents/models.py:482 +#: documents/models.py:460 msgid "View display mode" msgstr "" -#: documents/models.py:489 +#: documents/models.py:467 msgid "Document display fields" msgstr "" -#: documents/models.py:496 documents/models.py:559 +#: documents/models.py:474 documents/models.py:537 msgid "saved view" msgstr "" -#: documents/models.py:497 +#: documents/models.py:475 msgid "saved views" msgstr "" -#: documents/models.py:505 +#: documents/models.py:483 msgid "title contains" msgstr "" -#: documents/models.py:506 +#: documents/models.py:484 msgid "content contains" msgstr "" -#: documents/models.py:507 +#: documents/models.py:485 msgid "ASN is" msgstr "" -#: documents/models.py:508 +#: documents/models.py:486 msgid "correspondent is" msgstr "" -#: documents/models.py:509 +#: documents/models.py:487 msgid "document type is" msgstr "" -#: documents/models.py:510 +#: documents/models.py:488 msgid "is in inbox" msgstr "" -#: documents/models.py:511 +#: documents/models.py:489 msgid "has tag" msgstr "" -#: documents/models.py:512 +#: documents/models.py:490 msgid "has any tag" msgstr "" -#: documents/models.py:513 +#: documents/models.py:491 msgid "created before" msgstr "" -#: documents/models.py:514 +#: documents/models.py:492 msgid "created after" msgstr "" -#: documents/models.py:515 +#: documents/models.py:493 msgid "created year is" msgstr "" -#: documents/models.py:516 +#: documents/models.py:494 msgid "created month is" msgstr "" -#: documents/models.py:517 +#: documents/models.py:495 msgid "created day is" msgstr "" -#: documents/models.py:518 +#: documents/models.py:496 msgid "added before" msgstr "" -#: documents/models.py:519 +#: documents/models.py:497 msgid "added after" msgstr "" -#: documents/models.py:520 +#: documents/models.py:498 msgid "modified before" msgstr "" -#: documents/models.py:521 +#: documents/models.py:499 msgid "modified after" msgstr "" -#: documents/models.py:522 +#: documents/models.py:500 msgid "does not have tag" msgstr "" -#: documents/models.py:523 +#: documents/models.py:501 msgid "does not have ASN" msgstr "" -#: documents/models.py:524 +#: documents/models.py:502 msgid "title or content contains" msgstr "" -#: documents/models.py:525 +#: documents/models.py:503 msgid "fulltext query" msgstr "" -#: documents/models.py:526 +#: documents/models.py:504 msgid "more like this" msgstr "" -#: documents/models.py:527 +#: documents/models.py:505 msgid "has tags in" msgstr "" -#: documents/models.py:528 +#: documents/models.py:506 msgid "ASN greater than" msgstr "" -#: documents/models.py:529 +#: documents/models.py:507 msgid "ASN less than" msgstr "" -#: documents/models.py:530 +#: documents/models.py:508 msgid "storage path is" msgstr "" -#: documents/models.py:531 +#: documents/models.py:509 msgid "has correspondent in" msgstr "" -#: documents/models.py:532 +#: documents/models.py:510 msgid "does not have correspondent in" msgstr "" -#: documents/models.py:533 +#: documents/models.py:511 msgid "has document type in" msgstr "" -#: documents/models.py:534 +#: documents/models.py:512 msgid "does not have document type in" msgstr "" -#: documents/models.py:535 +#: documents/models.py:513 msgid "has storage path in" msgstr "" -#: documents/models.py:536 +#: documents/models.py:514 msgid "does not have storage path in" msgstr "" -#: documents/models.py:537 +#: documents/models.py:515 msgid "owner is" msgstr "" -#: documents/models.py:538 +#: documents/models.py:516 msgid "has owner in" msgstr "" -#: documents/models.py:539 +#: documents/models.py:517 msgid "does not have owner" msgstr "" -#: documents/models.py:540 +#: documents/models.py:518 msgid "does not have owner in" msgstr "" -#: documents/models.py:541 +#: documents/models.py:519 msgid "has custom field value" msgstr "" -#: documents/models.py:542 +#: documents/models.py:520 msgid "is shared by me" msgstr "" -#: documents/models.py:543 +#: documents/models.py:521 msgid "has custom fields" msgstr "" -#: documents/models.py:544 +#: documents/models.py:522 msgid "has custom field in" msgstr "" -#: documents/models.py:545 +#: documents/models.py:523 msgid "does not have custom field in" msgstr "" -#: documents/models.py:546 +#: documents/models.py:524 msgid "does not have custom field" msgstr "" -#: documents/models.py:547 +#: documents/models.py:525 msgid "custom fields query" msgstr "" -#: documents/models.py:548 +#: documents/models.py:526 msgid "created to" msgstr "" -#: documents/models.py:549 +#: documents/models.py:527 msgid "created from" msgstr "" -#: documents/models.py:550 +#: documents/models.py:528 msgid "added to" msgstr "" -#: documents/models.py:551 +#: documents/models.py:529 msgid "added from" msgstr "" -#: documents/models.py:552 +#: documents/models.py:530 msgid "mime type is" msgstr "" -#: documents/models.py:562 +#: documents/models.py:540 msgid "rule type" msgstr "" -#: documents/models.py:564 +#: documents/models.py:542 msgid "value" msgstr "" -#: documents/models.py:567 +#: documents/models.py:545 msgid "filter rule" msgstr "" -#: documents/models.py:568 +#: documents/models.py:546 msgid "filter rules" msgstr "" -#: documents/models.py:592 +#: documents/models.py:570 msgid "Auto Task" msgstr "" -#: documents/models.py:593 +#: documents/models.py:571 msgid "Scheduled Task" msgstr "" -#: documents/models.py:594 +#: documents/models.py:572 msgid "Manual Task" msgstr "" -#: documents/models.py:597 +#: documents/models.py:575 msgid "Consume File" msgstr "" -#: documents/models.py:598 +#: documents/models.py:576 msgid "Train Classifier" msgstr "" -#: documents/models.py:599 +#: documents/models.py:577 msgid "Check Sanity" msgstr "" -#: documents/models.py:600 +#: documents/models.py:578 msgid "Index Optimize" msgstr "" -#: documents/models.py:601 +#: documents/models.py:579 msgid "LLM Index Update" msgstr "" -#: documents/models.py:606 +#: documents/models.py:584 msgid "Task ID" msgstr "" -#: documents/models.py:607 +#: documents/models.py:585 msgid "Celery ID for the Task that was run" msgstr "" -#: documents/models.py:612 +#: documents/models.py:590 msgid "Acknowledged" msgstr "" -#: documents/models.py:613 +#: documents/models.py:591 msgid "If the task is acknowledged via the frontend or API" msgstr "" -#: documents/models.py:619 +#: documents/models.py:597 msgid "Task Filename" msgstr "" -#: documents/models.py:620 +#: documents/models.py:598 msgid "Name of the file which the Task was run for" msgstr "" -#: documents/models.py:627 +#: documents/models.py:605 msgid "Task Name" msgstr "" -#: documents/models.py:628 +#: documents/models.py:606 msgid "Name of the task that was run" msgstr "" -#: documents/models.py:635 +#: documents/models.py:613 msgid "Task State" msgstr "" -#: documents/models.py:636 +#: documents/models.py:614 msgid "Current state of the task being run" msgstr "" -#: documents/models.py:642 +#: documents/models.py:620 msgid "Created DateTime" msgstr "" -#: documents/models.py:643 +#: documents/models.py:621 msgid "Datetime field when the task result was created in UTC" msgstr "" -#: documents/models.py:649 +#: documents/models.py:627 msgid "Started DateTime" msgstr "" -#: documents/models.py:650 +#: documents/models.py:628 msgid "Datetime field when the task was started in UTC" msgstr "" -#: documents/models.py:656 +#: documents/models.py:634 msgid "Completed DateTime" msgstr "" -#: documents/models.py:657 +#: documents/models.py:635 msgid "Datetime field when the task was completed in UTC" msgstr "" -#: documents/models.py:663 +#: documents/models.py:641 msgid "Result Data" msgstr "" -#: documents/models.py:665 +#: documents/models.py:643 msgid "The data returned by the task" msgstr "" -#: documents/models.py:673 +#: documents/models.py:651 msgid "Task Type" msgstr "" -#: documents/models.py:674 +#: documents/models.py:652 msgid "The type of task that was run" msgstr "" -#: documents/models.py:685 +#: documents/models.py:663 msgid "Note for the document" msgstr "" -#: documents/models.py:709 +#: documents/models.py:687 msgid "user" msgstr "" -#: documents/models.py:714 +#: documents/models.py:692 msgid "note" msgstr "" -#: documents/models.py:715 +#: documents/models.py:693 msgid "notes" msgstr "" -#: documents/models.py:723 +#: documents/models.py:701 msgid "Archive" msgstr "" -#: documents/models.py:724 +#: documents/models.py:702 msgid "Original" msgstr "" -#: documents/models.py:735 paperless_mail/models.py:75 +#: documents/models.py:713 paperless_mail/models.py:75 msgid "expiration" msgstr "" -#: documents/models.py:742 +#: documents/models.py:720 msgid "slug" msgstr "" -#: documents/models.py:774 +#: documents/models.py:752 msgid "share link" msgstr "" -#: documents/models.py:775 +#: documents/models.py:753 msgid "share links" msgstr "" -#: documents/models.py:787 +#: documents/models.py:765 msgid "String" msgstr "" -#: documents/models.py:788 +#: documents/models.py:766 msgid "URL" msgstr "" -#: documents/models.py:789 +#: documents/models.py:767 msgid "Date" msgstr "" -#: documents/models.py:790 +#: documents/models.py:768 msgid "Boolean" msgstr "" -#: documents/models.py:791 +#: documents/models.py:769 msgid "Integer" msgstr "" -#: documents/models.py:792 +#: documents/models.py:770 msgid "Float" msgstr "" -#: documents/models.py:793 +#: documents/models.py:771 msgid "Monetary" msgstr "" -#: documents/models.py:794 +#: documents/models.py:772 msgid "Document Link" msgstr "" -#: documents/models.py:795 +#: documents/models.py:773 msgid "Select" msgstr "" -#: documents/models.py:796 +#: documents/models.py:774 msgid "Long Text" msgstr "" -#: documents/models.py:808 +#: documents/models.py:786 msgid "data type" msgstr "" -#: documents/models.py:815 +#: documents/models.py:793 msgid "extra data" msgstr "" -#: documents/models.py:819 +#: documents/models.py:797 msgid "Extra data for the custom field, such as select options" msgstr "" -#: documents/models.py:825 +#: documents/models.py:803 msgid "custom field" msgstr "" -#: documents/models.py:826 +#: documents/models.py:804 msgid "custom fields" msgstr "" -#: documents/models.py:926 +#: documents/models.py:904 msgid "custom field instance" msgstr "" -#: documents/models.py:927 +#: documents/models.py:905 msgid "custom field instances" msgstr "" -#: documents/models.py:992 +#: documents/models.py:970 msgid "Consumption Started" msgstr "" -#: documents/models.py:993 +#: documents/models.py:971 msgid "Document Added" msgstr "" -#: documents/models.py:994 +#: documents/models.py:972 msgid "Document Updated" msgstr "" -#: documents/models.py:995 +#: documents/models.py:973 msgid "Scheduled" msgstr "" -#: documents/models.py:998 +#: documents/models.py:976 msgid "Consume Folder" msgstr "" -#: documents/models.py:999 +#: documents/models.py:977 msgid "Api Upload" msgstr "" -#: documents/models.py:1000 +#: documents/models.py:978 msgid "Mail Fetch" msgstr "" -#: documents/models.py:1001 +#: documents/models.py:979 msgid "Web UI" msgstr "" -#: documents/models.py:1006 +#: documents/models.py:984 msgid "Modified" msgstr "" -#: documents/models.py:1007 +#: documents/models.py:985 msgid "Custom Field" msgstr "" -#: documents/models.py:1010 +#: documents/models.py:988 msgid "Workflow Trigger Type" msgstr "" -#: documents/models.py:1022 +#: documents/models.py:1000 msgid "filter path" msgstr "" -#: documents/models.py:1027 +#: documents/models.py:1005 msgid "" "Only consume documents with a path that matches this if specified. Wildcards " "specified as * are allowed. Case insensitive." msgstr "" -#: documents/models.py:1034 +#: documents/models.py:1012 msgid "filter filename" msgstr "" -#: documents/models.py:1039 paperless_mail/models.py:200 +#: documents/models.py:1017 paperless_mail/models.py:200 msgid "" "Only consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." msgstr "" -#: documents/models.py:1050 +#: documents/models.py:1028 msgid "filter documents from this mail rule" msgstr "" -#: documents/models.py:1066 +#: documents/models.py:1044 msgid "has these tag(s)" msgstr "" -#: documents/models.py:1073 +#: documents/models.py:1051 msgid "has all of these tag(s)" msgstr "" -#: documents/models.py:1080 +#: documents/models.py:1058 msgid "does not have these tag(s)" msgstr "" -#: documents/models.py:1088 +#: documents/models.py:1066 msgid "has this document type" msgstr "" -#: documents/models.py:1095 +#: documents/models.py:1073 msgid "does not have these document type(s)" msgstr "" -#: documents/models.py:1103 +#: documents/models.py:1081 msgid "has this correspondent" msgstr "" -#: documents/models.py:1110 +#: documents/models.py:1088 msgid "does not have these correspondent(s)" msgstr "" -#: documents/models.py:1118 +#: documents/models.py:1096 msgid "has this storage path" msgstr "" -#: documents/models.py:1125 +#: documents/models.py:1103 msgid "does not have these storage path(s)" msgstr "" -#: documents/models.py:1129 +#: documents/models.py:1107 msgid "filter custom field query" msgstr "" -#: documents/models.py:1132 +#: documents/models.py:1110 msgid "JSON-encoded custom field query expression." msgstr "" -#: documents/models.py:1136 +#: documents/models.py:1114 msgid "schedule offset days" msgstr "" -#: documents/models.py:1139 +#: documents/models.py:1117 msgid "The number of days to offset the schedule trigger by." msgstr "" -#: documents/models.py:1144 +#: documents/models.py:1122 msgid "schedule is recurring" msgstr "" -#: documents/models.py:1147 +#: documents/models.py:1125 msgid "If the schedule should be recurring." msgstr "" -#: documents/models.py:1152 +#: documents/models.py:1130 msgid "schedule recurring delay in days" msgstr "" -#: documents/models.py:1156 +#: documents/models.py:1134 msgid "The number of days between recurring schedule triggers." msgstr "" -#: documents/models.py:1161 +#: documents/models.py:1139 msgid "schedule date field" msgstr "" -#: documents/models.py:1166 +#: documents/models.py:1144 msgid "The field to check for a schedule trigger." msgstr "" -#: documents/models.py:1175 +#: documents/models.py:1153 msgid "schedule date custom field" msgstr "" -#: documents/models.py:1179 +#: documents/models.py:1157 msgid "workflow trigger" msgstr "" -#: documents/models.py:1180 +#: documents/models.py:1158 msgid "workflow triggers" msgstr "" -#: documents/models.py:1188 +#: documents/models.py:1166 msgid "email subject" msgstr "" -#: documents/models.py:1192 +#: documents/models.py:1170 msgid "" "The subject of the email, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1198 +#: documents/models.py:1176 msgid "email body" msgstr "" -#: documents/models.py:1201 +#: documents/models.py:1179 msgid "" "The body (message) of the email, can include some placeholders, see " "documentation." msgstr "" -#: documents/models.py:1207 +#: documents/models.py:1185 msgid "emails to" msgstr "" -#: documents/models.py:1210 +#: documents/models.py:1188 msgid "The destination email addresses, comma separated." msgstr "" -#: documents/models.py:1216 +#: documents/models.py:1194 msgid "include document in email" msgstr "" -#: documents/models.py:1227 +#: documents/models.py:1205 msgid "webhook url" msgstr "" -#: documents/models.py:1230 +#: documents/models.py:1208 msgid "The destination URL for the notification." msgstr "" -#: documents/models.py:1235 +#: documents/models.py:1213 msgid "use parameters" msgstr "" -#: documents/models.py:1240 +#: documents/models.py:1218 msgid "send as JSON" msgstr "" -#: documents/models.py:1244 +#: documents/models.py:1222 msgid "webhook parameters" msgstr "" -#: documents/models.py:1247 +#: documents/models.py:1225 msgid "The parameters to send with the webhook URL if body not used." msgstr "" -#: documents/models.py:1251 +#: documents/models.py:1229 msgid "webhook body" msgstr "" -#: documents/models.py:1254 +#: documents/models.py:1232 msgid "The body to send with the webhook URL if parameters not used." msgstr "" -#: documents/models.py:1258 +#: documents/models.py:1236 msgid "webhook headers" msgstr "" -#: documents/models.py:1261 +#: documents/models.py:1239 msgid "The headers to send with the webhook URL." msgstr "" -#: documents/models.py:1266 +#: documents/models.py:1244 msgid "include document in webhook" msgstr "" -#: documents/models.py:1277 +#: documents/models.py:1255 msgid "Assignment" msgstr "" -#: documents/models.py:1281 +#: documents/models.py:1259 msgid "Removal" msgstr "" -#: documents/models.py:1285 documents/templates/account/password_reset.html:15 +#: documents/models.py:1263 documents/templates/account/password_reset.html:15 msgid "Email" msgstr "" -#: documents/models.py:1289 +#: documents/models.py:1267 msgid "Webhook" msgstr "" -#: documents/models.py:1293 +#: documents/models.py:1271 msgid "Workflow Action Type" msgstr "" -#: documents/models.py:1298 documents/models.py:1531 +#: documents/models.py:1276 documents/models.py:1509 #: paperless_mail/models.py:145 msgid "order" msgstr "" -#: documents/models.py:1301 +#: documents/models.py:1279 msgid "assign title" msgstr "" -#: documents/models.py:1305 +#: documents/models.py:1283 msgid "Assign a document title, must be a Jinja2 template, see documentation." msgstr "" -#: documents/models.py:1313 paperless_mail/models.py:274 +#: documents/models.py:1291 paperless_mail/models.py:274 msgid "assign this tag" msgstr "" -#: documents/models.py:1322 paperless_mail/models.py:282 +#: documents/models.py:1300 paperless_mail/models.py:282 msgid "assign this document type" msgstr "" -#: documents/models.py:1331 paperless_mail/models.py:296 +#: documents/models.py:1309 paperless_mail/models.py:296 msgid "assign this correspondent" msgstr "" -#: documents/models.py:1340 +#: documents/models.py:1318 msgid "assign this storage path" msgstr "" -#: documents/models.py:1349 +#: documents/models.py:1327 msgid "assign this owner" msgstr "" -#: documents/models.py:1356 +#: documents/models.py:1334 msgid "grant view permissions to these users" msgstr "" -#: documents/models.py:1363 +#: documents/models.py:1341 msgid "grant view permissions to these groups" msgstr "" -#: documents/models.py:1370 +#: documents/models.py:1348 msgid "grant change permissions to these users" msgstr "" -#: documents/models.py:1377 +#: documents/models.py:1355 msgid "grant change permissions to these groups" msgstr "" -#: documents/models.py:1384 +#: documents/models.py:1362 msgid "assign these custom fields" msgstr "" -#: documents/models.py:1388 +#: documents/models.py:1366 msgid "custom field values" msgstr "" -#: documents/models.py:1392 +#: documents/models.py:1370 msgid "Optional values to assign to the custom fields." msgstr "" -#: documents/models.py:1401 +#: documents/models.py:1379 msgid "remove these tag(s)" msgstr "" -#: documents/models.py:1406 +#: documents/models.py:1384 msgid "remove all tags" msgstr "" -#: documents/models.py:1413 +#: documents/models.py:1391 msgid "remove these document type(s)" msgstr "" -#: documents/models.py:1418 +#: documents/models.py:1396 msgid "remove all document types" msgstr "" -#: documents/models.py:1425 +#: documents/models.py:1403 msgid "remove these correspondent(s)" msgstr "" -#: documents/models.py:1430 +#: documents/models.py:1408 msgid "remove all correspondents" msgstr "" -#: documents/models.py:1437 +#: documents/models.py:1415 msgid "remove these storage path(s)" msgstr "" -#: documents/models.py:1442 +#: documents/models.py:1420 msgid "remove all storage paths" msgstr "" -#: documents/models.py:1449 +#: documents/models.py:1427 msgid "remove these owner(s)" msgstr "" -#: documents/models.py:1454 +#: documents/models.py:1432 msgid "remove all owners" msgstr "" -#: documents/models.py:1461 +#: documents/models.py:1439 msgid "remove view permissions for these users" msgstr "" -#: documents/models.py:1468 +#: documents/models.py:1446 msgid "remove view permissions for these groups" msgstr "" -#: documents/models.py:1475 +#: documents/models.py:1453 msgid "remove change permissions for these users" msgstr "" -#: documents/models.py:1482 +#: documents/models.py:1460 msgid "remove change permissions for these groups" msgstr "" -#: documents/models.py:1487 +#: documents/models.py:1465 msgid "remove all permissions" msgstr "" -#: documents/models.py:1494 +#: documents/models.py:1472 msgid "remove these custom fields" msgstr "" -#: documents/models.py:1499 +#: documents/models.py:1477 msgid "remove all custom fields" msgstr "" -#: documents/models.py:1508 +#: documents/models.py:1486 msgid "email" msgstr "" -#: documents/models.py:1517 +#: documents/models.py:1495 msgid "webhook" msgstr "" -#: documents/models.py:1521 +#: documents/models.py:1499 msgid "workflow action" msgstr "" -#: documents/models.py:1522 +#: documents/models.py:1500 msgid "workflow actions" msgstr "" -#: documents/models.py:1537 +#: documents/models.py:1515 msgid "triggers" msgstr "" -#: documents/models.py:1544 +#: documents/models.py:1522 msgid "actions" msgstr "" -#: documents/models.py:1547 paperless_mail/models.py:154 +#: documents/models.py:1525 paperless_mail/models.py:154 msgid "enabled" msgstr "" -#: documents/models.py:1558 +#: documents/models.py:1536 msgid "workflow" msgstr "" -#: documents/models.py:1562 +#: documents/models.py:1540 msgid "workflow trigger type" msgstr "" -#: documents/models.py:1576 +#: documents/models.py:1554 msgid "date run" msgstr "" -#: documents/models.py:1582 +#: documents/models.py:1560 msgid "workflow run" msgstr "" -#: documents/models.py:1583 +#: documents/models.py:1561 msgid "workflow runs" msgstr ""