Compare commits

..
Author SHA1 Message Date
dependabot[bot] 43ab18916c Chore(deps-dev): Bump the development group across 1 directory with 2 updates
Bumps the development group with 2 updates in the / directory: [zensical](https://github.com/zensical/zensical) and [ruff](https://github.com/astral-sh/ruff).


Updates `zensical` from 0.0.51 to 0.0.57
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.51...v0.0.57)

Updates `ruff` from 0.16.1 to 0.16.4
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: zensical
  dependency-version: 0.0.57
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 02:20:53 +00:00
dependabot[bot] 853b57c71d Chore(deps): Bump nltk in the data-nlp-search group across 1 directory (#13917)
Bumps the data-nlp-search group with 1 update in the / directory: [nltk](https://github.com/nltk/nltk).


Updates `nltk` from 3.10.0 to 3.10.3
- [Release notes](https://github.com/nltk/nltk/releases)
- [Changelog](https://github.com/nltk/nltk/blob/develop/ChangeLog)
- [Commits](https://github.com/nltk/nltk/compare/v3.10.0...v3.10.3)

---
updated-dependencies:
- dependency-name: nltk
  dependency-version: 3.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: data-nlp-search
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 02:17:57 +00:00
GitHub Actions 1e5afe0968 Auto translate strings 2026-09-02 23:59:57 +00:00
shamoon 78ea120e0e Fix: use the header loading indicator on tasks page (#13949) 2026-09-02 16:58:14 -07:00
GitHub Actions 6b2a5e1a2e Auto translate strings 2026-09-02 23:51:42 +00:00
dependabot[bot]andshamoon bb88453a5f Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 14 updates (#13915)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 23:50:05 +00:00
dependabot[bot] 135f9f6251 Chore(deps): Bump djangorestframework in the uv group across 1 directory (#13904)
Bumps the uv group with 1 update in the / directory: [djangorestframework](https://github.com/encode/django-rest-framework).


Updates `djangorestframework` from 3.17.1 to 3.17.2
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.17.1...3.17.2)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-version: 3.17.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 22:48:44 +00:00
shamoon 1c0fbeb6f3 Fix: fix load sidebar size animating (#13947) 2026-09-02 15:25:24 -07:00
shamoon 1ba1f2b9c2 Fix: wrap long words without spaces in dropdowns (#13945) 2026-09-02 13:33:48 -07:00
shamoon 07f1a356f8 Fix: tweak tool calling localzation prompt (#13943) 2026-09-02 19:45:11 +00:00
GitHub Actions 8d41d31bd7 Auto translate strings 2026-09-02 18:10:21 +00:00
Trenton Handshamoon 351892bbab Fix: skip vector store document id filter for unrestricted chat users (#13937)
* Fix: skip vector store document id filter for unrestricted chat users

ChatStreamingView built an IN filter from every permitted document id
for the "chat over all documents" case, which exceeds the vector
store's SQLite bound-parameter safety limit on installs with more
than ~32700 documents, silently returning no context. For a user who
can see every document (an active superuser), that filter never
narrows anything, so skip it and let the retriever search the whole
index instead.

* Minor improvements from a Claude review

* When a user is unrestricted chatting, still exclude trashed documents using a 'NOT IN' SQL statement.  Wire that up where we need it

* Update src/paperless_ai/chat.py

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-09-02 18:08:54 +00:00
Thomas Steinbachandshamoon 5d6ea11828 Fix: adopt the request stream when pinning an outbound host (#13927)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-09-02 17:04:09 +00:00
shamoon c5765a50a1 Fix: ensure apply ai suggestions always runs after document created (#13940) 2026-09-02 16:12:49 +00:00
Trenton H c2a9532b8f Fix: Handle Celery enqueue failures when enqueuing files for consumption (#13935) 2026-09-02 15:58:27 +00:00
Trenton H 713c857a08 Fix: Handle Celery mail task chord errors (#13936)
* Fix: mail rule loops forever when all attachments are duplicates

When every attachment in a mail is rejected as a duplicate, the chord's
header tasks all fail. Celery's default task_allow_error_cb_on_chord_header
skips the error callback in that case, so no ProcessedMail row is ever
created, and the same mail is refetched and reprocessed on every poll for
as long as it stays in the rule's maximum_age window.

* Minor simplifications and cleanup
2026-09-02 08:41:30 -07:00
24 changed files with 822 additions and 1038 deletions
+61 -65
View File
@@ -5,14 +5,14 @@
<trans-unit id="ngb.alert.close" datatype="html">
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/alert/alert.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.carousel.slide-number" datatype="html">
<source> Slide <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> </source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">129</context>
</context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
@@ -20,219 +20,219 @@
<trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">147,148</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.carousel.next" datatype="html">
<source>Next</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.datepicker.select-month" datatype="html">
<source>Select month</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">89</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.datepicker.select-year" datatype="html">
<source>Select year</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">89</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
<source>Previous month</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">74,76</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.datepicker.next-month" datatype="html">
<source>Next month</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.first" datatype="html">
<source>««</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">297</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.previous" datatype="html">
<source>«</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">322</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html">
<source>»</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">358</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.last" datatype="html">
<source>»»</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">396</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html">
<source>First</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">414,415</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
<source>Previous</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">444,445</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html">
<source>Next</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">479,480</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html">
<source>Last</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">506</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.progressbar.value" datatype="html">
<source><x id="INTERPOLATION"/></source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html">
<source>HH</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">256,259</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.hours" datatype="html">
<source>Hours</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">293,295</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html">
<source>MM</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">322</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.minutes" datatype="html">
<source>Minutes</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">360</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<source>Increment hours</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">397,399</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<source>Decrement hours</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">435,438</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<source>Increment minutes</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<source>Decrement minutes</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.SS" datatype="html">
<source>SS</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.seconds" datatype="html">
<source>Seconds</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<source>Increment seconds</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<source>Decrement seconds</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.PM" datatype="html">
<source><x id="INTERPOLATION"/></source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html">
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/toast/toast.ts</context>
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/toast/toast.ts</context>
<context context-type="linenumber">110,112</context>
</context-group>
</trans-unit>
@@ -775,7 +775,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">19,20</context>
<context context-type="linenumber">20,21</context>
</context-group>
</trans-unit>
<trans-unit id="3894950702316166331" datatype="html">
@@ -788,10 +788,6 @@
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">49</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">26,29</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">47</context>
@@ -1854,7 +1850,7 @@
<source>Clear selection</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">10</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
@@ -1869,14 +1865,14 @@
<source>Dismiss all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
<trans-unit id="1616102757855967475" datatype="html">
<source>All</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">39</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -1915,14 +1911,14 @@
<source>Filter by</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">59</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="424356320420294719" datatype="html">
<source>All types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">64,65</context>
<context context-type="linenumber">60,61</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -1933,7 +1929,7 @@
<source>All sources</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">73,74</context>
<context context-type="linenumber">69,70</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -1944,7 +1940,7 @@
<source>Clear search</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">92</context>
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1955,14 +1951,14 @@
<source>Search tasks</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">96,97</context>
<context context-type="linenumber">92,93</context>
</context-group>
</trans-unit>
<trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">105,106</context>
<context context-type="linenumber">101,102</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -1977,14 +1973,14 @@
<source>{VAR_PLURAL, plural, =1 {1 task} other {<x id="INTERPOLATION"/> tasks}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
<source>Name</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">148,149</context>
<context context-type="linenumber">144,145</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -2095,7 +2091,7 @@
<source>Created</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">149,150</context>
<context context-type="linenumber">145,146</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
@@ -2126,21 +2122,21 @@
<source>Results</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">151,152</context>
<context context-type="linenumber">147,148</context>
</context-group>
</trans-unit>
<trans-unit id="314315645942131479" datatype="html">
<source>Info</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">153,154</context>
<context context-type="linenumber">149,150</context>
</context-group>
</trans-unit>
<trans-unit id="3193976279273491157" datatype="html">
<source>Actions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">154,155</context>
<context context-type="linenumber">150,151</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
@@ -2255,21 +2251,21 @@
<source>click for full output</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">205</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit id="8946500279334661567" datatype="html">
<source>View task details</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">211</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="1536087519743707362" datatype="html">
<source>Dismiss</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">218</context>
<context context-type="linenumber">214</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -2292,28 +2288,28 @@
<source>Open Document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="5404759957685833020" datatype="html">
<source>Result message</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">236,237</context>
<context context-type="linenumber">232,233</context>
</context-group>
</trans-unit>
<trans-unit id="6621329748219109148" datatype="html">
<source>Duplicate</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">243,244</context>
<context context-type="linenumber">239,240</context>
</context-group>
</trans-unit>
<trans-unit id="7593555694782789615" datatype="html">
<source>Open</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">247</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
@@ -2344,21 +2340,21 @@
<source>Input data</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">261,262</context>
<context context-type="linenumber">257,258</context>
</context-group>
</trans-unit>
<trans-unit id="1585185618099050920" datatype="html">
<source>Result data</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">267,268</context>
<context context-type="linenumber">263,264</context>
</context-group>
</trans-unit>
<trans-unit id="7976920528153858271" datatype="html">
<source>No tasks match the current filters.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">289,291</context>
<context context-type="linenumber">285,287</context>
</context-group>
</trans-unit>
<trans-unit id="2525230676386818985" datatype="html">
+13 -13
View File
@@ -15,14 +15,14 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^22.1.1",
"@angular/common": "~22.1.1",
"@angular/compiler": "~22.1.1",
"@angular/core": "~22.1.1",
"@angular/forms": "~22.1.1",
"@angular/localize": "~22.1.1",
"@angular/platform-browser": "~22.1.1",
"@angular/router": "~22.1.1",
"@angular/cdk": "^22.1.4",
"@angular/common": "~22.1.3",
"@angular/compiler": "~22.1.3",
"@angular/core": "~22.1.3",
"@angular/forms": "~22.1.3",
"@angular/localize": "~22.1.3",
"@angular/platform-browser": "~22.1.3",
"@angular/router": "~22.1.3",
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~23.6.0",
"@ngneat/dirty-check-forms": "^3.0.3",
@@ -45,16 +45,16 @@
},
"devDependencies": {
"@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.1.3",
"@angular-devkit/schematics": "^22.1.3",
"@angular-devkit/core": "^22.1.6",
"@angular-devkit/schematics": "^22.1.6",
"@angular-eslint/builder": "22.1.0",
"@angular-eslint/eslint-plugin": "22.1.0",
"@angular-eslint/eslint-plugin-template": "22.1.0",
"@angular-eslint/schematics": "22.1.0",
"@angular-eslint/template-parser": "22.1.0",
"@angular/build": "22.1.3",
"@angular/cli": "22.1.3",
"@angular/compiler-cli": "~22.1.1",
"@angular/build": "22.1.6",
"@angular/cli": "22.1.6",
"@angular/compiler-cli": "~22.1.3",
"@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0",
"@types/node": "^26.2.0",
+437 -629
View File
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@
i18n-title
info="Tasks shows detailed information about document consumption and system tasks."
i18n-info
[loading]="loading() && pagedTasks().length === 0"
>
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
@@ -21,11 +22,6 @@
</div>
</pngx-page-header>
@if (loading() && pagedTasks().length === 0) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
<div class="task-controls mb-3 gap-3 btn-toolbar align-items-center" role="toolbar">
<div class="task-view-scope btn-group btn-group-sm" role="group">
<input
@@ -285,6 +281,6 @@
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
</div>
}
} @else {
} @else if (!loading() || pagedTasks().length > 0) {
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
}
@@ -34,9 +34,7 @@
}
@media (min-width: 768px) {
&.expanded {
--pngx-sidebar-width: var(--pngx-sidebar-expanded-width);
}
--pngx-sidebar-width: var(--pngx-sidebar-expanded-width);
}
}
@media (max-width: 767.98px) {
@@ -113,6 +111,13 @@ main {
}
}
// only animate when the user toggles slim mode
.sidebar:not(.animating),
.sidebar:not(.animating) ~ main,
.sidebar:not(.animating) .sidebar-slim-toggler {
transition: none;
}
.sidebar.slim {
max-width: 55px;
@@ -123,8 +128,6 @@ main {
}
.sidebar.slim:not(.animating) {
transition: none;
li.nav-item span,
.sidebar-heading span {
display: none;
@@ -144,10 +147,6 @@ main {
}
}
.sidebar.slim:not(.animating) ~ main.col-slim {
transition: none;
}
.sidebar.animating {
li.nav-item span,
.sidebar-heading span {
@@ -196,6 +195,26 @@ main {
--bs-popover-body-padding-y: .5rem;
}
@media (prefers-reduced-motion: no-preference) {
.sidebar-sticky > ul,
.sidebar-sticky > .nav-group {
animation: sidebar-nav-in .3s cubic-bezier(.22, .61, .36, 1) backwards;
}
@for $i from 2 through 5 {
.sidebar-sticky > :nth-child(#{$i}) {
animation-delay: #{($i - 1) * 0.04}s;
}
}
}
@keyframes sidebar-nav-in {
from {
opacity: 0;
transform: translateY(6px);
}
}
.sidebar-sticky {
position: relative;
top: 0;
@@ -7,6 +7,8 @@
padding-left: calc(calc(var(--depth) - 2) * 1rem);
display: flex;
align-items: center;
min-width: 0;
overflow-wrap: anywhere;
.indicator {
display: inline-block;
@@ -18,3 +20,7 @@
margin-left: .5rem;
}
}
.badge {
flex-shrink: 0;
}
@@ -314,7 +314,7 @@ def _consume_file(
consumption_dir: Path,
*,
subdirs_as_tags: bool,
) -> None:
) -> bool:
"""
Queue a file for consumption.
@@ -322,15 +322,20 @@ def _consume_file(
filepath: Path to the file to consume.
consumption_dir: Base consumption directory.
subdirs_as_tags: Whether to create tags from subdirectory names.
Returns:
True if the file was successfully handed to Celery, False otherwise.
Callers must not record the file as queued on failure, or the rescan
will never retry it.
"""
# Verify file still exists and is accessible
try:
if not filepath.is_file():
logger.debug(f"Not consuming {filepath}: not a file or doesn't exist")
return
return False
except OSError as e:
logger.warning(f"Not consuming {filepath}: {e}")
return
return False
# Get tags from path if configured
tag_ids: list[int] | None = None
@@ -355,6 +360,9 @@ def _consume_file(
)
except Exception:
logger.exception(f"Error while queuing document {filepath}")
return False
return True
class Command(BaseCommand):
@@ -492,12 +500,12 @@ class Command(BaseCommand):
if not consumer_filter(Change.added, str(filepath)):
continue
_consume_file(
if _consume_file(
filepath=filepath,
consumption_dir=directory,
subdirs_as_tags=subdirs_as_tags,
)
queued.add(filepath.resolve())
):
queued.add(filepath.resolve())
return queued
@@ -651,14 +659,16 @@ class Command(BaseCommand):
# Check for stable files
for stable_path in tracker.get_stable_files():
_consume_file(
# Only remember files that were actually queued, so the
# rescan does not re-queue them while the consume task
# has yet to remove them from disk, but does retry a
# failed publish instead of stranding it
if _consume_file(
filepath=stable_path,
consumption_dir=directory,
subdirs_as_tags=subdirs_as_tags,
)
# Remember it so the rescan does not re-queue it while
# the consume task has yet to remove it from disk
queued.add(stable_path)
):
queued.add(stable_path)
# Exit watch loop to reconfigure timeout
break
+1 -1
View File
@@ -1003,7 +1003,7 @@ def run_workflows(
# kwargs so the PaperlessTask record can note the
# document, see _extract_input_data
apply_ai_suggestions.delay(
apply_ai_suggestions.delay_on_commit(
action_id=action.pk,
document_id=document.pk,
)
@@ -445,12 +445,13 @@ class TestConsumeFile:
target = consumption_dir / "document.pdf"
shutil.copy(sample_pdf, target)
_consume_file(
result = _consume_file(
filepath=target,
consumption_dir=consumption_dir,
subdirs_as_tags=False,
)
assert result is True
mock_consume_file_delay.apply_async.assert_called_once()
call_args = mock_consume_file_delay.apply_async.call_args
consumable_doc = call_args.kwargs["kwargs"]["input_doc"]
@@ -464,11 +465,12 @@ class TestConsumeFile:
mock_consume_file_delay: MagicMock,
) -> None:
"""Test _consume_file handles nonexistent files gracefully."""
_consume_file(
result = _consume_file(
filepath=consumption_dir / "nonexistent.pdf",
consumption_dir=consumption_dir,
subdirs_as_tags=False,
)
assert result is False
mock_consume_file_delay.apply_async.assert_not_called()
def test_consume_directory(
@@ -480,11 +482,12 @@ class TestConsumeFile:
subdir = consumption_dir / "subdir"
subdir.mkdir()
_consume_file(
result = _consume_file(
filepath=subdir,
consumption_dir=consumption_dir,
subdirs_as_tags=False,
)
assert result is False
mock_consume_file_delay.apply_async.assert_not_called()
def test_consume_with_permission_error(
@@ -499,13 +502,33 @@ class TestConsumeFile:
shutil.copy(sample_pdf, target)
mocker.patch.object(Path, "is_file", side_effect=PermissionError("denied"))
_consume_file(
result = _consume_file(
filepath=target,
consumption_dir=consumption_dir,
subdirs_as_tags=False,
)
assert result is False
mock_consume_file_delay.apply_async.assert_not_called()
def test_consume_with_apply_async_failure(
self,
consumption_dir: Path,
sample_pdf: Path,
mock_consume_file_delay: MagicMock,
) -> None:
"""Test _consume_file reports failure when apply_async raises."""
target = consumption_dir / "document.pdf"
shutil.copy(sample_pdf, target)
mock_consume_file_delay.apply_async.side_effect = Exception("broker down")
result = _consume_file(
filepath=target,
consumption_dir=consumption_dir,
subdirs_as_tags=False,
)
assert result is False
def test_consume_with_tags_error(
self,
consumption_dir: Path,
@@ -522,11 +545,12 @@ class TestConsumeFile:
side_effect=DatabaseError("Something happened"),
)
_consume_file(
result = _consume_file(
filepath=target,
consumption_dir=consumption_dir,
subdirs_as_tags=True,
)
assert result is True
mock_consume_file_delay.apply_async.assert_called_once()
call_args = mock_consume_file_delay.apply_async.call_args
overrides = call_args.kwargs["kwargs"]["overrides"]
@@ -1249,6 +1273,52 @@ class TestProcessExistingFilesQueued:
assert target.resolve() in queued
@pytest.mark.management
@pytest.mark.django_db
class TestCommandRetryAfterQueueFailure:
"""
Regression test for GH #13923.
A file whose ``apply_async`` publish fails (e.g. broker briefly down)
must not be marked as queued, so the periodic rescan retries it once
the broker recovers, instead of stranding it until the consumer
process is restarted.
"""
def test_watch_loop_retries_failed_publish_on_rescan(
self,
consumption_dir: Path,
sample_pdf: Path,
mock_consume_file_delay: MagicMock,
start_consumer: Callable[..., ConsumerThread],
) -> None:
"""A publish failure from the watch loop is retried by the rescan."""
apply_async = mock_consume_file_delay.apply_async
def fail_first_call(*args: object, **kwargs: object) -> None:
if apply_async.call_count == 1:
raise Exception("broker down")
apply_async.side_effect = fail_first_call
thread = start_consumer(stability_delay=0.1, rescan_interval=0.3)
target = consumption_dir / "document.pdf"
shutil.copy(sample_pdf, target)
deadline = monotonic() + 5.0
while apply_async.call_count < 2 and monotonic() < deadline:
sleep(0.1)
if thread.exception:
raise thread.exception
assert apply_async.call_count >= 2, (
"Expected the failed publish to be retried by the rescan, "
f"but apply_async was only called {apply_async.call_count} time(s)"
)
@pytest.mark.management
@pytest.mark.django_db
class TestCommandRescanRecovery:
+5 -1
View File
@@ -5621,11 +5621,15 @@ class TestApplyAISuggestionsWorkflowAction(
action = self.make_action()
self.make_workflow(action, WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED)
with mock.patch("documents.tasks.apply_ai_suggestions.delay") as delay:
with (
mock.patch("documents.tasks.apply_ai_suggestions.delay") as delay,
self.captureOnCommitCallbacks(execute=True),
):
run_workflows(
WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
self.doc,
)
delay.assert_not_called()
delay.assert_called_once_with(action_id=action.pk, document_id=self.doc.pk)
+11 -11
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-01 19:54+0000\n"
"POT-Creation-Date: 2026-09-02 18:09+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1628,7 +1628,7 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:524 documents/serialisers.py:878
#: documents/serialisers.py:2830 documents/views.py:313 documents/views.py:2619
#: documents/serialisers.py:2830 documents/views.py:314 documents/views.py:2623
#: paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1669,7 +1669,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2916 documents/views.py:4620
#: documents/serialisers.py:2916 documents/views.py:4624
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1937,36 +1937,36 @@ msgstr ""
msgid "Unable to parse URI {value}"
msgstr ""
#: documents/views.py:306 documents/views.py:2616
#: documents/views.py:307 documents/views.py:2620
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1590
#: documents/views.py:1591
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1601
#: documents/views.py:1602
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:2441 documents/views.py:2762
#: documents/views.py:2445 documents/views.py:2766
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:4633
#: documents/views.py:4637
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4679
#: documents/views.py:4683
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4743
#: documents/views.py:4747
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4757
#: documents/views.py:4761
msgid "The share link bundle is unavailable."
msgstr ""
+1 -1
View File
@@ -129,7 +129,7 @@ def _rewrite_request_to_pinned_ip(
method=request.method,
url=new_url,
headers=new_headers,
content=request.stream,
stream=request.stream,
extensions=request.extensions,
)
rewritten_request.extensions["sni_hostname"] = hostname
+6
View File
@@ -705,6 +705,12 @@ CELERY_BROKER_TRANSPORT_OPTIONS = {
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT: Final[int] = get_int_from_env("PAPERLESS_WORKER_TIMEOUT", 1800)
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-task_allow_error_cb_on_chord_header
# Without this, a failing chord header never triggers the errback, so a mail
# whose attachments all fail is never recorded and is re-fetched forever.
# The errback runs once per failed header task, so it must be idempotent.
CELERY_TASK_ALLOW_ERROR_CB_ON_CHORD_HEADER = True
CELERY_CACHE_BACKEND = "default"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-serializer
+1 -3
View File
@@ -135,9 +135,7 @@ def _stream_chat_with_documents(
# limit (_MAX_IN_VALUES) on large installs. Trashed documents stay
# indexed until permanent deletion (delete_document_from_llm_index
# hangs off post_delete, not trash), so must be excluded explicitly.
trashed_ids = Document.global_objects.filter(
deleted_at__isnull=False,
).values_list("pk", flat=True)
trashed_ids = Document.deleted_objects.values_list("pk", flat=True)
filters = exclude_document_ids_filter(str(pk) for pk in trashed_ids)
else:
filters = document_id_filters(
+6 -2
View File
@@ -131,11 +131,10 @@ class AIClient:
from llama_index.core.llms import ChatMessage
user_msg = ChatMessage(role="user", content=prompt)
if self.settings.llm_backend == LLMBackend.OLLAMA:
with self._normalize_timeouts():
result = self.llm.chat(
[user_msg],
[ChatMessage(role="user", content=prompt)],
format=DocumentClassifierSchema.model_json_schema(),
think=False,
)
@@ -149,6 +148,11 @@ class AIClient:
from llama_index.core.program.function_program import get_function_tool
tool = get_function_tool(DocumentClassifierSchema)
user_msg = ChatMessage(
role="user",
content=f"{prompt}\n\n"
f"Answer by calling the {tool.metadata.name} tool. Do not write the answer as text.",
)
with self._normalize_timeouts():
result = self.llm.chat_with_tools(
tools=[tool],
+1 -1
View File
@@ -4,7 +4,7 @@ Rewrite only the "title", "tags", "document_types", and "storage_paths" fields i
Do not translate correspondents or dates.
Preserve proper nouns, organization names, product names, and exact official document names. Translate generic category words when a {{ language_name }} equivalent exists.
Return the same JSON schema with all fields present.
Keep every entry you were given in those four fields, in the same order, using the original wording where no translation applies.
Suggestions:
{{ suggestions_json }}
-29
View File
@@ -154,35 +154,6 @@ class DocumentMetaTable:
}
class PermittedIdsTable:
"""Per-connection scratch space for an oversized IN-filter id list.
A literal ``IN (?,?,...)`` list binds one SQL parameter per id, capped by
SQLite's own SQLITE_MAX_VARIABLE_NUMBER (see _MAX_IN_VALUES in
vector_store.py). Loading the ids into a TEMP TABLE and filtering via a
subquery instead has no such limit. TEMP tables live in a
connection-private namespace -- never visible to another connection,
even under this identical name -- so this is safe under the vector
store's one-connection-per-request model without any extra locking or
per-call naming scheme.
"""
TABLE_NAME = "permitted_document_ids"
@staticmethod
def load(conn: sqlite3.Connection, ids: Iterable[int]) -> None:
"""Replace this connection's scratch table with ``ids``."""
conn.execute(f"DROP TABLE IF EXISTS temp.{PermittedIdsTable.TABLE_NAME}")
conn.execute(
f"CREATE TEMP TABLE {PermittedIdsTable.TABLE_NAME} "
"(id INTEGER PRIMARY KEY)",
)
conn.executemany(
f"INSERT INTO {PermittedIdsTable.TABLE_NAME} (id) VALUES (?)",
((i,) for i in ids),
)
class IndexMetaTable:
"""Typed accessors over index_meta's key/value rows -- replaces
PaperlessSqliteVecVectorStore._meta_get_on/_meta_set_on, which returned
+9
View File
@@ -146,6 +146,8 @@ def test_run_llm_query_ollama_uses_structured_json(mock_ai_config, mock_ollama_l
format=ANY,
think=False,
)
messages = mock_llm_instance.chat.call_args.args[0]
assert messages[0].content == "test_prompt"
def test_run_llm_query_openai_uses_tools(mock_ai_config, mock_openai_llm):
@@ -183,6 +185,13 @@ def test_run_llm_query_openai_uses_tools(mock_ai_config, mock_openai_llm):
assert result["title"] == "Test Title"
assert result["tags"] == {"existing_ids": [1], "new_names": []}
mock_llm_instance.chat_with_tools.assert_called_once()
kwargs = mock_llm_instance.chat_with_tools.call_args.kwargs
offered_tool_name = kwargs["tools"][0].metadata.name
assert kwargs["user_msg"].content == (
"test_prompt\n\n"
f"Answer by calling the {offered_tool_name} tool. "
"Do not write the answer as text."
)
def test_run_llm_query_openai_timeout_raises_local_error(
-83
View File
@@ -9,7 +9,6 @@ from paperless_ai.tables import DocumentChunksTable
from paperless_ai.tables import DocumentMetaRow
from paperless_ai.tables import DocumentMetaTable
from paperless_ai.tables import IndexMetaTable
from paperless_ai.tables import PermittedIdsTable
@pytest.fixture
@@ -339,85 +338,3 @@ class TestIndexMetaTable:
IndexMetaTable.increment_total_inserts(conn, 100)
IndexMetaTable.reset_total_inserts(conn, 7)
assert IndexMetaTable.get_total_inserts(conn) == 7
class TestPermittedIdsTable:
def _loaded_ids(self, conn: sqlite3.Connection) -> list[int]:
return [
row["id"]
for row in conn.execute(
f"SELECT id FROM {PermittedIdsTable.TABLE_NAME} ORDER BY id",
)
]
def test_load_then_read_back_all_ids(self, conn: sqlite3.Connection) -> None:
"""
GIVEN:
- A bare sqlite3 connection
WHEN:
- load() is called with a set of ids
THEN:
- Every id is present in the TEMP TABLE, and only those ids
"""
PermittedIdsTable.load(conn, [3, 1, 2])
assert self._loaded_ids(conn) == [1, 2, 3]
def test_load_replaces_previous_contents(self, conn: sqlite3.Connection) -> None:
"""
GIVEN:
- A connection whose PermittedIdsTable already holds one id set
WHEN:
- load() is called again with a different id set
THEN:
- Only the new ids are present -- a connection reused across
multiple queries in one request never leaks a stale filter
"""
PermittedIdsTable.load(conn, [1, 2, 3])
PermittedIdsTable.load(conn, [4, 5])
assert self._loaded_ids(conn) == [4, 5]
def test_load_is_connection_private(self) -> None:
"""
GIVEN:
- Two separate connections
WHEN:
- Each loads PermittedIdsTable with a different id set, under
the identical TABLE_NAME
THEN:
- Each connection sees only its own ids -- TEMP TABLE is
connection-private, so concurrent requests never collide or
cross-contaminate despite sharing the same table name (the
vector store opens one connection per request; see
PaperlessSqliteVecVectorStore)
"""
conn_a = sqlite3.connect(":memory:")
conn_a.row_factory = sqlite3.Row
conn_b = sqlite3.connect(":memory:")
conn_b.row_factory = sqlite3.Row
try:
PermittedIdsTable.load(conn_a, [1, 2, 3])
PermittedIdsTable.load(conn_b, [4, 5, 6])
assert self._loaded_ids(conn_a) == [1, 2, 3]
assert self._loaded_ids(conn_b) == [4, 5, 6]
finally:
conn_a.close()
conn_b.close()
def test_load_handles_more_ids_than_a_bound_parameter_list_could(
self,
conn: sqlite3.Connection,
) -> None:
"""
GIVEN:
- An id count over SQLite's own bound-parameter limit
(SQLITE_MAX_VARIABLE_NUMBER, 32766 by default) -- more than a
literal IN(?,?,...) list could ever bind in one statement
WHEN:
- load() is called with that many ids
THEN:
- Every id is loaded without error, since executemany() binds
one row at a time rather than one statement with N parameters
"""
ids = list(range(40_000))
PermittedIdsTable.load(conn, ids)
assert self._loaded_ids(conn) == ids
+26 -100
View File
@@ -18,7 +18,6 @@ from paperless_ai.migrations import Migration
from paperless_ai.migrations import m0001_v1_to_v2
from paperless_ai.tables import DocumentChunksTable
from paperless_ai.tables import DocumentMetaTable
from paperless_ai.tables import PermittedIdsTable
from paperless_ai.vector_store import _MAX_IN_VALUES
from paperless_ai.vector_store import DB_FILENAME
from paperless_ai.vector_store import DEFAULT_TABLE_NAME
@@ -281,23 +280,8 @@ class TestCrud:
class TestBuildWhere:
@pytest.fixture
def conn(self) -> Generator[sqlite3.Connection, None, None]:
"""A bare connection, sufficient for _build_where(): it only ever
touches the connection via PermittedIdsTable, which needs no vec0
extension loaded.
"""
connection = sqlite3.connect(":memory:")
try:
yield connection
finally:
connection.close()
def test_ne_filter_translates_to_not_equal_clause(
self,
conn: sqlite3.Connection,
) -> None:
where, params = _build_where(conn, _ne_filter(1))
def test_ne_filter_translates_to_not_equal_clause(self) -> None:
where, params = _build_where(_ne_filter(1))
assert where == "(document_id != ?)"
assert params == [1]
@@ -309,11 +293,8 @@ class TestBuildWhere:
"b1",
]
def test_nin_filter_translates_to_not_in_clause(
self,
conn: sqlite3.Connection,
) -> None:
where, params = _build_where(conn, _nin_filter([1, 2]))
def test_nin_filter_translates_to_not_in_clause(self) -> None:
where, params = _build_where(_nin_filter([1, 2]))
assert where == "(document_id NOT IN (?,?))"
assert params == [1, 2]
@@ -323,10 +304,7 @@ class TestBuildWhere:
_query(store, [0.0] * DIM, top_k=5, filters=_nin_filter([1, 2])).ids,
) == ["c1"]
def test_empty_in_filter_excludes_everything(
self,
conn: sqlite3.Connection,
) -> None:
def test_empty_in_filter_excludes_everything(self) -> None:
"""
GIVEN:
- An IN filter with an empty value list
@@ -336,14 +314,11 @@ class TestBuildWhere:
- It excludes everything (the opposite of an empty NOT IN
filter) -- an empty inclusion list must never widen results
"""
where, params = _build_where(conn, _in_filter([]))
where, params = _build_where(_in_filter([]))
assert where == "(1 = 0)"
assert params == []
def test_empty_nin_filter_excludes_nothing(
self,
conn: sqlite3.Connection,
) -> None:
def test_empty_nin_filter_excludes_nothing(self) -> None:
"""
GIVEN:
- A NOT IN filter with an empty value list -- e.g. an
@@ -355,14 +330,11 @@ class TestBuildWhere:
excludes everything) -- an empty exclusion list must never
narrow results
"""
where, params = _build_where(conn, _nin_filter([]))
where, params = _build_where(_nin_filter([]))
assert where == "(1 = 1)"
assert params == []
def test_fails_closed_when_no_filter_is_translatable(
self,
conn: sqlite3.Connection,
) -> None:
def test_fails_closed_when_no_filter_is_translatable(self) -> None:
# A nested MetadataFilters is not a MetadataFilter, so it is skipped.
# With no translatable clauses, the function must fail closed rather
# than emit "()" (invalid SQL) and never widen document access.
@@ -375,88 +347,42 @@ class TestBuildWhere:
),
],
)
where, params = _build_where(conn, MetadataFilters(filters=[nested]))
where, params = _build_where(MetadataFilters(filters=[nested]))
assert where == "1 = 0"
assert params == []
@pytest.mark.parametrize(
("build_filter", "sql_op"),
[(_in_filter, "IN"), (_nin_filter, "NOT IN")],
"build_filter",
[_in_filter, _nin_filter],
ids=["in", "nin"],
)
def test_filter_over_max_values_uses_permitted_ids_table(
def test_fails_closed_when_filter_exceeds_max_values(
self,
conn: sqlite3.Connection,
build_filter: Callable[[list[str]], MetadataFilters],
sql_op: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""
GIVEN:
- An IN or NOT IN filter with more values than _MAX_IN_VALUES
(SQLite's own bound-parameter limit is 32766; this threshold
sits below that with headroom for the query's other bound
parameters)
(SQLite's own bound-parameter limit is 32766; this guard sits
below that with headroom for the query's other bound parameters)
WHEN:
- _build_where() translates it to SQL
THEN:
- It builds a subquery against PermittedIdsTable's TEMP TABLE,
loaded with every id, instead of a literal list SQLite would
reject past its own limit -- true for NOT IN too (e.g. an
install with an enormous trash), not just IN
- It fails closed ("1 = 0", no params) instead of building a
clause SQLite would reject, and logs a warning -- this filter
scopes document access, so refusing to build it must never
widen the scope to "everything" by accident. Failing open on
a NOT IN would surface exactly the excluded rows
"""
ids = list(range(_MAX_IN_VALUES + 1))
oversized = build_filter([str(i) for i in ids])
oversized = build_filter([str(i) for i in range(_MAX_IN_VALUES + 1)])
where, params = _build_where(conn, oversized)
with caplog.at_level("WARNING"):
where, params = _build_where(oversized)
assert where == (
f"(document_id {sql_op} (SELECT id FROM {PermittedIdsTable.TABLE_NAME}))"
)
assert where == "(1 = 0)"
assert params == []
loaded = [
row[0]
for row in conn.execute(
f"SELECT id FROM {PermittedIdsTable.TABLE_NAME} ORDER BY id",
)
]
assert loaded == ids
@pytest.mark.parametrize(
("build_filter", "expected_ids"),
[(_in_filter, ["b1", "c1"]), (_nin_filter, ["a1"])],
ids=["in", "nin"],
)
def test_query_and_get_nodes_scope_correctly_when_filter_exceeds_max_values(
self,
store: PaperlessSqliteVecVectorStore,
mocker: MockerFixture,
build_filter: Callable[[list[int]], MetadataFilters],
expected_ids: list[str],
) -> None:
"""
GIVEN:
- _MAX_IN_VALUES lowered so a small IN/NOT IN filter exceeds it
WHEN:
- query() and get_nodes() are called with that filter
THEN:
- Both still correctly scope results -- the PermittedIdsTable
temp-table path behaves identically to the literal
IN(...)/NOT IN(...) path it replaces above the threshold
"""
mocker.patch("paperless_ai.vector_store._MAX_IN_VALUES", 1)
store.add(
[
make_node("a1", 1, seed=0.0),
make_node("b1", 2, seed=1.0),
make_node("c1", 3, seed=2.0),
],
)
result = _query(store, [0.0] * DIM, top_k=10, filters=build_filter([2, 3]))
nodes = store.get_nodes(filters=build_filter([2, 3]))
assert sorted(result.ids) == expected_ids
assert sorted(n.node_id for n in nodes) == expected_ids
assert "document_id" in caplog.text
def test_query_with_untranslatable_filter_returns_no_rows(
self,
+24 -25
View File
@@ -30,7 +30,6 @@ from paperless_ai.tables import DocumentChunksTable
from paperless_ai.tables import DocumentMetaRow
from paperless_ai.tables import DocumentMetaTable
from paperless_ai.tables import IndexMetaTable
from paperless_ai.tables import PermittedIdsTable
logger = logging.getLogger("paperless_ai.vector_store")
@@ -76,12 +75,14 @@ class _Row(NamedTuple):
embedding: bytes
# _build_where(): the largest IN value list translated into a literal
# IN (?,?,...) clause. SQLite's own hard limit (SQLITE_MAX_VARIABLE_NUMBER)
# is 32766 by default; this leaves headroom below that for the query's other
# bound parameters (the embedding blob, k, and any NE clause) and for the
# limit itself to move. Above this threshold _build_where() switches to
# PermittedIdsTable instead of failing closed -- see its docstring.
# _build_where(): the largest IN value list translated into bound SQL
# parameters. SQLite's own hard limit (SQLITE_MAX_VARIABLE_NUMBER) is 32766
# by default; this leaves headroom below that for the query's other bound
# parameters (the embedding blob, k, and any NE clause) and for the limit
# itself to move. An IN filter this large should not happen in practice --
# callers are expected to pass None (no filter) rather than every id when
# the filter would not actually narrow anything -- so this is a guard
# against a future regression, not a normal code path.
_MAX_IN_VALUES = 32700
@@ -105,10 +106,7 @@ def _vec0_params(rows: list[_Row]) -> list[tuple[str, int, str, bytes]]:
return [(r.chunk_id, r.document_id, r.node_content, r.embedding) for r in rows]
def _build_where(
conn: sqlite3.Connection,
filters: MetadataFilters | None,
) -> tuple[str, list[int]]:
def _build_where(filters: MetadataFilters | None) -> tuple[str, list[int]]:
"""Translate the EQ / IN / NIN / NE filters we use into a parameterized
SQL clause on vec0 metadata columns. Returns ("", []) when there is
nothing to filter. document_id is vec0's only filterable column and is
@@ -116,10 +114,6 @@ def _build_where(
still pass strings in places, e.g. indexing.py's MetadataFilter
construction) don't have to be individually correct -- vec0 doesn't
coerce types itself.
``conn`` is only used for an IN/NOT IN filter over _MAX_IN_VALUES: it
loads the ids into PermittedIdsTable's TEMP TABLE on that connection
rather than binding them as SQL parameters.
"""
if filters is None or not filters.filters:
return "", []
@@ -142,15 +136,20 @@ def _build_where(
clauses.append("1 = 0" if is_in else "1 = 1")
continue
if len(values) > _MAX_IN_VALUES:
# A literal list this large would exceed SQLite's own
# bound-parameter limit. Load the ids into a TEMP TABLE on
# this connection instead and filter via subquery, which has
# no such limit -- see PermittedIdsTable. Applies to NOT IN
# too (e.g. an install with an enormous trash), not just IN.
PermittedIdsTable.load(conn, values)
clauses.append(
f"{f.key} {sql_op} (SELECT id FROM {PermittedIdsTable.TABLE_NAME})",
# Refuse rather than risk SQLite's own bound-parameter limit
# ("too many SQL variables"): a list this large must match no
# rows, never widen the scope to "everything" -- true for
# NOT IN too, where failing open would surface every
# excluded row.
logger.warning(
"Refusing to build a %s filter on %r with %d values "
"(over the %d-value safety limit); returning no rows.",
sql_op,
f.key,
len(values),
_MAX_IN_VALUES,
)
clauses.append("1 = 0")
continue
placeholders = ",".join("?" for _ in values)
clauses.append(f"{f.key} {sql_op} ({placeholders})")
@@ -489,7 +488,7 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
)
if not self.table_exists():
return []
where, params = _build_where(self._conn, filters)
where, params = _build_where(filters)
sql = "SELECT node_content, embedding FROM " + DEFAULT_TABLE_NAME
if where:
sql += " WHERE " + where
@@ -505,7 +504,7 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
if query.query_embedding is None: # pragma: no cover
return VectorStoreQueryResult(nodes=[], similarities=[], ids=[])
top_k = query.similarity_top_k if query.similarity_top_k is not None else 10
where, params = _build_where(self._conn, query.filters)
where, params = _build_where(query.filters)
sql = (
"SELECT id, node_content, embedding, distance FROM "
+ DEFAULT_TABLE_NAME
+11 -5
View File
@@ -334,18 +334,24 @@ def error_callback(
"""
A shared task that is called whenever something goes wrong during
consumption of a file. See queue_consumption_tasks.
With CELERY_TASK_ALLOW_ERROR_CB_ON_CHORD_HEADER enabled this runs once per
failed header task, not once per chord, so it must be idempotent.
"""
rule = MailRule.objects.get(pk=rule_id)
received = make_aware(message_date) if is_naive(message_date) else message_date
ProcessedMail.objects.create(
ProcessedMail.objects.get_or_create(
rule=rule,
folder=rule.folder,
uid=message_uid,
uid_validity=uid_validity,
subject=message_subject,
received=make_aware(message_date) if is_naive(message_date) else message_date,
status="FAILED",
error=traceback.format_exc(),
defaults={
"subject": message_subject,
"received": received,
"status": "FAILED",
"error": traceback.format_exc(),
},
)
+39
View File
@@ -36,6 +36,7 @@ from paperless_mail.mail import MailAccountHandler
from paperless_mail.mail import MailError
from paperless_mail.mail import TagMailAction
from paperless_mail.mail import apply_mail_action
from paperless_mail.mail import error_callback
from paperless_mail.mail import get_mailbox
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
@@ -2045,6 +2046,44 @@ class TestPostConsumeAction(TestCase):
self.assertIn("Test Exception", processed_mail.error)
@pytest.mark.django_db
class TestErrorCallback:
def test_error_callback_is_idempotent_for_same_mail(self) -> None:
"""
GIVEN:
- A mail rule and a mail that failed to be consumed
WHEN:
- error_callback is invoked more than once for the same mail, as
happens when task_allow_error_cb_on_chord_header fires the
errback once per failed header task in a chord
THEN:
- Only one ProcessedMail row is created for that mail
"""
rule = MailRuleFactory()
message_uid = "12345"
for _ in range(2):
error_callback(
None,
Exception("Test Exception"),
None,
rule_id=rule.pk,
message_uid=message_uid,
message_subject="Test Subject",
message_date=timezone.make_aware(
timezone.datetime(2023, 1, 1, 12, 0, 0),
),
)
processed_mails = ProcessedMail.objects.filter(
rule=rule,
uid=message_uid,
folder=rule.folder,
)
assert processed_mails.count() == 1
assert processed_mails.get().status == "FAILED"
class TestManagementCommand(TestCase):
@mock.patch(
"paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts",
Generated
+38 -38
View File
@@ -4,11 +4,11 @@ requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.15' and sys_platform == 'darwin'",
"python_full_version >= '3.15' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'linux'",
@@ -1116,14 +1116,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.17.1"
version = "3.17.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
{ url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" },
]
[[package]]
@@ -2718,7 +2718,7 @@ wheels = [
[[package]]
name = "nltk"
version = "3.10.0"
version = "3.10.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -2727,9 +2727,9 @@ dependencies = [
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" },
]
[[package]]
@@ -4397,24 +4397,24 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.1"
version = "0.16.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" }
sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" },
{ url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" },
{ url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" },
{ url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" },
{ url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" },
{ url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" },
{ url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" },
{ url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" },
{ url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" },
{ url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" },
{ url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" },
{ url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" },
{ url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" },
{ url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" },
{ url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" },
{ url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" },
{ url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" },
{ url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" },
{ url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" },
{ url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" },
{ url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" },
{ url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" },
]
[[package]]
@@ -5014,10 +5014,10 @@ version = "2.13.0+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
"python_full_version < '3.12' and sys_platform == 'linux'",
]
@@ -5809,7 +5809,7 @@ wheels = [
[[package]]
name = "zensical"
version = "0.0.51"
version = "0.0.57"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -5821,18 +5821,18 @@ dependencies = [
{ name = "pyyaml" },
{ name = "tomli" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b8/f7/d07ffb268ca86afb26b7f32dbabe25dec03d3aa63ba4d876720c84681d33/zensical-0.0.51.tar.gz", hash = "sha256:de25de067bedfa18f916d7f366fd64a7fbf09bfcc615b44d1ddbe3b5fe02ab49", size = 3979640, upload-time = "2026-07-17T18:08:03.445Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/f4/fa40086c46a2e59e3d9239031f76623622e60e0d79f3df1282df2797a5c4/zensical-0.0.57.tar.gz", hash = "sha256:25fcbdf89a57153cc3ad1108a89d17c7226da5d3c551a8839c69cbd9c472a9d8", size = 4000458, upload-time = "2026-08-21T20:43:49.5Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/21/02db3e1fb3904016bfac310037c95b9f1eaaf0ffe7b4a84f14263a7d95df/zensical-0.0.51-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:134d776afa526098e05e34713e2f577c075e57a232e01b97842bb0206716afce", size = 12791154, upload-time = "2026-07-17T18:07:20.748Z" },
{ url = "https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535", size = 12692190, upload-time = "2026-07-17T18:07:24.553Z" },
{ url = "https://files.pythonhosted.org/packages/2e/90/7a60e126a10c37c6b789938ff17e73fe76bba707fa029cb40ac659aeaa82/zensical-0.0.51-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c9579809f88608e7aa2cff516fff9d267d74a843cf6088a5f4227de2f092bb5", size = 13139337, upload-time = "2026-07-17T18:07:27.885Z" },
{ url = "https://files.pythonhosted.org/packages/ae/c3/9101c97b90d4713ef2816db03366a45ae4762efebffd296737a2dd2df325/zensical-0.0.51-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296dc7a14aa28b81a58eb57df2d5c9c9a4b0de7e90c11d99c943354287952925", size = 13069851, upload-time = "2026-07-17T18:07:31.814Z" },
{ url = "https://files.pythonhosted.org/packages/d2/79/0474df9e15a2c18f6281a786e10177c1b6e16feac1c568e7f36ad39b339c/zensical-0.0.51-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f779d2d87b4bf228cf2e279bc0ae6bcf3b36a9335ff283a317d01f7c15ae46b2", size = 13451083, upload-time = "2026-07-17T18:07:35.543Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454", size = 13110446, upload-time = "2026-07-17T18:07:38.783Z" },
{ url = "https://files.pythonhosted.org/packages/d9/89/aa9a95f81771614c37bdc52b8ab21fcdef4c8de7c9cedf34e9bf62674281/zensical-0.0.51-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:186ef37e0eee0e969e2cfae47b1b97775e3164e2cba95c71faa4dd6ef47ed009", size = 13315871, upload-time = "2026-07-17T18:07:42.43Z" },
{ url = "https://files.pythonhosted.org/packages/08/11/1bf6e9ded29d376f8c12644cc4de04676b010fee8caa17f682606b1f16d5/zensical-0.0.51-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5d91ce246ed930224603083cef02ae8947132fc7c52901d72015ea03526fa58", size = 13344382, upload-time = "2026-07-17T18:07:46.066Z" },
{ url = "https://files.pythonhosted.org/packages/d6/ec/663f16ff82d08b212e7c3236a88bd332f73331f94ddac1c91aaf882bbd1e/zensical-0.0.51-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1108eae82c6e8ffc33026f60b485c1512647a5333be4f547166b7c8877b98af", size = 13499628, upload-time = "2026-07-17T18:07:49.196Z" },
{ url = "https://files.pythonhosted.org/packages/60/b4/7f1b6c3cf06d9f6ff5216523168a5d6ccc693444d5ceeb911eca97b30d98/zensical-0.0.51-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6fa0ecaf14f56841bfc595fa141396350c72aafbec73a016ebe3c824ed21ac72", size = 13451420, upload-time = "2026-07-17T18:07:52.563Z" },
{ url = "https://files.pythonhosted.org/packages/92/b9/49c37dc65105d1ca4a8b600a02c84ece00218d2293b2630611c620185ca3/zensical-0.0.57-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98867d1a6ea2c57f1ebcf4902f61601f427350f2df0c04e30cfac8ba6163cd29", size = 12888507, upload-time = "2026-08-21T20:43:20.365Z" },
{ url = "https://files.pythonhosted.org/packages/05/f7/54539984418de11387bbace39a744195555d32c98c95bf4d112b432548f5/zensical-0.0.57-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d7935d77d73a279545052e05d89d31960f30c1f33f53933f4c101fa271aee74", size = 12778169, upload-time = "2026-08-21T20:43:22.879Z" },
{ url = "https://files.pythonhosted.org/packages/40/16/74aa60aa4cfecd5bd31ce60cb6a092cb56f1bc1aaadcc173463861ea4eb5/zensical-0.0.57-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7046d433511d97aa603915f0f6792d15b7f839793abc2b66ab7b7ff753ecff5", size = 13230823, upload-time = "2026-08-21T20:43:25.141Z" },
{ url = "https://files.pythonhosted.org/packages/8a/d1/742d2487dd65dd18277daebcd37db56d5bd4a2408df02bde703ef8fb7b64/zensical-0.0.57-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab85c5066b95e3a877cf8971e4ce30abb1ca1459fbfcc631f0a5a2bab56351a4", size = 13170523, upload-time = "2026-08-21T20:43:27.456Z" },
{ url = "https://files.pythonhosted.org/packages/56/6f/12b570775d344f1a3d77e26d4ae0160bcac9e41ca38f7135352ccdf9b2c8/zensical-0.0.57-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f13d1b57ad3c8b8634933a93ea870ebac11245fe0c968d27fd2a059ee1c6311", size = 13549941, upload-time = "2026-08-21T20:43:29.964Z" },
{ url = "https://files.pythonhosted.org/packages/7b/4e/436e6fc76674244c084ef7f6f17dc5ff85c76b15aef77c48b703fd0a2dda/zensical-0.0.57-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:021dd8fb70d1816cd012684fcf45d32b8f88a0cd28b7cbe71e5f8564f6d5764d", size = 13210086, upload-time = "2026-08-21T20:43:32.098Z" },
{ url = "https://files.pythonhosted.org/packages/ef/52/20f3aeda9af1090f24241670a5cc20fff7494545fea9f5fa094c82f3dbdf/zensical-0.0.57-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7e10f3c27fdc3eac3a9ae6ddcd87f3f00edc9f332050923313c95537961bfadd", size = 13408253, upload-time = "2026-08-21T20:43:34.258Z" },
{ url = "https://files.pythonhosted.org/packages/e1/f2/2b18ba2f19674dbfcf745f3b66e005cc8efa66a1bcaba5e1b4f79467868a/zensical-0.0.57-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:78c85fee55c5aac3bdf8157e980c56397dca835167a5577c5429b5eb24ed990c", size = 13446689, upload-time = "2026-08-21T20:43:36.527Z" },
{ url = "https://files.pythonhosted.org/packages/05/ba/68cdba447a9097e5f97742eef046020c6fa42d82972849b3a46a0718e890/zensical-0.0.57-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:478d252e1924f3876e72cf7806967cb62e50d86eddb3da04bf43e882b532fa1b", size = 13598580, upload-time = "2026-08-21T20:43:38.646Z" },
{ url = "https://files.pythonhosted.org/packages/ec/89/6358a4df272328bed5bea90b04d43e73758bc45ff058c5cb2665e1147314/zensical-0.0.57-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66a9ca6b5f625b2a2b215eec2f3c72843a92d5d512042045ac6351d5dee9b339", size = 13557609, upload-time = "2026-08-21T20:43:40.866Z" },
]
[[package]]