Compare commits

..
Author SHA1 Message Date
stumpylog 6c85e8877a Chore: type GlobalPermissionsByMethod.perms_map keys as HTTP methods
perms_map accepted any string key, so a misspelled method or a HEAD entry
would never match. Keys are now a Literal of the mappable methods, without
HEAD, and the map is typed read-only.
2026-10-14 13:17:26 -07:00
stumpylog 6b700393a0 Chore: share one base class for the method-mapped permission classes
ViewDocumentsPermissions, TrashPermissions, PaperlessNotePermissions and
AcknowledgeTasksPermissions each repeated the same has_permission body and
disagreed on HTTP methods missing from their perms_map.

GlobalPermissionsByMethod now holds that logic once. HEAD is checked
against the GET entry and a method missing from the map is denied. The
task acknowledge map gains OPTIONS, now requiring change_paperlesstask
like POST instead of being open to any authenticated user. The global
statistics and system status checks also share their active and
authenticated preamble.
2026-10-14 13:08:39 -07:00
GitHub Actions 34a6c718fa Auto translate strings 2026-10-14 15:18:54 +00:00
shamoon 14aaa42777 Fix: support combined text/query searches (#14443) 2026-10-14 08:17:22 -07:00
shamoon c86c822bc7 Fix ai auto-close message 2026-10-13 12:15:28 -07:00
dependabot[bot] b23b118a49 Chore(deps): Bump the uv group across 1 directory with 3 updates (#14418)
Bumps the uv group with 3 updates in the / directory: [banks](https://github.com/masci/banks), [fsspec](https://github.com/fsspec/filesystem_spec) and [multidict](https://github.com/aio-libs/multidict).


Updates `banks` from 2.4.5 to 2.5.1
- [Release notes](https://github.com/masci/banks/releases)
- [Commits](https://github.com/masci/banks/compare/v2.4.5...v2.5.1)

Updates `fsspec` from 2026.1.0 to 2026.6.0
- [Commits](https://github.com/fsspec/filesystem_spec/compare/2026.1.0...2026.6.0)

Updates `multidict` from 6.7.1 to 6.9.1
- [Release notes](https://github.com/aio-libs/multidict/releases)
- [Changelog](https://github.com/aio-libs/multidict/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/multidict/compare/v6.7.1...v6.9.1)

---
updated-dependencies:
- dependency-name: banks
  dependency-version: 2.5.1
  dependency-type: indirect
- dependency-name: fsspec
  dependency-version: 2026.6.0
  dependency-type: indirect
- dependency-name: multidict
  dependency-version: 6.9.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 17:25:40 +00:00
52 changed files with 750 additions and 521 deletions

No files matched your search

+5 -4
View File
@@ -76,11 +76,12 @@ jobs:
return; return;
} }
// Reaching here with the honeypot means AI usage was not disclosed in the form, so that
// message can name the cause.
// A missing template label only tells us the issue did not come from the form, which // A missing template label only tells us the issue did not come from the form, which
// is reason enough to close it, but not proof of how it was written. // is reason enough to close it, but not proof of how it was written. It takes priority:
const body = honeypot // a hand-built body can carry the honeypot *and* a disclosure, and the form message is
// the only one that is accurate for both. Reaching the honeypot message means the issue
// came from the form without disclosing AI usage, so that message can name the cause.
const body = honeypot && !bypassedTemplate
? 'This issue was automatically closed because it contains a marker that is only visible to ' + ? 'This issue was automatically closed because it contains a marker that is only visible to ' +
'automated tools, which indicates it was generated by an AI agent, but the "AI usage" field does not ' + 'automated tools, which indicates it was generated by an AI agent, but the "AI usage" field does not ' +
'disclose this.\n\n' + 'disclose this.\n\n' +
+2 -2
View File
@@ -256,8 +256,8 @@ isort.force-single-line = true
[tool.codespell] [tool.codespell]
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish,NIN,nin,reprot" ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish,NIN,nin,reprot"
skip = """\ skip = """\
src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless/tests/samples/mail/*,src/documents/tests/samples\ src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/paperless/tests/samples\
/*,src/paperless_testing/sample_files/*,*.po,*.json\ /mail/*,src/documents/tests/samples/*,*.po,*.json\
""" """
write-changes = true write-changes = true
+1 -3
View File
@@ -134,9 +134,7 @@ def seed_database() -> None:
originals.mkdir(parents=True) originals.mkdir(parents=True)
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails" thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
thumbnails.mkdir(parents=True) thumbnails.mkdir(parents=True)
sample_pdf = ( sample_pdf = SOURCE_ROOT / "documents" / "tests" / "samples" / "simple.pdf"
SOURCE_ROOT / "paperless_testing" / "sample_files" / "simple-digital.pdf"
)
for document in documents: for document in documents:
shutil.copyfile(sample_pdf, originals / document.filename) shutil.copyfile(sample_pdf, originals / document.filename)
shutil.copyfile( shutil.copyfile(
@@ -484,6 +484,22 @@ describe('FilterEditorComponent', () => {
expect(component.textFilter).toBeNull() expect(component.textFilter).toBeNull()
}) })
it('should ingest title filter rules alongside a fulltext query with date created', () => {
component.filterRules = [
{
rule_type: FILTER_SIMPLE_TITLE,
value: 'foo',
},
{
rule_type: FILTER_FULLTEXT_QUERY,
value: 'created:[-1 week to now]',
},
]
expect(component.dateCreatedRelativeDate).toEqual(1) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('title') // TEXT_FILTER_TARGET_TITLE
})
it('should ingest text filter rules for fulltext query that include date added', () => { it('should ingest text filter rules for fulltext query that include date added', () => {
expect(component.dateAddedRelativeDate).toBeNull() expect(component.dateAddedRelativeDate).toBeNull()
component.filterRules = [ component.filterRules = [
@@ -1845,7 +1861,7 @@ describe('FilterEditorComponent', () => {
]) ])
}) })
it('should carry over text filtering on date created with relative date', () => { it('should keep text filtering separate from date created relative date', () => {
component.textFilter = 'foo' component.textFilter = 'foo'
const dateCreatedDropdown = fixture.debugElement.queryAll( const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent) By.directive(DatesDropdownComponent)
@@ -1855,14 +1871,18 @@ describe('FilterEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
tick(400) tick(400)
expect(component.filterRules).toEqual([ expect(component.filterRules).toEqual([
{
rule_type: FILTER_SIMPLE_TEXT,
value: 'foo',
},
{ {
rule_type: FILTER_FULLTEXT_QUERY, rule_type: FILTER_FULLTEXT_QUERY,
value: 'foo,created:[-1 week to now]', value: 'created:[-1 week to now]',
}, },
]) ])
}) })
it('should carry over text filtering once with created and added relative dates', () => { it('should keep text filtering separate from created and added relative dates', () => {
component.textFilter = 'foo' component.textFilter = 'foo'
const datesDropdown = fixture.debugElement.query( const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent) By.directive(DatesDropdownComponent)
@@ -1873,14 +1893,18 @@ describe('FilterEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
tick(400) tick(400)
expect(component.filterRules).toEqual([ expect(component.filterRules).toEqual([
{
rule_type: FILTER_SIMPLE_TEXT,
value: 'foo',
},
{ {
rule_type: FILTER_FULLTEXT_QUERY, rule_type: FILTER_FULLTEXT_QUERY,
value: 'foo,created:[-1 week to now],added:[-1 month to now]', value: 'created:[-1 week to now],added:[-1 month to now]',
}, },
]) ])
}) })
it('should convert legacy title filters into full text query when adding a created relative date', () => { it('should keep legacy title filters as a title search when adding a created relative date', () => {
component.filterRules = [ component.filterRules = [
{ {
rule_type: FILTER_TITLE, rule_type: FILTER_TITLE,
@@ -1895,14 +1919,18 @@ describe('FilterEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
tick(400) tick(400)
expect(component.filterRules).toEqual([ expect(component.filterRules).toEqual([
{
rule_type: FILTER_SIMPLE_TITLE,
value: 'foo',
},
{ {
rule_type: FILTER_FULLTEXT_QUERY, rule_type: FILTER_FULLTEXT_QUERY,
value: 'foo,created:[-1 week to now]', value: 'created:[-1 week to now]',
}, },
]) ])
}) })
it('should convert simple title filters into full text query when adding a created relative date', () => { it('should keep simple title filters as a title search when adding a created relative date', () => {
component.filterRules = [ component.filterRules = [
{ {
rule_type: FILTER_SIMPLE_TITLE, rule_type: FILTER_SIMPLE_TITLE,
@@ -1917,9 +1945,13 @@ describe('FilterEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
tick(400) tick(400)
expect(component.filterRules).toEqual([ expect(component.filterRules).toEqual([
{
rule_type: FILTER_SIMPLE_TITLE,
value: 'foo',
},
{ {
rule_type: FILTER_FULLTEXT_QUERY, rule_type: FILTER_FULLTEXT_QUERY,
value: 'foo,created:[-1 week to now]', value: 'created:[-1 week to now]',
}, },
]) ])
}) })
@@ -2005,7 +2037,7 @@ describe('FilterEditorComponent', () => {
]) ])
}) })
it('should carry over text filtering on date added with relative date', () => { it('should keep text filtering separate from date added relative date', () => {
component.textFilter = 'foo' component.textFilter = 'foo'
const datesDropdown = fixture.debugElement.query( const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent) By.directive(DatesDropdownComponent)
@@ -2015,9 +2047,13 @@ describe('FilterEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
tick(400) tick(400)
expect(component.filterRules).toEqual([ expect(component.filterRules).toEqual([
{
rule_type: FILTER_SIMPLE_TEXT,
value: 'foo',
},
{ {
rule_type: FILTER_FULLTEXT_QUERY, rule_type: FILTER_FULLTEXT_QUERY,
value: 'foo,added:[-1 week to now]', value: 'added:[-1 week to now]',
}, },
]) ])
}) })
@@ -1083,23 +1083,6 @@ export class FilterEditorComponent
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY (fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
) )
// if had a title / content search and added a relative date we need to carry it over...
if (
!existingRule &&
this._textFilter?.length > 0 &&
(this.textFilterTarget == TEXT_FILTER_TARGET_TITLE_CONTENT ||
this.textFilterTarget == TEXT_FILTER_TARGET_TITLE)
) {
existingRule = filterRules.find(
(fr) =>
fr.rule_type == FILTER_TITLE_CONTENT ||
fr.rule_type == FILTER_SIMPLE_TEXT ||
fr.rule_type == FILTER_TITLE ||
fr.rule_type == FILTER_SIMPLE_TITLE
)
existingRule.rule_type = FILTER_FULLTEXT_QUERY
}
let queryArgs = existingRule?.value.split(',') ?? [] let queryArgs = existingRule?.value.split(',') ?? []
if (this.dateCreatedRelativeDate !== null) { if (this.dateCreatedRelativeDate !== null) {
const rd = RELATIVE_DATE_QUERYSTRINGS.find( const rd = RELATIVE_DATE_QUERYSTRINGS.find(
-38
View File
@@ -11,8 +11,6 @@ from typing import TYPE_CHECKING
import pytest import pytest
from paperless_testing import samples
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
@@ -151,39 +149,3 @@ def fake_progress_manager(
monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager) monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager)
return FakeProgressManager return FakeProgressManager
@pytest.fixture(scope="session")
def shared_samples_dir() -> Path:
"""Directory of sample files used by more than one app's tests."""
return samples.SHARED_SAMPLES_DIR
@pytest.fixture(scope="session")
def simple_digital_pdf_file() -> Path:
"""One-page PDF with a text layer."""
return samples.SIMPLE_DIGITAL_PDF
@pytest.fixture(scope="session")
def multi_page_digital_pdf_file() -> Path:
"""Three-page PDF with a text layer."""
return samples.MULTI_PAGE_DIGITAL_PDF
@pytest.fixture(scope="session")
def with_form_pdf_file() -> Path:
"""PDF containing a fillable form."""
return samples.WITH_FORM_PDF
@pytest.fixture(scope="session")
def multi_page_images_pdf_file() -> Path:
"""Multi-page PDF of scanned images, no text layer."""
return samples.MULTI_PAGE_IMAGES_PDF
@pytest.fixture(scope="session")
def thumbnail_webp_file() -> Path:
"""Small WebP thumbnail."""
return samples.THUMBNAIL_WEBP
+48 -50
View File
@@ -1,5 +1,11 @@
from collections.abc import Mapping
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import ClassVar
from typing import Literal
from typing import TypeGuard
from typing import TypeVar from typing import TypeVar
from typing import cast
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@@ -62,28 +68,23 @@ class PaperlessAdminPermissions(BasePermission):
return request.user.is_active and request.user.is_staff return request.user.is_active and request.user.is_staff
def has_global_statistics_permission(user: User | None) -> bool: def _is_active_and_authenticated(user: User | None) -> TypeGuard[User]:
if ( return (
user is None user is not None
or not getattr(user, "is_active", False) and getattr(user, "is_active", False)
or not getattr(user, "is_authenticated", False) and getattr(user, "is_authenticated", False)
): )
return False
return getattr(user, "is_superuser", False) or user.has_perm(
"paperless.view_global_statistics", def has_global_statistics_permission(user: User | None) -> bool:
return _is_active_and_authenticated(user) and (
getattr(user, "is_superuser", False)
or user.has_perm("paperless.view_global_statistics")
) )
def has_system_status_permission(user: User | None) -> bool: def has_system_status_permission(user: User | None) -> bool:
if ( return _is_active_and_authenticated(user) and (
user is None
or not getattr(user, "is_active", False)
or not getattr(user, "is_authenticated", False)
):
return False
return (
getattr(user, "is_superuser", False) getattr(user, "is_superuser", False)
or getattr(user, "is_staff", False) or getattr(user, "is_staff", False)
or user.has_perm("paperless.view_system_monitoring") or user.has_perm("paperless.view_system_monitoring")
@@ -705,7 +706,32 @@ def has_perms_owner_aware(user, perms, obj):
return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj) return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj)
class ViewDocumentsPermissions(BasePermission): # HEAD is deliberately absent: GlobalPermissionsByMethod checks it against GET.
MappedMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
class GlobalPermissionsByMethod(BasePermission):
"""
Requires the global (model-level) permissions listed in ``perms_map`` for
the request's HTTP method. HEAD is checked against the ``"GET"`` entry,
since DRF runs the GET handler for it. A method missing from
``perms_map`` is denied.
"""
perms_map: ClassVar[Mapping[MappedMethod, Sequence[str]]]
def has_permission(self, request: Any, view: Any) -> bool:
if not request.user or not request.user.is_authenticated:
return False
method = "GET" if request.method == "HEAD" else request.method
# An unknown method is not a MappedMethod, but .get() returns None for
# it, so it is still denied.
perms = self.perms_map.get(cast("MappedMethod", method))
return perms is not None and request.user.has_perms(perms)
class ViewDocumentsPermissions(GlobalPermissionsByMethod):
""" """
Permissions class that checks for model permissions for only viewing Documents. Permissions class that checks for model permissions for only viewing Documents.
""" """
@@ -716,31 +742,18 @@ class ViewDocumentsPermissions(BasePermission):
"POST": ["documents.view_document"], "POST": ["documents.view_document"],
} }
def has_permission(self, request, view):
if not request.user or (not request.user.is_authenticated): # pragma: no cover
return False
return request.user.has_perms(self.perms_map.get(request.method, [])) class TrashPermissions(GlobalPermissionsByMethod):
class TrashPermissions(BasePermission):
"""Check the global document permission for each trash operation.""" """Check the global document permission for each trash operation."""
perms_map = { perms_map = {
"OPTIONS": ["documents.view_document"], "OPTIONS": ["documents.view_document"],
"HEAD": ["documents.view_document"],
"GET": ["documents.view_document"], "GET": ["documents.view_document"],
"POST": ["documents.delete_document"], "POST": ["documents.delete_document"],
} }
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated: # pragma: no cover
return False
return request.user.has_perms(self.perms_map.get(request.method, [])) class PaperlessNotePermissions(GlobalPermissionsByMethod):
class PaperlessNotePermissions(BasePermission):
""" """
Permissions class that checks for model permissions for Notes. Permissions class that checks for model permissions for Notes.
""" """
@@ -760,28 +773,13 @@ class PaperlessNotePermissions(BasePermission):
], ],
} }
def has_permission(self, request, view):
if not request.user or (not request.user.is_authenticated): # pragma: no cover
return False
perms = self.perms_map[request.method] class AcknowledgeTasksPermissions(GlobalPermissionsByMethod):
return request.user.has_perms(perms)
class AcknowledgeTasksPermissions(BasePermission):
""" """
Permissions class that checks for model permissions for acknowledging tasks. Permissions class that checks for model permissions for acknowledging tasks.
""" """
perms_map = { perms_map = {
"OPTIONS": ["documents.change_paperlesstask"],
"POST": ["documents.change_paperlesstask"], "POST": ["documents.change_paperlesstask"],
} }
def has_permission(self, request: Any, view: Any) -> bool:
if not request.user or not request.user.is_authenticated: # pragma: no cover
return False
perms = self.perms_map.get(request.method, [])
return request.user.has_perms(perms)
+9
View File
@@ -926,6 +926,7 @@ class TantivyBackend:
sort_field: str | None = None, sort_field: str | None = None,
sort_reverse: bool = False, sort_reverse: bool = False,
search_mode: SearchMode = SearchMode.QUERY, search_mode: SearchMode = SearchMode.QUERY,
filter_query: str | None = None,
limit: int | None = None, limit: int | None = None,
) -> list[int]: ) -> list[int]:
""" """
@@ -941,6 +942,7 @@ class TantivyBackend:
sort_field: Field to sort by (None for relevance ranking) sort_field: Field to sort by (None for relevance ranking)
sort_reverse: Whether to reverse the sort order sort_reverse: Whether to reverse the sort order
search_mode: Query parsing mode (QUERY, TEXT, or TITLE) search_mode: Query parsing mode (QUERY, TEXT, or TITLE)
filter_query: Optional advanced query results must also match
limit: Maximum number of IDs to return (None = all matching docs) limit: Maximum number of IDs to return (None = all matching docs)
Returns: Returns:
@@ -948,6 +950,13 @@ class TantivyBackend:
""" """
self._ensure_open() self._ensure_open()
user_query = self._parse_query(query, search_mode) user_query = self._parse_query(query, search_mode)
if filter_query is not None:
user_query = user_query.and_must_match(
tantivy.Query.const_score_query(
self._parse_query(filter_query, SearchMode.QUERY),
0.0, # zero so doesn't affect relevance
),
)
final_query = self._apply_permission_filter(user_query, user) final_query = self._apply_permission_filter(user_query, user)
searcher = self._index.searcher() searcher = self._index.searcher()
+10 -6
View File
@@ -12,25 +12,29 @@ if TYPE_CHECKING:
from paperless_testing.dirs import PaperlessDirs from paperless_testing.dirs import PaperlessDirs
@pytest.fixture(scope="session")
def document_samples_dir() -> Path:
"""Path to the shared test sample documents."""
return Path(__file__).parent / "samples" / "documents"
@pytest.fixture() @pytest.fixture()
def sample_doc( def sample_doc(
paperless_dirs: "PaperlessDirs", paperless_dirs: "PaperlessDirs",
simple_digital_pdf_file: Path, document_samples_dir: Path,
multi_page_images_pdf_file: Path,
thumbnail_webp_file: Path,
) -> "Document": ) -> "Document":
"""Create a document with valid files and matching checksums.""" """Create a document with valid files and matching checksums."""
with filelock.FileLock(paperless_dirs.media_lock): with filelock.FileLock(paperless_dirs.media_lock):
shutil.copy( shutil.copy(
simple_digital_pdf_file, document_samples_dir / "originals" / "0000001.pdf",
paperless_dirs.originals_dir / "0000001.pdf", paperless_dirs.originals_dir / "0000001.pdf",
) )
shutil.copy( shutil.copy(
multi_page_images_pdf_file, document_samples_dir / "archive" / "0000001.pdf",
paperless_dirs.archive_dir / "0000001.pdf", paperless_dirs.archive_dir / "0000001.pdf",
) )
shutil.copy( shutil.copy(
thumbnail_webp_file, document_samples_dir / "thumbnails" / "0000001.webp",
paperless_dirs.thumbnail_dir / "0000001.webp", paperless_dirs.thumbnail_dir / "0000001.webp",
) )
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
This is a test file.
@@ -53,7 +53,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
archive_checksum="D", archive_checksum="D",
) )
shutil.copy(self.SIMPLE_PDF, self.doc2.source_path) shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path) shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path) shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path)
shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path) shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path)
+2 -1
View File
@@ -835,6 +835,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
for filters in ( for filters in (
{"text": "new doc 2017-03-16"}, {"text": "new doc 2017-03-16"},
{"title_search": "apple"}, {"title_search": "apple"},
{"title_search": "apple", "query": "created:[-1 year to now]"},
): ):
with self.subTest(filters=filters): with self.subTest(filters=filters):
get_backend.return_value.search_ids.return_value = [self.doc2.id] get_backend.return_value.search_ids.return_value = [self.doc2.id]
@@ -886,7 +887,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
"all": True, "all": True,
"filters": { "filters": {
"text": "B", "text": "B",
"query": "c1", "title_search": "c1",
}, },
"method": "set_storage_path", "method": "set_storage_path",
"parameters": {"storage_path": self.sp1.id}, "parameters": {"storage_path": self.sp1.id},
+61 -49
View File
@@ -56,8 +56,6 @@ from paperless_testing.http import read_streaming_response
from paperless_testing.permissions import grant_all_global from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object from paperless_testing.permissions import grant_object
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import THUMBNAIL_WEBP
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
@@ -1854,11 +1852,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SimpleUploadedFile( with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f}, {"document": f},
@@ -1886,7 +1880,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
payload = SimpleUploadedFile( payload = SimpleUploadedFile(
"../../outside.pdf", "../../outside.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(), (Path(__file__).parent / "samples" / "simple.pdf").read_bytes(),
content_type="application/pdf", content_type="application/pdf",
) )
@@ -1915,7 +1909,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
pdf_bytes = SIMPLE_DIGITAL_PDF.read_bytes() pdf_bytes = (Path(__file__).parent / "samples" / "simple.pdf").read_bytes()
boundary = "paperless-boundary" boundary = "paperless-boundary"
payload = ( payload = (
( (
@@ -1991,7 +1985,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
def test_upload_insufficient_permissions(self) -> None: def test_upload_insufficient_permissions(self) -> None:
self.client.force_authenticate(user=UserFactory(username="testuser2")) self.client.force_authenticate(user=UserFactory(username="testuser2"))
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f}, {"document": f},
@@ -2004,11 +1998,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SimpleUploadedFile( with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{ {
@@ -2041,7 +2031,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"documenst": f}, {"documenst": f},
@@ -2067,7 +2057,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "title": "my custom title"}, {"document": f, "title": "my custom title"},
@@ -2087,7 +2077,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
) )
c = Correspondent.objects.create(name="test-corres") c = Correspondent.objects.create(name="test-corres")
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "correspondent": c.id}, {"document": f, "correspondent": c.id},
@@ -2106,7 +2096,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "correspondent": 3456}, {"document": f, "correspondent": 3456},
@@ -2121,7 +2111,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
) )
dt = DocumentType.objects.create(name="invoice") dt = DocumentType.objects.create(name="invoice")
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "document_type": dt.id}, {"document": f, "document_type": dt.id},
@@ -2140,7 +2130,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "document_type": 34578}, {"document": f, "document_type": 34578},
@@ -2155,7 +2145,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
) )
sp = StoragePath.objects.create(name="invoices") sp = StoragePath.objects.create(name="invoices")
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "storage_path": sp.id}, {"document": f, "storage_path": sp.id},
@@ -2174,7 +2164,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "storage_path": 34578}, {"document": f, "storage_path": 34578},
@@ -2190,7 +2180,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
t1 = Tag.objects.create(name="tag1") t1 = Tag.objects.create(name="tag1")
t2 = Tag.objects.create(name="tag2") t2 = Tag.objects.create(name="tag2")
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "tags": [t2.id, t1.id]}, {"document": f, "tags": [t2.id, t1.id]},
@@ -2211,7 +2201,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
t1 = Tag.objects.create(name="tag1") t1 = Tag.objects.create(name="tag1")
t2 = Tag.objects.create(name="tag2") t2 = Tag.objects.create(name="tag2")
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "tags": [t2.id, t1.id, 734563]}, {"document": f, "tags": [t2.id, t1.id, 734563]},
@@ -2235,7 +2225,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
0, 0,
tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"), tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "created": created}, {"document": f, "created": created},
@@ -2251,11 +2241,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SimpleUploadedFile( with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "archive_serial_number": 500}, {"document": f, "archive_serial_number": 500},
@@ -2282,11 +2268,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
data_type=CustomField.FieldDataType.STRING, data_type=CustomField.FieldDataType.STRING,
) )
with SimpleUploadedFile( with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{ {
@@ -2341,7 +2323,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
w1.actions.add(action1) w1.actions.add(action1)
w1.save() w1.save()
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{ {
@@ -2382,11 +2364,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
data_type=CustomField.FieldDataType.INT, data_type=CustomField.FieldDataType.INT,
) )
with SimpleUploadedFile( with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{ {
@@ -2435,7 +2413,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
] ]
for payload in error_payloads: for payload in error_payloads:
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
data = {"document": f, **payload} data = {"document": f, **payload}
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
@@ -2493,7 +2471,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
) )
with SIMPLE_DIGITAL_PDF.open("rb") as f: with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post( response = self.client.post(
"/api/documents/post_document/", "/api/documents/post_document/",
{"document": f, "from_webui": True}, {"document": f, "from_webui": True},
@@ -2532,8 +2510,14 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
archive_filename="archive.pdf", archive_filename="archive.pdf",
) )
source_file: Path = THUMBNAIL_WEBP source_file: Path = (
archive_file: Path = SIMPLE_DIGITAL_PDF Path(__file__).parent
/ "samples"
/ "documents"
/ "thumbnails"
/ "0000001.webp"
)
archive_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
shutil.copy(source_file, doc.source_path) shutil.copy(source_file, doc.source_path)
shutil.copy(archive_file, doc.archive_path) shutil.copy(archive_file, doc.archive_path)
@@ -2566,7 +2550,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
mime_type="application/pdf", mime_type="application/pdf",
) )
shutil.copy(SIMPLE_DIGITAL_PDF, doc.source_path) shutil.copy(Path(__file__).parent / "samples" / "simple.pdf", doc.source_path)
response = self.client.get(f"/api/documents/{doc.pk}/metadata/") response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -3669,6 +3653,34 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
) )
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_notes_head_and_unsupported_methods(self) -> None:
"""
GIVEN:
- A user allowed to view notes on their own document
WHEN:
- The notes endpoint is requested with HEAD, PUT and PATCH
THEN:
- HEAD is allowed like GET
- PUT and PATCH are rejected with 403 rather than a server error
"""
user = UserFactory()
grant_global(user, "view_note", "view_document")
doc = DocumentFactory(owner=user)
self.client.force_authenticate(user)
self.assertEqual(
self.client.head(f"/api/documents/{doc.pk}/notes/").status_code,
status.HTTP_200_OK,
)
self.assertEqual(
self.client.put(f"/api/documents/{doc.pk}/notes/").status_code,
status.HTTP_403_FORBIDDEN,
)
self.assertEqual(
self.client.patch(f"/api/documents/{doc.pk}/notes/").status_code,
status.HTTP_403_FORBIDDEN,
)
def test_delete_note(self) -> None: def test_delete_note(self) -> None:
""" """
GIVEN: GIVEN:
@@ -4304,8 +4316,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
filename="test2.pdf", filename="test2.pdf",
) )
archive_file = SIMPLE_DIGITAL_PDF archive_file = Path(__file__).parent / "samples" / "simple.pdf"
source_file = SIMPLE_DIGITAL_PDF source_file = Path(__file__).parent / "samples" / "simple.pdf"
shutil.copy(archive_file, doc.archive_path) shutil.copy(archive_file, doc.archive_path)
shutil.copy(source_file, doc2.source_path) shutil.copy(source_file, doc2.source_path)
+5 -8
View File
@@ -12,9 +12,6 @@ from documents.tests.utils import SampleDirMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global from paperless_testing.permissions import grant_global
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import WITH_FORM_PDF
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase): class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
@@ -45,15 +42,15 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
# Copy sample files to document paths (using different files to distinguish versions) # Copy sample files to document paths (using different files to distinguish versions)
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, self.SAMPLE_DIR / "documents" / "originals" / "0000001.pdf",
self.doc1.archive_path, self.doc1.archive_path,
) )
shutil.copy( shutil.copy(
MULTI_PAGE_DIGITAL_PDF, self.SAMPLE_DIR / "documents" / "originals" / "0000002.pdf",
self.doc1.source_path, self.doc1.source_path,
) )
shutil.copy( shutil.copy(
WITH_FORM_PDF, self.SAMPLE_DIR / "documents" / "originals" / "0000003.pdf",
self.doc2.source_path, self.doc2.source_path,
) )
@@ -380,7 +377,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
checksum="3", checksum="3",
filename="test3.pdf", filename="test3.pdf",
) )
shutil.copy(self.SIMPLE_PDF, doc3.source_path) shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc3.source_path)
doc4 = Document.objects.create( doc4 = Document.objects.create(
title="test1", title="test1",
@@ -389,7 +386,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
checksum="4", checksum="4",
filename="test4.pdf", filename="test4.pdf",
) )
shutil.copy(self.SIMPLE_PDF, doc4.source_path) shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc4.source_path)
response = self.client.post( response = self.client.post(
self.ENDPOINT, self.ENDPOINT,
+76 -1
View File
@@ -245,8 +245,68 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.data["count"], 1) self.assertEqual(response.data["count"], 1)
self.assertEqual(response.data["results"][0]["id"], title_match.id) self.assertEqual(response.data["results"][0]["id"], title_match.id)
def test_simple_search_combined_with_query_filter(self) -> None:
"""
GIVEN:
- A document with the term in its title, created this year
- A document with the term only in its content, created this year
- A document with the term in its title, created years ago
WHEN:
- A title or text search is combined with a relative created
date query, as the filter editor sends it
THEN:
- The simple search keeps its own field scope and the query only
narrows the results (issue #14436)
"""
this_year = timezone.localdate().replace(month=1, day=1)
title_match = Document.objects.create(
title="foo report",
content="nothing relevant",
checksum="F1",
created=this_year,
pk=21,
)
content_match = Document.objects.create(
title="something else",
content="foo appears here",
checksum="F2",
created=this_year,
pk=22,
)
old_title_match = Document.objects.create(
title="foo archive",
content="nothing relevant",
checksum="F3",
created=datetime.date(2001, 1, 1),
pk=23,
)
backend = get_backend()
for doc in (title_match, content_match, old_title_match):
backend.add_or_update(doc)
query = "created:[-1 year to now]"
response = self.client.get(
"/api/documents/",
{"title_search": "foo", "query": query},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[r["id"] for r in response.data["results"]],
[title_match.id],
)
response = self.client.get(
"/api/documents/",
{"text": "foo", "query": query},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertCountEqual(
[r["id"] for r in response.data["results"]],
[title_match.id, content_match.id],
)
def test_search_rejects_multiple_search_modes(self) -> None: def test_search_rejects_multiple_search_modes(self) -> None:
response = self.client.get("/api/documents/?text=bank&query=bank") response = self.client.get("/api/documents/?text=bank&title_search=bank")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data["detail"], response.data["detail"],
@@ -941,6 +1001,21 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 10) self.assertEqual(len(response.data), 10)
def test_search_autocomplete_head_requires_view_document(self) -> None:
"""
GIVEN:
- A user without the global view document permission
WHEN:
- A HEAD request is made for autocomplete
THEN:
- It is rejected like the equivalent GET request
"""
self.client.force_authenticate(user=UserFactory())
response = self.client.head("/api/search/autocomplete/?term=test")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_search_autocomplete_respect_permissions(self) -> None: def test_search_autocomplete_respect_permissions(self) -> None:
""" """
GIVEN: GIVEN:
+2 -2
View File
@@ -167,7 +167,7 @@ class TestBarcode(
- No barcodes detected - No barcodes detected
- No pages to split on - No pages to split on
""" """
test_file = self.SIMPLE_PDF test_file = self.SAMPLE_DIR / "simple.pdf"
with self.get_reader(test_file) as reader: with self.get_reader(test_file) as reader:
reader.detect() reader.detect()
separator_page_numbers = reader.get_separation_pages() separator_page_numbers = reader.get_separation_pages()
@@ -459,7 +459,7 @@ class TestBarcode(
THEN: THEN:
- Nothing happens - Nothing happens
""" """
test_file = self.SIMPLE_PDF test_file = self.SAMPLE_DIR / "simple.pdf"
with self.get_reader(test_file) as reader: with self.get_reader(test_file) as reader:
try: try:
+30 -9
View File
@@ -27,9 +27,6 @@ from documents.permissions import set_permissions_for_objects
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory from paperless_testing.factories import DocumentFactory
from paperless_testing.permissions import grant_object from paperless_testing.permissions import grant_object
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import WITH_FORM_PDF
class TestBulkEdit(DirectoriesMixin, TestCase): class TestBulkEdit(DirectoriesMixin, TestCase):
@@ -707,27 +704,47 @@ class TestPDFActions(DirectoriesMixin, TestCase):
super().setUp() super().setUp()
sample1 = self.dirs.scratch_dir / "sample.pdf" sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1, sample1,
) )
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf" sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1_archive, sample1_archive,
) )
sample2 = self.dirs.scratch_dir / "sample2.pdf" sample2 = self.dirs.scratch_dir / "sample2.pdf"
shutil.copy( shutil.copy(
MULTI_PAGE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf",
sample2, sample2,
) )
sample2_archive = self.dirs.archive_dir / "sample2_archive.pdf" sample2_archive = self.dirs.archive_dir / "sample2_archive.pdf"
shutil.copy( shutil.copy(
MULTI_PAGE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf",
sample2_archive, sample2_archive,
) )
sample3 = self.dirs.scratch_dir / "sample3.pdf" sample3 = self.dirs.scratch_dir / "sample3.pdf"
shutil.copy( shutil.copy(
WITH_FORM_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000003.pdf",
sample3, sample3,
) )
self.doc1 = Document.objects.create( self.doc1 = Document.objects.create(
@@ -761,7 +778,11 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
img_doc_archive = self.dirs.archive_dir / "sample_image.pdf" img_doc_archive = self.dirs.archive_dir / "sample_image.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
img_doc_archive, img_doc_archive,
) )
self.img_doc = Document.objects.create( self.img_doc = Document.objects.create(
+28 -9
View File
@@ -36,9 +36,6 @@ from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory from paperless_testing.factories import UserFactory
from paperless_testing.fakes.progress import FakeProgressManager from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import MULTI_PAGE_IMAGES_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
class _BaseNewStyleParser: class _BaseNewStyleParser:
@@ -115,7 +112,9 @@ class _BaseNewStyleParser:
class DummyParser(_BaseNewStyleParser): class DummyParser(_BaseNewStyleParser):
_ARCHIVE_SRC = MULTI_PAGE_IMAGES_PDF _ARCHIVE_SRC = (
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
)
def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None: def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None:
self._text = "The Text" self._text = "The Text"
@@ -201,19 +200,33 @@ class TestConsumer(
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def get_test_file(self): def get_test_file(self):
src = SIMPLE_DIGITAL_PDF src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample.pdf" dst = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(src, dst) shutil.copy(src, dst)
return dst return dst
def get_test_file2(self): def get_test_file2(self):
src = MULTI_PAGE_DIGITAL_PDF src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf"
)
dst = self.dirs.scratch_dir / "sample2.pdf" dst = self.dirs.scratch_dir / "sample2.pdf"
shutil.copy(src, dst) shutil.copy(src, dst)
return dst return dst
def get_test_archive_file(self): def get_test_archive_file(self):
src = MULTI_PAGE_IMAGES_PDF src = (
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample_archive.pdf" dst = self.dirs.scratch_dir / "sample_archive.pdf"
shutil.copy(src, dst) shutil.copy(src, dst)
return dst return dst
@@ -1049,7 +1062,7 @@ class TestConsumer(
@mock.patch("documents.consumer.get_parser_registry") @mock.patch("documents.consumer.get_parser_registry")
def test_similar_filenames(self, m) -> None: def test_similar_filenames(self, m) -> None:
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent / "samples" / "simple.pdf",
settings.CONSUMPTION_DIR / "simple.pdf", settings.CONSUMPTION_DIR / "simple.pdf",
) )
shutil.copy( shutil.copy(
@@ -1590,7 +1603,13 @@ class TestConsumerRemoteOCR(
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool: def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool:
src = SIMPLE_DIGITAL_PDF src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample.pdf" dst = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(src, dst) shutil.copy(src, dst)
+2 -5
View File
@@ -37,15 +37,12 @@ class TestDoubleSided(
self.double_sided_dir.mkdir() self.double_sided_dir.mkdir()
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
def _sample(self, name: str) -> Path:
return self.SIMPLE_PDF if name == "simple.pdf" else self.SAMPLE_DIR / name
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"): def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
""" """
Starts the consume process and also ensures the Starts the consume process and also ensures the
destination file does not exist afterwards destination file does not exist afterwards
""" """
src = self._sample(srcname) src = self.SAMPLE_DIR / srcname
dst = self.double_sided_dir / dstname dst = self.double_sided_dir / dstname
dst.parent.mkdir(parents=True, exist_ok=True) dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst) shutil.copy(src, dst)
@@ -60,7 +57,7 @@ class TestDoubleSided(
return msg return msg
def create_staging_file(self, src="double-sided-odd.pdf", datetime=None) -> None: def create_staging_file(self, src="double-sided-odd.pdf", datetime=None) -> None:
shutil.copy(self._sample(src), self.staging_file) shutil.copy(self.SAMPLE_DIR / src, self.staging_file)
if datetime is None: if datetime is None:
datetime = dt.datetime.now() datetime = dt.datetime.now()
os.utime(str(self.staging_file), (datetime.timestamp(),) * 2) os.utime(str(self.staging_file), (datetime.timestamp(),) * 2)
+1 -2
View File
@@ -23,9 +23,8 @@ from documents.tasks import update_document_content_maybe_archive_file
from paperless_testing.assertions import FileSystemAssertsMixin from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory from paperless_testing.factories import DocumentFactory
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
sample_file: Path = SIMPLE_DIGITAL_PDF sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
@pytest.mark.management @pytest.mark.management
+80 -21
View File
@@ -58,7 +58,6 @@ from paperless_testing.dirs import paperless_environment
from paperless_testing.factories import DocumentBarcodeFactory from paperless_testing.factories import DocumentBarcodeFactory
from paperless_testing.factories import DocumentFactory from paperless_testing.factories import DocumentFactory
from paperless_testing.permissions import grant_object from paperless_testing.permissions import grant_object
from paperless_testing.samples import install_document_samples
@pytest.mark.management @pytest.mark.management
@@ -204,7 +203,10 @@ class TestExportImport(
def test_exporter(self, *, use_filename_format=False) -> None: def test_exporter(self, *, use_filename_format=False) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
num_permission_objects = Permission.objects.count() num_permission_objects = Permission.objects.count()
@@ -308,7 +310,10 @@ class TestExportImport(
def test_exporter_with_filename_format(self) -> None: def test_exporter_with_filename_format(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
with override_settings( with override_settings(
FILENAME_FORMAT="{created_year}/{correspondent}/{title}", FILENAME_FORMAT="{created_year}/{correspondent}/{title}",
@@ -317,7 +322,10 @@ class TestExportImport(
def test_exporter_includes_share_links_and_bundles(self) -> None: def test_exporter_includes_share_links_and_bundles(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
share_link = ShareLink.objects.create( share_link = ShareLink.objects.create(
slug="share-link-slug", slug="share-link-slug",
@@ -416,7 +424,10 @@ class TestExportImport(
def test_update_export_changed_time(self) -> None: def test_update_export_changed_time(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export() self._do_export()
self.assertIsFile(self.target / "manifest.json") self.assertIsFile(self.target / "manifest.json")
@@ -452,7 +463,10 @@ class TestExportImport(
def test_update_export_changed_checksum(self) -> None: def test_update_export_changed_checksum(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export() self._do_export()
@@ -479,7 +493,10 @@ class TestExportImport(
def test_update_export_deleted_document(self) -> None: def test_update_export_deleted_document(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export() manifest = self._do_export()
@@ -511,7 +528,10 @@ class TestExportImport(
@override_settings(FILENAME_FORMAT="{title}/{correspondent}") @override_settings(FILENAME_FORMAT="{title}/{correspondent}")
def test_update_export_changed_location(self) -> None: def test_update_export_changed_location(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_filename_format=True) self._do_export(use_filename_format=True)
self.assertIsFile(self.target / "wow1" / "c.pdf") self.assertIsFile(self.target / "wow1" / "c.pdf")
@@ -553,7 +573,10 @@ class TestExportImport(
- Zipfile contains exported files - Zipfile contains exported files
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
args = ["document_exporter", self.target, "--zip"] args = ["document_exporter", self.target, "--zip"]
@@ -582,7 +605,10 @@ class TestExportImport(
- Zipfile contains exported files - Zipfile contains exported files
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
args = ["document_exporter", self.target, "--zip", "--use-filename-format"] args = ["document_exporter", self.target, "--zip", "--use-filename-format"]
@@ -618,7 +644,10 @@ class TestExportImport(
- The existing file and directory in target are removed - The existing file and directory in target are removed
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
# Create stuff in target directory # Create stuff in target directory
existing_file = self.target / "test.txt" existing_file = self.target / "test.txt"
@@ -714,7 +743,10 @@ class TestExportImport(
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export() manifest = self._do_export()
has_archive = False has_archive = False
@@ -757,7 +789,10 @@ class TestExportImport(
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export() manifest = self._do_export()
has_thumbnail = False has_thumbnail = False
@@ -802,7 +837,10 @@ class TestExportImport(
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export(split_manifest=True) manifest = self._do_export(split_manifest=True)
has_document = False has_document = False
@@ -827,7 +865,10 @@ class TestExportImport(
def _export_import_barcodes(self, *, split_manifest: bool) -> None: def _export_import_barcodes(self, *, split_manifest: bool) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
DocumentBarcodeFactory(document=self.d1, value="https://example.com") DocumentBarcodeFactory(document=self.d1, value="https://example.com")
DocumentBarcodeFactory(document=self.d2, page=2, value="DE8937") DocumentBarcodeFactory(document=self.d2, page=2, value="DE8937")
@@ -863,7 +904,10 @@ class TestExportImport(
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_folder_prefix=True) self._do_export(use_folder_prefix=True)
@@ -890,7 +934,10 @@ class TestExportImport(
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_folder_prefix=True, split_manifest=True) self._do_export(use_folder_prefix=True, split_manifest=True)
@@ -917,7 +964,10 @@ class TestExportImport(
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
num_content_type_objects = ContentType.objects.count() num_content_type_objects = ContentType.objects.count()
num_permission_objects = Permission.objects.count() num_permission_objects = Permission.objects.count()
@@ -954,7 +1004,10 @@ class TestExportImport(
def test_exporter_with_auditlog_disabled(self) -> None: def test_exporter_with_auditlog_disabled(self) -> None:
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
with override_settings( with override_settings(
AUDIT_LOG_ENABLED=False, AUDIT_LOG_ENABLED=False,
@@ -974,7 +1027,10 @@ class TestExportImport(
survive the round-trip with deleted_at preserved survive the round-trip with deleted_at preserved
""" """
shutil.rmtree(Path(self.dirs.media_dir) / "documents") shutil.rmtree(Path(self.dirs.media_dir) / "documents")
install_document_samples(Path(self.dirs.media_dir) / "documents") shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
# d1 has self.note and self.cfi1 attached via setUp # d1 has self.note and self.cfi1 attached via setUp
self.d1.delete() self.d1.delete()
@@ -1018,7 +1074,10 @@ class TestExportImport(
""" """
shutil.rmtree(self.dirs.media_dir / "documents") shutil.rmtree(self.dirs.media_dir / "documents")
install_document_samples(self.dirs.media_dir / "documents") shutil.copytree(
self.SAMPLE_DIR / "documents",
self.dirs.media_dir / "documents",
)
_ = self._do_export(data_only=True) _ = self._do_export(data_only=True)
@@ -11,7 +11,6 @@ from documents.models import Document
from documents.parsers import get_default_thumbnail from documents.parsers import get_default_thumbnail
from paperless_testing.assertions import FileSystemAssertsMixin from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
@pytest.mark.management @pytest.mark.management
@@ -25,7 +24,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
filename="test.pdf", filename="test.pdf",
) )
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent / "samples" / "simple.pdf",
self.d1.source_path, self.d1.source_path,
) )
@@ -37,7 +36,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
filename="test2.pdf", filename="test2.pdf",
) )
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent / "samples" / "simple.pdf",
self.d2.source_path, self.d2.source_path,
) )
+23 -5
View File
@@ -21,7 +21,6 @@ from documents.tests.helpers import dummy_preprocess
from paperless_testing.assertions import FileSystemAssertsMixin from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory from paperless_testing.factories import DocumentFactory
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
@pytest.mark.django_db @pytest.mark.django_db
@@ -230,12 +229,20 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
""" """
sample1 = self.dirs.scratch_dir / "sample.pdf" sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1, sample1,
) )
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf" sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1_archive, sample1_archive,
) )
doc = Document.objects.create( doc = Document.objects.create(
@@ -263,7 +270,11 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
""" """
sample1 = self.dirs.scratch_dir / "sample.pdf" sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy( shutil.copy(
SIMPLE_DIGITAL_PDF, Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1, sample1,
) )
doc = Document.objects.create( doc = Document.objects.create(
@@ -279,7 +290,14 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
def _create_root_with_version(self) -> tuple[Document, Document]: def _create_root_with_version(self) -> tuple[Document, Document]:
sample1 = self.dirs.scratch_dir / "sample.pdf" sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(SIMPLE_DIGITAL_PDF, sample1) shutil.copy(
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1,
)
root = DocumentFactory(content="root content", mime_type="application/pdf") root = DocumentFactory(content="root content", mime_type="application/pdf")
version = DocumentFactory( version = DocumentFactory(
content="my document", content="my document",
+27 -27
View File
@@ -178,7 +178,7 @@ class TestWorkflows(
self.assertEqual(action.__str__(), "WorkflowAction 1") self.assertEqual(action.__str__(), "WorkflowAction 1")
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -290,7 +290,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -409,7 +409,7 @@ class TestWorkflows(
w2.save() w2.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -478,7 +478,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -531,7 +531,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -608,7 +608,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -687,7 +687,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -765,7 +765,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -876,7 +876,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -928,7 +928,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc) generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve() destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination) create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination) shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix()) Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db() doc.refresh_from_db()
@@ -2023,7 +2023,7 @@ class TestWorkflows(
superuser = UserFactory(username="superuser", superuser=True) superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser) self.client.force_authenticate(user=superuser)
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
tasks.consume_file( tasks.consume_file(
@@ -3009,7 +3009,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc) generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve() destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination) create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination) shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix()) Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db() doc.refresh_from_db()
doc.tags.set([self.t1, self.t2]) doc.tags.set([self.t1, self.t2])
@@ -3073,7 +3073,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc) generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve() destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination) create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination) shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix()) Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db() doc.refresh_from_db()
doc.tags.set([self.t1]) doc.tags.set([self.t1])
@@ -3222,7 +3222,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -3346,7 +3346,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -3496,7 +3496,7 @@ class TestWorkflows(
workflow.actions.set([assignment_action, email_action]) workflow.actions.set([assignment_action, email_action])
temp_working_copy = shutil.copy( temp_working_copy = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "working-copy.pdf", self.dirs.scratch_dir / "working-copy.pdf",
) )
@@ -3596,7 +3596,7 @@ class TestWorkflows(
# move the file # move the file
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -3704,7 +3704,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc) generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve() destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination) create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination) shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix()) Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
@@ -3915,7 +3915,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -4036,7 +4036,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -4387,7 +4387,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -4835,7 +4835,7 @@ class TestWorkflows(
w.save() w.save()
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
@@ -5027,7 +5027,7 @@ class TestWorkflows(
# Create a test file to be consumed # Create a test file to be consumed
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
test_file_path = Path(test_file) test_file_path = Path(test_file)
@@ -5091,7 +5091,7 @@ class TestWorkflows(
# Create a test file to be consumed # Create a test file to be consumed
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple2.pdf", self.dirs.scratch_dir / "simple2.pdf",
) )
test_file_path = Path(test_file) test_file_path = Path(test_file)
@@ -5529,7 +5529,7 @@ class TestDateWorkflowLocalization(
) )
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
tmp_path / "simple.pdf", tmp_path / "simple.pdf",
) )
@@ -5596,7 +5596,7 @@ class TestRemoteOCRWorkflowAction(DirectoriesMixin, SampleDirMixin, APITestCase)
self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION) self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION)
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
overrides = DocumentMetadataOverrides() overrides = DocumentMetadataOverrides()
@@ -5814,7 +5814,7 @@ class TestApplyAISuggestionsWorkflowAction(
) )
test_file = shutil.copy( test_file = shutil.copy(
self.SIMPLE_PDF, self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf", self.dirs.scratch_dir / "simple.pdf",
) )
-3
View File
@@ -11,7 +11,6 @@ from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
from paperless_testing.fakes.progress import FakeProgressManager from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
class ConsumeTaskMixin: class ConsumeTaskMixin:
@@ -54,8 +53,6 @@ class SampleDirMixin:
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes" BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
SIMPLE_PDF = SIMPLE_DIGITAL_PDF
class GetConsumerMixin: class GetConsumerMixin:
@contextmanager @contextmanager
+34 -22
View File
@@ -289,6 +289,7 @@ logger = logging.getLogger("paperless.api")
# clauses efficiently, so this threshold mainly protects SQLite users. # clauses efficiently, so this threshold mainly protects SQLite users.
_TANTIVY_INTERSECT_THRESHOLD = 5_000 _TANTIVY_INTERSECT_THRESHOLD = 5_000
_TANTIVY_SEARCH_PARAM_NAMES = ("text", "title_search", "query", "more_like_id") _TANTIVY_SEARCH_PARAM_NAMES = ("text", "title_search", "query", "more_like_id")
_TANTIVY_COMBINABLE_SEARCH_PARAMS = ({"text", "query"}, {"title_search", "query"})
# whoosh-compat's fieldname tagger (used only for SearchMode.QUERY, via the # whoosh-compat's fieldname tagger (used only for SearchMode.QUERY, via the
# whoosh grammar in parse_user_query) is O(n^2) in plain word characters: # whoosh grammar in parse_user_query) is O(n^2) in plain word characters:
@@ -308,6 +309,18 @@ _TANTIVY_SEARCH_PARAM_NAMES = ("text", "title_search", "query", "more_like_id")
_MAX_QUERY_LENGTH: Final[int] = 4096 _MAX_QUERY_LENGTH: Final[int] = 4096
def _validate_tantivy_search_params(active: list[str]) -> None:
# may also have a filter query alongside a simple search e.g. relative dates
if len(active) > 1 and set(active) not in _TANTIVY_COMBINABLE_SEARCH_PARAMS:
raise ValidationError(
{
"detail": _(
"Specify only one of text, title_search, query, or more_like_id.",
),
},
)
def _get_tantivy_query_and_mode(params): def _get_tantivy_query_and_mode(params):
from documents.search import QueryTooLongError from documents.search import QueryTooLongError
from documents.search import SearchMode from documents.search import SearchMode
@@ -321,9 +334,15 @@ def _get_tantivy_query_and_mode(params):
else: else:
return None # pragma: no cover return None # pragma: no cover
if len(raw) > _MAX_QUERY_LENGTH: filter_query = (
raise QueryTooLongError(len(raw), _MAX_QUERY_LENGTH) str(params["query"])
return raw, mode if mode is not SearchMode.QUERY and "query" in params
else None
)
for value in (raw, filter_query):
if value is not None and len(value) > _MAX_QUERY_LENGTH:
raise QueryTooLongError(len(value), _MAX_QUERY_LENGTH)
return raw, mode, filter_query
def _get_more_like_id(query_params: dict[str, Any], user: User | None) -> int: def _get_more_like_id(query_params: dict[str, Any], user: User | None) -> int:
@@ -2566,15 +2585,9 @@ class UnifiedSearchViewSet(DocumentViewSet):
def parse_search_params() -> SearchParams: def parse_search_params() -> SearchParams:
"""Extract query string, search mode, and ordering from request.""" """Extract query string, search mode, and ordering from request."""
active = self._get_active_search_params(request) _validate_tantivy_search_params(
if len(active) > 1: self._get_active_search_params(request),
raise ValidationError( )
{
"detail": _(
"Specify only one of text, title_search, query, or more_like_id.",
),
},
)
ordering_param = request.query_params.get("ordering", "") ordering_param = request.query_params.get("ordering", "")
sort_reverse = ordering_param.startswith("-") sort_reverse = ordering_param.startswith("-")
@@ -2636,7 +2649,9 @@ class UnifiedSearchViewSet(DocumentViewSet):
filtered_qs: QuerySet[Document], filtered_qs: QuerySet[Document],
) -> SearchResultPage: ) -> SearchResultPage:
"""Handle text/title/query search: IDs, ORM intersection, page highlights.""" """Handle text/title/query search: IDs, ORM intersection, page highlights."""
query_str, search_mode = _get_tantivy_query_and_mode(request.query_params) query_str, search_mode, filter_query = _get_tantivy_query_and_mode(
request.query_params,
)
# "score" is not a real Tantivy sort field — it means relevance order, # "score" is not a real Tantivy sort field — it means relevance order,
# which is Tantivy's default when no sort field is specified. # which is Tantivy's default when no sort field is specified.
@@ -2649,6 +2664,7 @@ class UnifiedSearchViewSet(DocumentViewSet):
), ),
sort_reverse=sort_reverse, sort_reverse=sort_reverse,
search_mode=search_mode, search_mode=search_mode,
filter_query=filter_query,
) )
ordered_ids = intersect_and_order( ordered_ids = intersect_and_order(
all_ids, all_ids,
@@ -2883,14 +2899,7 @@ class DocumentSelectionMixin:
] ]
if not search_filters: if not search_filters:
return None return None
if len(search_filters) > 1: _validate_tantivy_search_params(search_filters)
raise ValidationError(
{
"detail": _(
"Specify only one of text, title_search, query, or more_like_id.",
),
},
)
from documents.search import SearchQueryError from documents.search import SearchQueryError
from documents.search import get_backend from documents.search import get_backend
@@ -2909,11 +2918,14 @@ class DocumentSelectionMixin:
user=search_user, user=search_user,
) )
else: else:
query_str, search_mode = _get_tantivy_query_and_mode(filters) query_str, search_mode, filter_query = _get_tantivy_query_and_mode(
filters,
)
search_ids = backend.search_ids( search_ids = backend.search_ids(
query_str, query_str,
user=search_user, user=search_user,
search_mode=search_mode, search_mode=search_mode,
filter_query=filter_query,
) )
except SearchQueryError as e: except SearchQueryError as e:
# Same user-fixable-query mapping as the search list endpoint: # Same user-fixable-query mapping as the search list endpoint:
+24 -24
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-12 19:12+0000\n" "POT-Creation-Date: 2026-10-14 15:18+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -1653,7 +1653,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:515 documents/serialisers.py:872 #: documents/serialisers.py:515 documents/serialisers.py:872
#: documents/serialisers.py:2902 documents/views.py:344 documents/views.py:2752 #: documents/serialisers.py:2902 documents/views.py:363 documents/views.py:2768
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1694,7 +1694,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2988 documents/views.py:4799 #: documents/serialisers.py:2988 documents/views.py:4811
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1945,40 +1945,40 @@ msgstr ""
msgid ", " msgid ", "
msgstr "" msgstr ""
#: documents/views.py:337 documents/views.py:2749 #: documents/views.py:318
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1684
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1695
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1707
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:2574 documents/views.py:2890
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4815 #: documents/views.py:356 documents/views.py:2765
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1703
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1714
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1726
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4827
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4862 #: documents/views.py:4874
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4926 #: documents/views.py:4938
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4940 #: documents/views.py:4952
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+24
View File
@@ -432,6 +432,30 @@ def tesseract_samples_dir(parser_samples_dir: Path) -> Path:
return parser_samples_dir / "tesseract" return parser_samples_dir / "tesseract"
@pytest.fixture(scope="session")
def multi_page_images_pdf_file(tesseract_samples_dir: Path) -> Path:
"""Path to a multi-page PDF with images.
Returns
-------
Path
Absolute path to ``tesseract/multi-page-images.pdf``.
"""
return tesseract_samples_dir / "multi-page-images.pdf"
@pytest.fixture(scope="session")
def simple_digital_pdf_file(tesseract_samples_dir: Path) -> Path:
"""Path to a simple digital PDF sample file.
Returns
-------
Path
Absolute path to ``tesseract/simple-digital.pdf``.
"""
return tesseract_samples_dir / "simple-digital.pdf"
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def simple_no_dpi_png_file(tesseract_samples_dir: Path) -> Path: def simple_no_dpi_png_file(tesseract_samples_dir: Path) -> Path:
"""Path to a simple PNG without DPI information. """Path to a simple PNG without DPI information.
@@ -160,11 +160,11 @@ class TestGetPageCount:
def test_single_page_pdf( def test_single_page_pdf(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
assert ( assert (
tesseract_parser.get_page_count( tesseract_parser.get_page_count(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
) )
== 1 == 1
@@ -187,7 +187,7 @@ class TestGetPageCount:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
caplog, caplog,
) -> None: ) -> None:
""" """
@@ -203,7 +203,7 @@ class TestGetPageCount:
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
page_count = tesseract_parser.get_page_count( page_count = tesseract_parser.get_page_count(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
) )
assert page_count is None assert page_count is None
@@ -272,10 +272,10 @@ class TestGetThumbnail:
def test_thumbnail_is_file( def test_thumbnail_is_file(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
thumb = tesseract_parser.get_thumbnail( thumb = tesseract_parser.get_thumbnail(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
) )
assert thumb.is_file() assert thumb.is_file()
@@ -284,7 +284,7 @@ class TestGetThumbnail:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
def _raise_on_pdf(input_file, output_file, **kwargs) -> None: def _raise_on_pdf(input_file, output_file, **kwargs) -> None:
if ".pdf" in str(input_file): if ".pdf" in str(input_file):
@@ -294,7 +294,7 @@ class TestGetThumbnail:
mocker.patch("documents.parsers.run_convert", side_effect=_raise_on_pdf) mocker.patch("documents.parsers.run_convert", side_effect=_raise_on_pdf)
thumb = tesseract_parser.get_thumbnail( thumb = tesseract_parser.get_thumbnail(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
) )
assert thumb.is_file() assert thumb.is_file()
@@ -320,11 +320,11 @@ class TestExtractText:
def test_extract_text_from_digital_pdf( def test_extract_text_from_digital_pdf(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
text = tesseract_parser.extract_text( text = tesseract_parser.extract_text(
None, None,
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
) )
assert text is not None assert text is not None
assert "This is a test document." in text.strip() assert "This is a test document." in text.strip()
@@ -339,7 +339,7 @@ class TestParsePdf:
def test_simple_digital_creates_archive( def test_simple_digital_creates_archive(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -353,7 +353,7 @@ class TestParsePdf:
- Text is extracted - Text is extracted
""" """
tesseract_parser.parse( tesseract_parser.parse(
multi_page_digital_pdf_file, tesseract_samples_dir / "multi-page-digital.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -368,10 +368,10 @@ class TestParsePdf:
def test_with_form_default( def test_with_form_default(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
with_form_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.parse( tesseract_parser.parse(
with_form_pdf_file, tesseract_samples_dir / "with-form.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -384,11 +384,11 @@ class TestParsePdf:
def test_with_form_redo_no_archive_when_not_requested( def test_with_form_redo_no_archive_when_not_requested(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
with_form_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.settings.mode = ModeChoices.REDO tesseract_parser.settings.mode = ModeChoices.REDO
tesseract_parser.parse( tesseract_parser.parse(
with_form_pdf_file, tesseract_samples_dir / "with-form.pdf",
"application/pdf", "application/pdf",
produce_archive=False, produce_archive=False,
) )
@@ -401,11 +401,11 @@ class TestParsePdf:
def test_with_form_force( def test_with_form_force(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
with_form_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.settings.mode = ModeChoices.FORCE tesseract_parser.settings.mode = ModeChoices.FORCE
tesseract_parser.parse( tesseract_parser.parse(
with_form_pdf_file, tesseract_samples_dir / "with-form.pdf",
"application/pdf", "application/pdf",
) )
assert_ordered_substrings( assert_ordered_substrings(
@@ -446,7 +446,7 @@ class TestParsePdf:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
mocker.patch( mocker.patch(
"ocrmypdf.ocr", "ocrmypdf.ocr",
@@ -454,7 +454,7 @@ class TestParsePdf:
) )
with pytest.raises(ParseError): with pytest.raises(ParseError):
tesseract_parser.parse( tesseract_parser.parse(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
) )
@@ -530,10 +530,10 @@ class TestParseMultiPage:
def test_multi_page_digital( def test_multi_page_digital(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.parse( tesseract_parser.parse(
multi_page_digital_pdf_file, tesseract_samples_dir / "multi-page-digital.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -557,12 +557,12 @@ class TestParseMultiPage:
self, self,
mode: str, mode: str,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.settings.pages = 2 tesseract_parser.settings.pages = 2
tesseract_parser.settings.mode = mode tesseract_parser.settings.mode = mode
tesseract_parser.parse( tesseract_parser.parse(
multi_page_digital_pdf_file, tesseract_samples_dir / "multi-page-digital.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -576,11 +576,11 @@ class TestParseMultiPage:
def test_multi_page_images_skip( def test_multi_page_images_skip(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_images_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
tesseract_parser.parse( tesseract_parser.parse(
multi_page_images_pdf_file, tesseract_samples_dir / "multi-page-images.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -594,7 +594,7 @@ class TestParseMultiPage:
def test_multi_page_images_redo_pages_2( def test_multi_page_images_redo_pages_2(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_images_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -609,7 +609,7 @@ class TestParseMultiPage:
tesseract_parser.settings.pages = 2 tesseract_parser.settings.pages = 2
tesseract_parser.settings.mode = ModeChoices.REDO tesseract_parser.settings.mode = ModeChoices.REDO
tesseract_parser.parse( tesseract_parser.parse(
multi_page_images_pdf_file, tesseract_samples_dir / "multi-page-images.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -622,7 +622,7 @@ class TestParseMultiPage:
def test_multi_page_images_force_page_1( def test_multi_page_images_force_page_1(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_images_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -637,7 +637,7 @@ class TestParseMultiPage:
tesseract_parser.settings.pages = 1 tesseract_parser.settings.pages = 1
tesseract_parser.settings.mode = ModeChoices.FORCE tesseract_parser.settings.mode = ModeChoices.FORCE
tesseract_parser.parse( tesseract_parser.parse(
multi_page_images_pdf_file, tesseract_samples_dir / "multi-page-images.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -733,7 +733,7 @@ class TestSkipArchive:
def test_skip_noarchive_with_text_layer( def test_skip_noarchive_with_text_layer(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -747,7 +747,7 @@ class TestSkipArchive:
""" """
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
tesseract_parser.parse( tesseract_parser.parse(
multi_page_digital_pdf_file, tesseract_samples_dir / "multi-page-digital.pdf",
"application/pdf", "application/pdf",
produce_archive=False, produce_archive=False,
) )
@@ -762,7 +762,7 @@ class TestSkipArchive:
def test_skip_noarchive_image_only_creates_archive( def test_skip_noarchive_image_only_creates_archive(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
multi_page_images_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -775,7 +775,7 @@ class TestSkipArchive:
""" """
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
tesseract_parser.parse( tesseract_parser.parse(
multi_page_images_pdf_file, tesseract_samples_dir / "multi-page-images.pdf",
"application/pdf", "application/pdf",
) )
assert tesseract_parser.archive_path is not None assert tesseract_parser.archive_path is not None
@@ -787,29 +787,29 @@ class TestSkipArchive:
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
("produce_archive", "sample_fixture", "expect_archive"), ("produce_archive", "filename", "expect_archive"),
[ [
pytest.param( pytest.param(
True, True,
"multi_page_digital_pdf_file", "multi-page-digital.pdf",
True, True,
id="produce-archive-with-text", id="produce-archive-with-text",
), ),
pytest.param( pytest.param(
True, True,
"multi_page_images_pdf_file", "multi-page-images.pdf",
True, True,
id="produce-archive-no-text", id="produce-archive-no-text",
), ),
pytest.param( pytest.param(
False, False,
"multi_page_digital_pdf_file", "multi-page-digital.pdf",
False, False,
id="no-archive-with-text-layer", id="no-archive-with-text-layer",
), ),
pytest.param( pytest.param(
False, False,
"multi_page_images_pdf_file", "multi-page-images.pdf",
False, False,
id="no-archive-no-text-layer", id="no-archive-no-text-layer",
), ),
@@ -818,10 +818,10 @@ class TestSkipArchive:
def test_produce_archive_flag( def test_produce_archive_flag(
self, self,
produce_archive: bool, # noqa: FBT001 produce_archive: bool, # noqa: FBT001
sample_fixture: str, filename: str,
expect_archive: bool, # noqa: FBT001 expect_archive: bool, # noqa: FBT001
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
request: pytest.FixtureRequest, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -834,9 +834,8 @@ class TestSkipArchive:
- Text is always extracted - Text is always extracted
""" """
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
sample = request.getfixturevalue(sample_fixture)
tesseract_parser.parse( tesseract_parser.parse(
sample, tesseract_samples_dir / filename,
"application/pdf", "application/pdf",
produce_archive=produce_archive, produce_archive=produce_archive,
) )
@@ -853,7 +852,7 @@ class TestSkipArchive:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -869,7 +868,7 @@ class TestSkipArchive:
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
mock_ocr = mocker.patch("ocrmypdf.ocr") mock_ocr = mocker.patch("ocrmypdf.ocr")
tesseract_parser.parse( tesseract_parser.parse(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
produce_archive=False, produce_archive=False,
) )
@@ -908,7 +907,7 @@ class TestSkipArchive:
def test_tagged_pdf_produces_pdfa_archive_without_ocr( def test_tagged_pdf_produces_pdfa_archive_without_ocr(
self, self,
tesseract_parser: RasterisedDocumentParser, tesseract_parser: RasterisedDocumentParser,
simple_digital_pdf_file: Path, tesseract_samples_dir: Path,
) -> None: ) -> None:
""" """
GIVEN: GIVEN:
@@ -923,7 +922,7 @@ class TestSkipArchive:
""" """
tesseract_parser.settings.mode = ModeChoices.AUTO tesseract_parser.settings.mode = ModeChoices.AUTO
tesseract_parser.parse( tesseract_parser.parse(
simple_digital_pdf_file, tesseract_samples_dir / "simple-digital.pdf",
"application/pdf", "application/pdf",
produce_archive=True, produce_archive=True,
) )
@@ -0,0 +1,19 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<p>Some Text</p>
<p>
<img src="cid:part1.pNdUSz0s.D3NqVtPg@example.de" alt="Has to be rewritten to work..">
<img src="http://localhost:8080/assets/logo_full_white.svg" alt="This image should not be shown.">
</p>
<p>and an embedded image.<br>
</p>
<p id="changeme">Paragraph unchanged.</p>
<scRipt>
document.getElementById("changeme").innerHTML = "Paragraph changed via Java Script.";
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.
+6 -4
View File
@@ -16,6 +16,8 @@ from paperless.parsers.utils import read_file_handle_unicode_errors
if TYPE_CHECKING: if TYPE_CHECKING:
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
SAMPLES = Path(__file__).parent / "samples" / "tesseract"
class TestReadFileHandleUnicodeErrors: class TestReadFileHandleUnicodeErrors:
def test_plain_utf8(self, tmp_path: Path) -> None: def test_plain_utf8(self, tmp_path: Path) -> None:
@@ -53,11 +55,11 @@ class TestReadFileHandleUnicodeErrors:
class TestIsTaggedPdf: class TestIsTaggedPdf:
def test_tagged_pdf_returns_true(self, simple_digital_pdf_file: Path) -> None: def test_tagged_pdf_returns_true(self) -> None:
assert is_tagged_pdf(simple_digital_pdf_file) is True assert is_tagged_pdf(SAMPLES / "simple-digital.pdf") is True
def test_untagged_pdf_returns_false(self, multi_page_images_pdf_file: Path) -> None: def test_untagged_pdf_returns_false(self) -> None:
assert is_tagged_pdf(multi_page_images_pdf_file) is False assert is_tagged_pdf(SAMPLES / "multi-page-images.pdf") is False
def test_nonexistent_path_returns_false(self) -> None: def test_nonexistent_path_returns_false(self) -> None:
assert is_tagged_pdf(Path("/nonexistent/file.pdf")) is False assert is_tagged_pdf(Path("/nonexistent/file.pdf")) is False
-48
View File
@@ -1,48 +0,0 @@
"""Test sample files shared across tests.
A sample used by a single app stays in that app's ``tests/samples`` tree.
Samples needed by more than one app, or stored under more than one name, live
here exactly once.
"""
import shutil
from pathlib import Path
SHARED_SAMPLES_DIR = (Path(__file__).parent / "sample_files").resolve()
SIMPLE_DIGITAL_PDF = SHARED_SAMPLES_DIR / "simple-digital.pdf"
MULTI_PAGE_DIGITAL_PDF = SHARED_SAMPLES_DIR / "multi-page-digital.pdf"
WITH_FORM_PDF = SHARED_SAMPLES_DIR / "with-form.pdf"
MULTI_PAGE_IMAGES_PDF = SHARED_SAMPLES_DIR / "multi-page-images.pdf"
THUMBNAIL_WEBP = SHARED_SAMPLES_DIR / "thumbnail.webp"
# The fake media tree under documents/tests/samples/documents keeps only the
# files unique to it. The files that duplicate a shared sample are laid back
# down under their historical opaque names (DB rows and exporter manifests
# refer to them) by install_document_samples().
_DOCUMENT_SAMPLES_DIR = (
Path(__file__).parent.parent / "documents" / "tests" / "samples" / "documents"
).resolve()
_SHARED_AS_MEDIA = (
("originals/0000001.pdf", SIMPLE_DIGITAL_PDF),
("originals/0000002.pdf", MULTI_PAGE_DIGITAL_PDF),
("originals/0000003.pdf", WITH_FORM_PDF),
("archive/0000001.pdf", MULTI_PAGE_IMAGES_PDF),
("thumbnails/0000001.webp", THUMBNAIL_WEBP),
("thumbnails/0000002.webp", THUMBNAIL_WEBP),
("thumbnails/0000003.webp", THUMBNAIL_WEBP),
("thumbnails/0000004.webp", THUMBNAIL_WEBP),
)
def install_document_samples(dest: Path) -> None:
"""Populate ``dest`` with the full fake media tree.
Replaces ``shutil.copytree(<samples>/documents, dest)`` in tests.
"""
shutil.copytree(_DOCUMENT_SAMPLES_DIR, dest, dirs_exist_ok=True)
for relpath, source in _SHARED_AS_MEDIA:
target = dest / relpath
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
Generated
+135 -100
View File
@@ -364,7 +364,7 @@ wheels = [
[[package]] [[package]]
name = "banks" name = "banks"
version = "2.4.5" version = "2.5.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecated" }, { name = "deprecated" },
@@ -374,9 +374,9 @@ dependencies = [
{ name = "platformdirs" }, { name = "platformdirs" },
{ name = "pydantic" }, { name = "pydantic" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a1/b5/4784ee9518b97f9f69c714a4303f9a6186a7e4ff2349f89e24767e9754d9/banks-2.4.5.tar.gz", hash = "sha256:ff575732fc67d5493a73c21e0d7268bc49e86fff02b0b8735e8efb9fcb9af3a4", size = 190822, upload-time = "2026-07-07T08:14:12.055Z" } sdist = { url = "https://files.pythonhosted.org/packages/f0/92/0449c32e718c10e4af40a3e040c830a77cdaeec6e9066622ee60e4c93e6e/banks-2.5.1.tar.gz", hash = "sha256:a7ca5c250b605ae90cfd6e4ca5dd6ce2ee377d5d59021b6feba44e345910026d", size = 196398, upload-time = "2026-09-10T10:15:34.569Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/b0/4d34cb2fe538aeb6d4b0723b3339cb932120db084a5c9a3c6966a26bbe1b/banks-2.4.5-py3-none-any.whl", hash = "sha256:ac2e0091b4c79379d4773c9d04a138a0d937ee27c5803bf0142acc6d6769eea1", size = 36145, upload-time = "2026-07-07T08:14:10.974Z" }, { url = "https://files.pythonhosted.org/packages/3f/6f/06c0824ee2611de47a0fc731fa57f6042d3f573025d9f504adfd65e50d51/banks-2.5.1-py3-none-any.whl", hash = "sha256:b7c91365626bab4b46197d24ce04cd69540730ed669a50a399f240318ba3dcfb", size = 39462, upload-time = "2026-09-10T10:15:35.606Z" },
] ]
[[package]] [[package]]
@@ -1446,11 +1446,11 @@ wheels = [
[[package]] [[package]]
name = "fsspec" name = "fsspec"
version = "2026.1.0" version = "2026.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" },
] ]
[[package]] [[package]]
@@ -2621,101 +2621,136 @@ wheels = [
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.7.1" version = "6.9.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, { url = "https://files.pythonhosted.org/packages/e3/24/3823efc330630a1f132bcd0a9b182ddec3c53f452f551c8e8d3aaf5a2d20/multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4", size = 96739, upload-time = "2026-09-21T17:54:42.815Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, { url = "https://files.pythonhosted.org/packages/6a/69/36331fe1d3aeb0c525a9972d6cf82791075a395aa96f026f060009d691ae/multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987", size = 58514, upload-time = "2026-09-21T17:54:44.064Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, { url = "https://files.pythonhosted.org/packages/8c/ca/5860651f782078ef13f2761c2593f4ba066d76f626b2f30f23a7f838b775/multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c", size = 57303, upload-time = "2026-09-21T17:54:45.304Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, { url = "https://files.pythonhosted.org/packages/c7/9f/c56e2fa223cb5d182660e1a09eec38880f96b7cd22769582f74447c60418/multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1", size = 320076, upload-time = "2026-09-21T17:54:46.826Z" },
{ url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, { url = "https://files.pythonhosted.org/packages/59/97/eda4fe0cad51f96096363ed32d3e0f8df28dab00beff4d92adc9db517726/multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07", size = 315489, upload-time = "2026-09-21T17:54:48.25Z" },
{ url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, { url = "https://files.pythonhosted.org/packages/24/14/10b1ded0a4085a51c3054125f854da4f47ce4fec724a2d2503231b83f3db/multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1", size = 296716, upload-time = "2026-09-21T17:54:49.749Z" },
{ url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, { url = "https://files.pythonhosted.org/packages/dc/14/04d155dd18528443cb9009f3a9f35f7417773797d072c95104ff17b8b9b7/multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3", size = 330244, upload-time = "2026-09-21T17:54:51.26Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, { url = "https://files.pythonhosted.org/packages/8b/90/284d10b3a9e5b312a8ec5b7a175fc7d560eefdc99886320449891a197992/multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80", size = 331288, upload-time = "2026-09-21T17:54:52.595Z" },
{ url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, { url = "https://files.pythonhosted.org/packages/27/50/f420de3683f9b047fc5588064d84043ef57a7152ef451b8373dfc7068d22/multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387", size = 320663, upload-time = "2026-09-21T17:54:53.91Z" },
{ url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, { url = "https://files.pythonhosted.org/packages/01/ab/0120a650d7ce4fe167299c13a13cc4ee2dc72458a9c0e9e9330b824b73c9/multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345", size = 291139, upload-time = "2026-09-21T17:54:55.313Z" },
{ url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, { url = "https://files.pythonhosted.org/packages/b3/3f/c66342f73ee5cd2c9b1dda6cfce89b5f348c48921b8c8b6e59aafbbfe31a/multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd", size = 310044, upload-time = "2026-09-21T17:54:56.745Z" },
{ url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, { url = "https://files.pythonhosted.org/packages/05/0d/e44b90d9e77e44ec3473c4c7ac7ea95764d7e8fbf6d5a085ef9bff7ce0c2/multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f", size = 303279, upload-time = "2026-09-21T17:54:58.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, { url = "https://files.pythonhosted.org/packages/97/8a/7741f7c23c211fae31ab546f0ba41569dae7671cf7ca30d5afb59e9d92aa/multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b", size = 326177, upload-time = "2026-09-21T17:54:59.825Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, { url = "https://files.pythonhosted.org/packages/e8/63/0cdbfbbe36316b2ef4b8e011afaca2cf291a73fe4fabf34dd0b22e3ee48a/multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82", size = 323526, upload-time = "2026-09-21T17:55:01.43Z" },
{ url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, { url = "https://files.pythonhosted.org/packages/98/05/0c86e9678e78f4596df0ed7f59d2aa755107da3f2a92f1b7fa150e94cbb8/multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953", size = 290278, upload-time = "2026-09-21T17:55:02.993Z" },
{ url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, { url = "https://files.pythonhosted.org/packages/95/8e/71bd7f43c4883cba6c5f96db88bd2311d52e74ad7680be5915938490d56f/multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a", size = 321142, upload-time = "2026-09-21T17:55:04.48Z" },
{ url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, { url = "https://files.pythonhosted.org/packages/2d/00/bf59d6bb22a4c152e4039574c29490caf3c65107715121807b4bd7cde244/multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13", size = 319045, upload-time = "2026-09-21T17:55:06.119Z" },
{ url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, { url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" },
{ url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, { url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" },
{ url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, { url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" },
{ url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, { url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" },
{ url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, { url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" },
{ url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, { url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" },
{ url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, { url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" },
{ url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, { url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" },
{ url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, { url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" },
{ url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, { url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, { url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" },
{ url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, { url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" },
{ url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, { url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" },
{ url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, { url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" },
{ url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, { url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" },
{ url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, { url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" },
{ url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, { url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" },
{ url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, { url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" },
{ url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, { url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" },
{ url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, { url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" },
{ url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, { url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, { url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, { url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" },
{ url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, { url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" },
{ url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, { url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" },
{ url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, { url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" },
{ url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, { url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" },
{ url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, { url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" },
{ url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, { url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" },
{ url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, { url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" },
{ url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, { url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" },
{ url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, { url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" },
{ url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, { url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" },
{ url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, { url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" },
{ url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, { url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" },
{ url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, { url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" },
{ url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" },
{ url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" },
{ url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" },
{ url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" },
{ url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" },
{ url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" },
{ url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" },
{ url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" },
{ url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" },
{ url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" },
{ url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" },
{ url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" },
{ url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" },
{ url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" },
{ url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" },
{ url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" },
{ url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" },
{ url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" },
{ url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" },
{ url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" },
{ url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" },
{ url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" },
{ url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" },
{ url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" },
{ url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" },
{ url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" },
{ url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" },
{ url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" },
{ url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" },
{ url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" },
{ url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" },
{ url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" },
{ url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" },
{ url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" },
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" },
{ url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" },
{ url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" },
{ url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" },
{ url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" },
{ url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" },
{ url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" },
{ url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" },
{ url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" },
{ url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" },
{ url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" },
{ url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" },
{ url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" },
{ url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" },
{ url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" },
{ url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" },
{ url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" },
{ url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" },
{ url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" },
{ url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" },
{ url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" },
{ url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" },
{ url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" },
{ url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" },
{ url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" },
{ url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" },
{ url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" },
{ url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" },
{ url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" },
{ url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" },
{ url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" },
{ url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" },
] ]
[[package]] [[package]]
@@ -3139,7 +3174,7 @@ requires-dist = [
{ name = "django-compression-middleware", specifier = "~=0.5.0" }, { name = "django-compression-middleware", specifier = "~=0.5.0" },
{ name = "django-cors-headers", specifier = "~=4.9.0" }, { name = "django-cors-headers", specifier = "~=4.9.0" },
{ name = "django-extensions", specifier = "~=4.1" }, { name = "django-extensions", specifier = "~=4.1" },
{ name = "django-filter", specifier = ">=25.1,<27.0" }, { name = "django-filter", specifier = ">=25.1,<27" },
{ name = "django-guardian", specifier = ">=3.3.3,<3.6" }, { name = "django-guardian", specifier = ">=3.3.3,<3.6" },
{ name = "django-multiselectfield", specifier = "~=1.0.1" }, { name = "django-multiselectfield", specifier = "~=1.0.1" },
{ name = "django-rich", specifier = "~=2.2.0" }, { name = "django-rich", specifier = "~=2.2.0" },