Compare commits

...

12 Commits

Author SHA1 Message Date
Trenton H
59a25f8601 Updates this baseline too 2026-02-09 16:44:40 -08:00
Trenton H
5950f22e87 Typing 2026-02-09 16:32:39 -08:00
Trenton H
40ff58ad39 Wrong indentation level 2026-02-09 16:19:46 -08:00
Trenton H
914362224c Drops (direct) dependency on TQDM and removes its typing entry too 2026-02-09 15:54:29 -08:00
Trenton H
e0b45539a6 Replaces tqdm with rich 2026-02-09 15:53:27 -08:00
dependabot[bot]
c4ed4e7f36 Chore(deps): Bump the utilities-patch group across 1 directory with 3 updates (#12051)
Bumps the utilities-patch group with 3 updates in the / directory: llama-index-llms-openai, [prek](https://github.com/j178/prek) and [types-tqdm](https://github.com/typeshed-internal/stub_uploader).


Updates `llama-index-llms-openai` from 0.6.17 to 0.6.18

Updates `prek` from 0.3.1 to 0.3.2
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.3.1...v0.3.2)

Updates `types-tqdm` from 4.67.2.20260202 to 4.67.3.20260205
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: llama-index-llms-openai
  dependency-version: 0.6.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: prek
  dependency-version: 0.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: types-tqdm
  dependency-version: 4.67.3.20260205
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 15:52:29 -08:00
dependabot[bot]
0b89e2847e Chore(deps): Bump j178/prek-action in the actions group (#12033)
Bumps the actions group with 1 update: [j178/prek-action](https://github.com/j178/prek-action).


Updates `j178/prek-action` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: j178/prek-action
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 13:17:31 -08:00
dependabot[bot]
21623e4455 docker(deps): Bump astral-sh/uv from 0.9.29-python3.12-trixie-slim to 0.10.0-python3.12-trixie-slim (#12019)
* docker(deps): Bump astral-sh/uv

Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.9.29-python3.12-trixie-slim to 0.10.0-python3.12-trixie-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.29...0.10.0)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.10.0-python3.12-trixie-slim
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update DEFAULT_UV_VERSION to 0.10.x

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 13:02:48 -08:00
shamoon
9d7231d2dc Tweak: improve 2-digit year parsing (#12044) 2026-02-08 23:00:00 -08:00
GitHub Actions
4208d9255a Auto translate strings 2026-02-09 05:26:28 +00:00
shamoon
9e9e55758f Enhancement: pngx pdf viewer (#12043) 2026-02-08 21:24:43 -08:00
Trenton H
6a87c3f4dd Fixes handling the case where there is no status reported from celery (due to external termination of the worker) (#12040) 2026-02-08 17:26:35 -08:00
52 changed files with 1721 additions and 704 deletions

View File

@@ -23,7 +23,7 @@ concurrency:
group: backend-${{ github.event.pull_request.number || github.ref }} group: backend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
env: env:
DEFAULT_UV_VERSION: "0.9.x" DEFAULT_UV_VERSION: "0.10.x"
NLTK_DATA: "/usr/share/nltk_data" NLTK_DATA: "/usr/share/nltk_data"
jobs: jobs:
test: test:

View File

@@ -21,4 +21,4 @@ jobs:
with: with:
python-version: "3.14" python-version: "3.14"
- name: Run prek - name: Run prek
uses: j178/prek-action@v1.1.0 uses: j178/prek-action@v1.1.1

View File

@@ -277,8 +277,6 @@ src/documents/management/commands/document_exporter.py:0: error: Skipping analyz
src/documents/management/commands/document_exporter.py:0: error: Skipping analyzing "auditlog.models": module is installed, but missing library stubs or py.typed marker [import-untyped] src/documents/management/commands/document_exporter.py:0: error: Skipping analyzing "auditlog.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_importer.py:0: error: Argument 1 to "create_source_path_directory" has incompatible type "Path | None"; expected "Path" [arg-type]
src/documents/management/commands/document_importer.py:0: error: Argument 2 to "copy_file_with_basic_stats" has incompatible type "Path | None"; expected "Path | str" [arg-type]
src/documents/management/commands/document_importer.py:0: error: Attribute "version" already defined on line 0 [no-redef] src/documents/management/commands/document_importer.py:0: error: Attribute "version" already defined on line 0 [no-redef]
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]

View File

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

View File

@@ -77,7 +77,6 @@ dependencies = [
"setproctitle~=1.3.4", "setproctitle~=1.3.4",
"tika-client~=0.10.0", "tika-client~=0.10.0",
"torch~=2.10.0", "torch~=2.10.0",
"tqdm~=4.67.1",
"watchfiles>=1.1.1", "watchfiles>=1.1.1",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-reloaded>=2.7.5", "whoosh-reloaded>=2.7.5",
@@ -150,7 +149,6 @@ typing = [
"types-pytz", "types-pytz",
"types-redis", "types-redis",
"types-setuptools", "types-setuptools",
"types-tqdm",
] ]
[tool.uv] [tool.uv]

View File

@@ -86,7 +86,6 @@
], ],
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"ng2-pdf-viewer",
"file-saver", "file-saver",
"utif" "utif"
], ],

View File

@@ -72,7 +72,7 @@ test('should show a mobile preview', async ({ page }) => {
await page.setViewportSize({ width: 400, height: 1000 }) await page.setViewportSize({ width: 400, height: 1000 })
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible() await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
await page.getByRole('tab', { name: 'Preview' }).click() await page.getByRole('tab', { name: 'Preview' }).click()
await page.waitForSelector('pdf-viewer') await page.waitForSelector('pngx-pdf-viewer')
}) })
test('should show a list of notes', async ({ page }) => { test('should show a list of notes', async ({ page }) => {

View File

@@ -31,6 +31,10 @@ module.exports = {
moduleNameMapper: { moduleNameMapper: {
...esmPreset.moduleNameMapper, ...esmPreset.moduleNameMapper,
'^src/(.*)': '<rootDir>/src/$1', '^src/(.*)': '<rootDir>/src/$1',
'^pdfjs-dist/legacy/build/pdf\\.mjs$':
'<rootDir>/src/test/mocks/pdfjs-legacy-build-pdf.ts',
'^pdfjs-dist/web/pdf_viewer\\.mjs$':
'<rootDir>/src/test/mocks/pdfjs-web-pdf_viewer.ts',
}, },
workerIdleMemoryLimit: '512MB', workerIdleMemoryLimit: '512MB',
reporters: [ reporters: [

View File

@@ -1209,7 +1209,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1472</context> <context context-type="linenumber">1481</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1577733187050997705" datatype="html"> <trans-unit id="1577733187050997705" datatype="html">
@@ -2798,11 +2798,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1108</context> <context context-type="linenumber">1116</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1473</context> <context context-type="linenumber">1482</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -3393,7 +3393,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1061</context> <context context-type="linenumber">1069</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -3498,7 +3498,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1524</context> <context context-type="linenumber">1533</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6661109599266152398" datatype="html"> <trans-unit id="6661109599266152398" datatype="html">
@@ -3509,7 +3509,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1525</context> <context context-type="linenumber">1534</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5162686434580248853" datatype="html"> <trans-unit id="5162686434580248853" datatype="html">
@@ -3520,7 +3520,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1526</context> <context context-type="linenumber">1535</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8157388568390631653" datatype="html"> <trans-unit id="8157388568390631653" datatype="html">
@@ -6190,7 +6190,7 @@
<source>Open preview</source> <source>Open preview</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context> <context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2984628903434675339" datatype="html"> <trans-unit id="2984628903434675339" datatype="html">
@@ -7443,7 +7443,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">405</context> <context context-type="linenumber">403</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5028777105388019087" datatype="html"> <trans-unit id="5028777105388019087" datatype="html">
@@ -7649,63 +7649,63 @@
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">499</context> <context context-type="linenumber">497</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2218903673684131427" datatype="html"> <trans-unit id="2218903673684131427" datatype="html">
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source> <source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">428,430</context> <context context-type="linenumber">434,436</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3200733026060976258" datatype="html"> <trans-unit id="3200733026060976258" datatype="html">
<source>Document changes detected</source> <source>Document changes detected</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">467</context> <context context-type="linenumber">473</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2887155916749964" datatype="html"> <trans-unit id="2887155916749964" datatype="html">
<source>The version of this document in your browser session appears older than the existing version.</source> <source>The version of this document in your browser session appears older than the existing version.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">468</context> <context context-type="linenumber">474</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="237142428785956348" datatype="html"> <trans-unit id="237142428785956348" datatype="html">
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source> <source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">469</context> <context context-type="linenumber">475</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8720977247725652816" datatype="html"> <trans-unit id="8720977247725652816" datatype="html">
<source>Ok</source> <source>Ok</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">471</context> <context context-type="linenumber">477</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6142395741265832184" datatype="html"> <trans-unit id="6142395741265832184" datatype="html">
<source>Next document</source> <source>Next document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">597</context> <context context-type="linenumber">605</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="651985345816518480" datatype="html"> <trans-unit id="651985345816518480" datatype="html">
<source>Previous document</source> <source>Previous document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">607</context> <context context-type="linenumber">615</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2885986061416655600" datatype="html"> <trans-unit id="2885986061416655600" datatype="html">
<source>Close document</source> <source>Close document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">615</context> <context context-type="linenumber">623</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context> <context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
@@ -7716,67 +7716,67 @@
<source>Save document</source> <source>Save document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">622</context> <context context-type="linenumber">630</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1784543155727940353" datatype="html"> <trans-unit id="1784543155727940353" datatype="html">
<source>Save and close / next</source> <source>Save and close / next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">631</context> <context context-type="linenumber">639</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5758784066858623886" datatype="html"> <trans-unit id="5758784066858623886" datatype="html">
<source>Error retrieving metadata</source> <source>Error retrieving metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">686</context> <context context-type="linenumber">694</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3456881259945295697" datatype="html"> <trans-unit id="3456881259945295697" datatype="html">
<source>Error retrieving suggestions.</source> <source>Error retrieving suggestions.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">741</context> <context context-type="linenumber">749</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2194092841814123758" datatype="html"> <trans-unit id="2194092841814123758" datatype="html">
<source>Document &quot;<x id="PH" equiv-text="newValues.title"/>&quot; saved successfully.</source> <source>Document &quot;<x id="PH" equiv-text="newValues.title"/>&quot; saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">950</context> <context context-type="linenumber">958</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">974</context> <context context-type="linenumber">982</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6626387786259219838" datatype="html"> <trans-unit id="6626387786259219838" datatype="html">
<source>Error saving document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;</source> <source>Error saving document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">980</context> <context context-type="linenumber">988</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="448882439049417053" datatype="html"> <trans-unit id="448882439049417053" datatype="html">
<source>Error saving document</source> <source>Error saving document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1030</context> <context context-type="linenumber">1038</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8410796510716511826" datatype="html"> <trans-unit id="8410796510716511826" datatype="html">
<source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document.title"/>&quot; to the trash?</source> <source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document.title"/>&quot; to the trash?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1062</context> <context context-type="linenumber">1070</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="282586936710748252" datatype="html"> <trans-unit id="282586936710748252" datatype="html">
<source>Documents can be restored prior to permanent deletion.</source> <source>Documents can be restored prior to permanent deletion.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1063</context> <context context-type="linenumber">1071</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -7787,7 +7787,7 @@
<source>Move to trash</source> <source>Move to trash</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1065</context> <context context-type="linenumber">1073</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -7798,14 +7798,14 @@
<source>Error deleting document</source> <source>Error deleting document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1084</context> <context context-type="linenumber">1092</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="619486176823357521" datatype="html"> <trans-unit id="619486176823357521" datatype="html">
<source>Reprocess confirm</source> <source>Reprocess confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1104</context> <context context-type="linenumber">1112</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -7816,102 +7816,102 @@
<source>This operation will permanently recreate the archive file for this document.</source> <source>This operation will permanently recreate the archive file for this document.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1105</context> <context context-type="linenumber">1113</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="302054111564709516" datatype="html"> <trans-unit id="302054111564709516" datatype="html">
<source>The archive file will be re-generated with the current settings.</source> <source>The archive file will be re-generated with the current settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1106</context> <context context-type="linenumber">1114</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8251197608401006898" datatype="html"> <trans-unit id="8251197608401006898" datatype="html">
<source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> <source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1116</context> <context context-type="linenumber">1124</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4409560272830824468" datatype="html"> <trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source> <source>Error executing operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1127</context> <context context-type="linenumber">1135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6030453331794586802" datatype="html"> <trans-unit id="6030453331794586802" datatype="html">
<source>Error downloading document</source> <source>Error downloading document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1176</context> <context context-type="linenumber">1184</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4458954481601077369" datatype="html"> <trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source> <source>Page Fit</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1253</context> <context context-type="linenumber">1263</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4663705961777238777" datatype="html"> <trans-unit id="4663705961777238777" datatype="html">
<source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source> <source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1491</context> <context context-type="linenumber">1500</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9043972994040261999" datatype="html"> <trans-unit id="9043972994040261999" datatype="html">
<source>Error executing PDF edit operation</source> <source>Error executing PDF edit operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1503</context> <context context-type="linenumber">1512</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6172690334763056188" datatype="html"> <trans-unit id="6172690334763056188" datatype="html">
<source>Please enter the current password before attempting to remove it.</source> <source>Please enter the current password before attempting to remove it.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1514</context> <context context-type="linenumber">1523</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="968660764814228922" datatype="html"> <trans-unit id="968660764814228922" datatype="html">
<source>Password removal operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source> <source>Password removal operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1546</context> <context context-type="linenumber">1555</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2282118435712883014" datatype="html"> <trans-unit id="2282118435712883014" datatype="html">
<source>Error executing password removal operation</source> <source>Error executing password removal operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1560</context> <context context-type="linenumber">1569</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3740891324955700797" datatype="html"> <trans-unit id="3740891324955700797" datatype="html">
<source>Print failed.</source> <source>Print failed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1597</context> <context context-type="linenumber">1606</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6457245677384603573" datatype="html"> <trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source> <source>Error loading document for printing.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1609</context> <context context-type="linenumber">1618</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6085793215710522488" datatype="html"> <trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1674</context> <context context-type="linenumber">1683</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1678</context> <context context-type="linenumber">1687</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4958946940233632319" datatype="html"> <trans-unit id="4958946940233632319" datatype="html">
@@ -11234,14 +11234,14 @@
<source>Prev</source> <source>Prev</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">404</context> <context context-type="linenumber">402</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1241348629231510663" datatype="html"> <trans-unit id="1241348629231510663" datatype="html">
<source>End</source> <source>End</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">406</context> <context context-type="linenumber">404</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>

View File

@@ -27,12 +27,12 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.4.0",
"ngx-bootstrap-icons": "^1.9.3", "ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.1.0", "ngx-color": "^10.1.0",
"ngx-cookie-service": "^21.1.0", "ngx-cookie-service": "^21.1.0",
"ngx-device-detector": "^11.0.0", "ngx-device-detector": "^11.0.0",
"ngx-ui-tour-ng-bootstrap": "^18.0.0", "ngx-ui-tour-ng-bootstrap": "^18.0.0",
"pdfjs-dist": "^5.4.624",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0", "utif": "^3.1.0",

157
src-ui/pnpm-lock.yaml generated
View File

@@ -56,9 +56,6 @@ importers:
mime-names: mime-names:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
ng2-pdf-viewer:
specifier: ^10.4.0
version: 10.4.0
ngx-bootstrap-icons: ngx-bootstrap-icons:
specifier: ^1.9.3 specifier: ^1.9.3
version: 1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)) version: 1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))
@@ -74,6 +71,9 @@ importers:
ngx-ui-tour-ng-bootstrap: ngx-ui-tour-ng-bootstrap:
specifier: ^18.0.0 specifier: ^18.0.0
version: 18.0.0(2a89effa12f6df8cde064aa7713e7e29) version: 18.0.0(2a89effa12f6df8cde064aa7713e7e29)
pdfjs-dist:
specifier: ^5.4.624
version: 5.4.624
rxjs: rxjs:
specifier: ^7.8.2 specifier: ^7.8.2
version: 7.8.2 version: 7.8.2
@@ -2130,6 +2130,76 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas-android-arm64@0.1.90':
resolution: {integrity: sha512-3JBULVF+BIgr7yy7Rf8UjfbkfFx4CtXrkJFD1MDgKJ83b56o0U9ciT8ZGTCNmwWkzu8RbNKlyqPP3KYRG88y7Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.90':
resolution: {integrity: sha512-L8XVTXl+8vd8u7nPqcX77NyG5RuFdVsJapQrKV9WE3jBayq1aSMht/IH7Dwiz/RNJ86E5ZSg9pyUPFIlx52PZA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.90':
resolution: {integrity: sha512-h0ukhlnGhacbn798VWYTQZpf6JPDzQYaow+vtQ2Fat7j7ImDdpg6tfeqvOTO1r8wS+s+VhBIFITC7aA1Aik0ZQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
resolution: {integrity: sha512-JCvTl99b/RfdBtgftqrf+5UNF7GIbp7c5YBFZ+Bd6++4Y3phaXG/4vD9ZcF1bw1P4VpALagHmxvodHuQ9/TfTg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.90':
resolution: {integrity: sha512-vbWFp8lrP8NIM5L4zNOwnsqKIkJo0+GIRUDcLFV9XEJCptCc1FY6/tM02PT7GN4PBgochUPB1nBHdji6q3ieyQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.90':
resolution: {integrity: sha512-8Bc0BgGEeOaux4EfIfNzcRRw0JE+lO9v6RWQFCJNM9dJFE4QJffTf88hnmbOaI6TEMpgWOKipbha3dpIdUqb/g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
resolution: {integrity: sha512-0iiVDG5IH+gJb/YUrY/pRdbsjcgvwUmeckL/0gShWAA7004ygX2ST69M1wcfyxXrzFYjdF8S/Sn6aCAeBi89XQ==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.90':
resolution: {integrity: sha512-SkKmlHMvA5spXuKfh7p6TsScDf7lp5XlMbiUhjdCtWdOS6Qke/A4qGVOciy6piIUCJibL+YX+IgdGqzm2Mpx/w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.90':
resolution: {integrity: sha512-o6QgS10gAS4vvELGDOOWYfmERXtkVRYFWBCjomILWfMgCvBVutn8M97fsMW5CrEuJI8YuxuJ7U+/DQ9oG93vDA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.90':
resolution: {integrity: sha512-2UHO/DC1oyuSjeCAhHA0bTD9qsg58kknRqjJqRfvIEFtdqdtNTcWXMCT9rQCuJ8Yx5ldhyh2SSp7+UDqD2tXZQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.90':
resolution: {integrity: sha512-48CxEbzua5BP4+OumSZdi3+9fNiRO8cGNBlO2bKwx1PoyD1R2AXzPtqd/no1f1uSl0W2+ihOO1v3pqT3USbmgQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.90':
resolution: {integrity: sha512-vO9j7TfwF9qYCoTOPO39yPLreTRslBVOaeIwhDZkizDvBb0MounnTl0yeWUMBxP4Pnkg9Sv+3eQwpxNUmTwt0w==}
engines: {node: '>= 10'}
'@napi-rs/nice-android-arm-eabi@1.1.1': '@napi-rs/nice-android-arm-eabi@1.1.1':
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -5015,9 +5085,6 @@ packages:
neo-async@2.6.2: neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
ng2-pdf-viewer@10.4.0:
resolution: {integrity: sha512-TPh1oLZoeARggreTG60Sl3ikSn+Z3+At9pLZ0o/vxPjc7mW2ok2XPyl2Oqz7VyP80ipVorldm1hsLPBmNe2zzA==}
ngx-bootstrap-icons@1.9.3: ngx-bootstrap-icons@1.9.3:
resolution: {integrity: sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==} resolution: {integrity: sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==}
engines: {node: '>= 16.18.1', npm: '>= 8.11.0'} engines: {node: '>= 16.18.1', npm: '>= 8.11.0'}
@@ -5084,6 +5151,9 @@ packages:
node-int64@0.4.0: node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-readable-to-web-readable-stream@0.4.2:
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -5279,13 +5349,9 @@ packages:
path-to-regexp@8.3.0: path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
path2d@0.2.2: pdfjs-dist@5.4.624:
resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==} resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
engines: {node: '>=6'} engines: {node: '>=20.16.0 || >=22.3.0'}
pdfjs-dist@4.8.69:
resolution: {integrity: sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==}
engines: {node: '>=18'}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -8796,6 +8862,54 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true optional: true
'@napi-rs/canvas-android-arm64@0.1.90':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.90':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.90':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.90':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.90':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.90':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.90':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.90':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.90':
optional: true
'@napi-rs/canvas@0.1.90':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.90
'@napi-rs/canvas-darwin-arm64': 0.1.90
'@napi-rs/canvas-darwin-x64': 0.1.90
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.90
'@napi-rs/canvas-linux-arm64-gnu': 0.1.90
'@napi-rs/canvas-linux-arm64-musl': 0.1.90
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.90
'@napi-rs/canvas-linux-x64-gnu': 0.1.90
'@napi-rs/canvas-linux-x64-musl': 0.1.90
'@napi-rs/canvas-win32-arm64-msvc': 0.1.90
'@napi-rs/canvas-win32-x64-msvc': 0.1.90
optional: true
'@napi-rs/nice-android-arm-eabi@1.1.1': '@napi-rs/nice-android-arm-eabi@1.1.1':
optional: true optional: true
@@ -11975,11 +12089,6 @@ snapshots:
neo-async@2.6.2: {} neo-async@2.6.2: {}
ng2-pdf-viewer@10.4.0:
dependencies:
pdfjs-dist: 4.8.69
tslib: 2.8.1
ngx-bootstrap-icons@1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)): ngx-bootstrap-icons@1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)):
dependencies: dependencies:
'@angular/common': 21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) '@angular/common': 21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)
@@ -12060,6 +12169,9 @@ snapshots:
node-int64@0.4.0: {} node-int64@0.4.0: {}
node-readable-to-web-readable-stream@0.4.2:
optional: true
node-releases@2.0.27: {} node-releases@2.0.27: {}
nopt@9.0.0: nopt@9.0.0:
@@ -12291,13 +12403,10 @@ snapshots:
path-to-regexp@8.3.0: {} path-to-regexp@8.3.0: {}
path2d@0.2.2: pdfjs-dist@5.4.624:
optional: true
pdfjs-dist@4.8.69:
optionalDependencies: optionalDependencies:
canvas: 3.0.0 '@napi-rs/canvas': 0.1.90
path2d: 0.2.2 node-readable-to-web-readable-stream: 0.4.2
picocolors@1.1.1: {} picocolors@1.1.1: {}

View File

@@ -100,10 +100,10 @@ const mock = () => {
} }
} }
Object.defineProperty(window, 'open', { value: jest.fn() }) Object.defineProperty(globalThis, 'open', { value: jest.fn() })
Object.defineProperty(window, 'localStorage', { value: mock() }) Object.defineProperty(globalThis, 'localStorage', { value: mock() })
Object.defineProperty(window, 'sessionStorage', { value: mock() }) Object.defineProperty(globalThis, 'sessionStorage', { value: mock() })
Object.defineProperty(window, 'getComputedStyle', { Object.defineProperty(globalThis, 'getComputedStyle', {
value: () => ['-webkit-appearance'], value: () => ['-webkit-appearance'],
}) })
Object.defineProperty(navigator, 'clipboard', { Object.defineProperty(navigator, 'clipboard', {
@@ -115,13 +115,33 @@ Object.defineProperty(navigator, 'canShare', { value: () => true })
if (!navigator.share) { if (!navigator.share) {
Object.defineProperty(navigator, 'share', { value: jest.fn() }) Object.defineProperty(navigator, 'share', { value: jest.fn() })
} }
if (!URL.createObjectURL) { if (!globalThis.URL.createObjectURL) {
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() }) Object.defineProperty(globalThis.URL, 'createObjectURL', { value: jest.fn() })
} }
if (!URL.revokeObjectURL) { if (!globalThis.URL.revokeObjectURL) {
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() }) Object.defineProperty(globalThis.URL, 'revokeObjectURL', { value: jest.fn() })
} }
Object.defineProperty(window, 'ResizeObserver', { value: mock() }) class MockResizeObserver {
private readonly callback: ResizeObserverCallback
constructor(callback: ResizeObserverCallback) {
this.callback = callback
}
observe = jest.fn()
unobserve = jest.fn()
disconnect = jest.fn()
trigger = (entries: ResizeObserverEntry[] = []) => {
this.callback(entries, this)
}
}
Object.defineProperty(globalThis, 'ResizeObserver', {
writable: true,
configurable: true,
value: MockResizeObserver,
})
if (typeof IntersectionObserver === 'undefined') { if (typeof IntersectionObserver === 'undefined') {
class MockIntersectionObserver { class MockIntersectionObserver {
@@ -136,7 +156,7 @@ if (typeof IntersectionObserver === 'undefined') {
takeRecords = jest.fn() takeRecords = jest.fn()
} }
Object.defineProperty(window, 'IntersectionObserver', { Object.defineProperty(globalThis, 'IntersectionObserver', {
writable: true, writable: true,
configurable: true, configurable: true,
value: MockIntersectionObserver, value: MockIntersectionObserver,

View File

@@ -222,8 +222,8 @@
</div> </div>
<div class="col"> <div class="col">
<select class="form-select" formControlName="pdfViewerDefaultZoom"> <select class="form-select" formControlName="pdfViewerDefaultZoom">
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option> <option [ngValue]="PdfZoomScale.PageWidth" i18n>Fit width</option>
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option> <option [ngValue]="PdfZoomScale.PageFit" i18n>Fit page</option>
</select> </select>
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p> <p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
</div> </div>

View File

@@ -65,8 +65,8 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
import { SelectComponent } from '../../common/input/select/select.component' import { SelectComponent } from '../../common/input/select/select.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode' import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
import { PdfZoomScale } from '../../common/pdf-viewer/pdf-viewer.types'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { ZoomSetting } from '../../document-detail/zoom-setting'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
enum SettingsNavIDs { enum SettingsNavIDs {
@@ -196,7 +196,7 @@ export class SettingsComponent
public readonly GlobalSearchType = GlobalSearchType public readonly GlobalSearchType = GlobalSearchType
public readonly ZoomSetting = ZoomSetting public readonly PdfZoomScale = PdfZoomScale
public readonly PdfEditorEditMode = PdfEditorEditMode public readonly PdfEditorEditMode = PdfEditorEditMode

View File

@@ -1,4 +1,4 @@
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer> <pngx-pdf-viewer class="visually-hidden" [src]="pdfSrc" [renderMode]="PdfRenderMode.Single" [page]="1" [selectable]="false" (afterLoadComplete)="pdfLoaded($event)"></pngx-pdf-viewer>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{ title }}</h4> <h4 class="modal-title">{{ title }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button> <button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
@@ -59,7 +59,7 @@
<span class="placeholder w-100 h-100"></span> <span class="placeholder w-100 h-100"></span>
</div> </div>
} }
<pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false" (page-rendered)="p.loaded = true"></pdf-viewer> <pngx-pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [renderMode]="PdfRenderMode.Single" (rendered)="p.loaded = true"></pngx-pdf-viewer>
} @placeholder { } @placeholder {
<div class="placeholder-glow w-100 h-100 z-10"> <div class="placeholder-glow w-100 h-100 z-10">
<span class="placeholder w-100 h-100"></span> <span class="placeholder w-100 h-100"></span>

View File

@@ -15,13 +15,13 @@
background-color: gray; background-color: gray;
height: 240px; height: 240px;
pdf-viewer { pngx-pdf-viewer {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
::ng-deep .ng2-pdf-viewer-container { ::ng-deep .pngx-pdf-viewer-container {
overflow: hidden; overflow: hidden;
} }

View File

@@ -6,12 +6,16 @@ import {
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import {
PdfRenderMode,
PngxPdfDocumentProxy,
} from '../pdf-viewer/pdf-viewer.types'
import { PdfEditorEditMode } from './pdf-editor-edit-mode' import { PdfEditorEditMode } from './pdf-editor-edit-mode'
interface PageOperation { interface PageOperation {
@@ -29,11 +33,12 @@ interface PageOperation {
imports: [ imports: [
DragDropModule, DragDropModule,
FormsModule, FormsModule,
PdfViewerModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
PngxPdfViewerComponent,
], ],
}) })
export class PDFEditorComponent extends ConfirmDialogComponent { export class PDFEditorComponent extends ConfirmDialogComponent {
PdfRenderMode = PdfRenderMode
public PdfEditorEditMode = PdfEditorEditMode public PdfEditorEditMode = PdfEditorEditMode
private documentService = inject(DocumentService) private documentService = inject(DocumentService)
@@ -53,7 +58,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
return this.documentService.getPreviewUrl(this.documentID) return this.documentService.getPreviewUrl(this.documentID)
} }
pdfLoaded(pdf: PDFDocumentProxy) { pdfLoaded(pdf: PngxPdfDocumentProxy) {
this.totalPages = pdf.numPages this.totalPages = pdf.numPages
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({ this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
page: i + 1, page: i + 1,

View File

@@ -0,0 +1,3 @@
<div #container class="pngx-pdf-viewer-container">
<div #viewer class="pdfViewer"></div>
</div>

View File

@@ -0,0 +1,153 @@
:host {
display: block;
width: 100%;
height: 100%;
position: relative;
}
:host ::ng-deep .pngx-pdf-viewer-container {
position: absolute;
inset: 0;
overflow: auto;
}
:host ::ng-deep .pdfViewer {
--scale-factor: 1;
--page-bg-color: unset;
padding-bottom: 0;
}
:host ::ng-deep .pdfViewer .page {
--user-unit: 1;
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
--scale-round-x: 1px;
--scale-round-y: 1px;
direction: ltr;
margin: 0 auto 10px;
border: 0;
position: relative;
overflow: visible;
background-clip: content-box;
background-color: var(--page-bg-color, rgb(255 255 255));
}
:host ::ng-deep .pdfViewer > .page:last-of-type {
margin-bottom: 0;
}
:host ::ng-deep .pdfViewer.singlePageView {
display: inline-block;
}
:host ::ng-deep .pdfViewer.singlePageView .page {
margin: 0;
border: none;
}
:host ::ng-deep .pdfViewer .canvasWrapper {
overflow: hidden;
width: 100%;
height: 100%;
}
:host ::ng-deep .pdfViewer .canvasWrapper canvas {
position: absolute;
top: 0;
left: 0;
margin: 0;
display: block;
width: 100%;
height: 100%;
contain: content;
}
:host ::ng-deep .textLayer {
position: absolute;
text-align: initial;
inset: 0;
overflow: clip;
opacity: 1;
line-height: 1;
text-size-adjust: none;
transform-origin: 0 0;
caret-color: CanvasText;
z-index: 0;
user-select: text;
--min-font-size: 1;
--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
--min-font-size-inv: calc(1 / var(--min-font-size));
}
:host ::ng-deep .textLayer.highlighting {
touch-action: none;
}
:host ::ng-deep .textLayer :is(span, br) {
position: absolute;
white-space: pre;
color: transparent;
cursor: text;
transform-origin: 0% 0%;
}
:host ::ng-deep .textLayer > :not(.markedContent),
:host ::ng-deep .textLayer .markedContent span:not(.markedContent) {
z-index: 1;
--font-height: 0;
font-size: calc(var(--text-scale-factor) * var(--font-height));
--scale-x: 1;
--rotate: 0deg;
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
scale(var(--min-font-size-inv));
}
:host ::ng-deep .textLayer .markedContent {
display: contents;
}
:host ::ng-deep .textLayer span[role='img'] {
user-select: none;
cursor: default;
}
:host ::ng-deep .textLayer .highlight {
--highlight-bg-color: rgb(180 0 170 / 0.25);
--highlight-selected-bg-color: rgb(0 100 0 / 0.25);
--highlight-backdrop-filter: none;
--highlight-selected-backdrop-filter: none;
margin: -1px;
padding: 1px;
background-color: var(--highlight-bg-color);
backdrop-filter: var(--highlight-backdrop-filter);
border-radius: 4px;
}
:host ::ng-deep .appended:is(.textLayer .highlight) {
position: initial;
}
:host ::ng-deep .begin:is(.textLayer .highlight) {
border-radius: 4px 0 0 4px;
}
:host ::ng-deep .end:is(.textLayer .highlight) {
border-radius: 0 4px 4px 0;
}
:host ::ng-deep .middle:is(.textLayer .highlight) {
border-radius: 0;
}
:host ::ng-deep .selected:is(.textLayer .highlight) {
background-color: var(--highlight-selected-bg-color);
}
:host ::ng-deep .textLayer ::selection {
background: rgba(30, 100, 255, 0.35);
}
:host ::ng-deep .annotationLayer {
position: absolute;
inset: 0;
pointer-events: none;
}

View File

@@ -0,0 +1,299 @@
import { SimpleChange } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
import { PngxPdfViewerComponent } from './pdf-viewer.component'
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
describe('PngxPdfViewerComponent', () => {
let fixture: ComponentFixture<PngxPdfViewerComponent>
let component: PngxPdfViewerComponent
const initComponent = async (src = 'test.pdf') => {
component.src = src
fixture.detectChanges()
await fixture.whenStable()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PngxPdfViewerComponent],
}).compileComponents()
fixture = TestBed.createComponent(PngxPdfViewerComponent)
component = fixture.componentInstance
})
it('loads a document and emits events', async () => {
const loadSpy = jest.fn()
const renderedSpy = jest.fn()
component.afterLoadComplete.subscribe(loadSpy)
component.rendered.subscribe(renderedSpy)
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
'/assets/js/pdf.worker.min.mjs'
)
const isVisible = (component as any).findController.onIsPageVisible as
| (() => boolean)
| undefined
expect(isVisible?.()).toBe(true)
expect(loadSpy).toHaveBeenCalledWith(
expect.objectContaining({ numPages: 1 })
)
expect(renderedSpy).toHaveBeenCalled()
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
})
it('initializes single-page viewer and disables text layer', async () => {
component.renderMode = PdfRenderMode.Single
component.selectable = false
await initComponent()
const viewer = (component as any).pdfViewer as PDFSinglePageViewer & {
options: Record<string, unknown>
}
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
expect(viewer.options.textLayerMode).toBe(0)
})
it('applies zoom, rotation, and page changes', async () => {
await initComponent()
const pageSpy = jest.fn()
component.pageChange.subscribe(pageSpy)
component.zoomScale = PdfZoomScale.PageFit
component.zoom = PdfZoomLevel.Two
component.rotation = 90
component.page = 2
component.ngOnChanges({
zoomScale: new SimpleChange(
PdfZoomScale.PageWidth,
PdfZoomScale.PageFit,
false
),
zoom: new SimpleChange(PdfZoomLevel.One, PdfZoomLevel.Two, false),
rotation: new SimpleChange(undefined, 90, false),
page: new SimpleChange(undefined, 2, false),
})
const viewer = (component as any).pdfViewer as PDFViewer
expect(viewer.pagesRotation).toBe(90)
expect(viewer.currentPageNumber).toBe(2)
expect(pageSpy).toHaveBeenCalledWith(2)
viewer.currentScale = 1
;(component as any).applyScale()
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
expect(viewer.currentScale).toBe(2)
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
component.page = 2
;(component as any).lastViewerPage = 2
;(component as any).applyViewerState()
expect((component as any).lastViewerPage).toBeUndefined()
expect(applyScaleSpy).toHaveBeenCalled()
})
it('dispatches find when search query changes after render', async () => {
await initComponent()
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
;(component as any).hasRenderedPage = true
component.searchQuery = 'needle'
component.ngOnChanges({
searchQuery: new SimpleChange('', 'needle', false),
})
expect(dispatchSpy).toHaveBeenCalledWith('find', {
query: 'needle',
caseSensitive: false,
highlightAll: true,
phraseSearch: true,
})
component.ngOnChanges({
searchQuery: new SimpleChange('needle', 'needle', false),
})
expect(dispatchSpy).toHaveBeenCalledTimes(1)
})
it('emits error when document load fails', async () => {
const errorSpy = jest.fn()
component.loadError.subscribe(errorSpy)
jest.spyOn(pdfjs, 'getDocument').mockImplementationOnce(() => {
return {
promise: Promise.reject(new Error('boom')),
destroy: jest.fn(),
} as any
})
await initComponent('bad.pdf')
expect(errorSpy).toHaveBeenCalled()
})
it('cleans up resources on destroy', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as { cleanup: jest.Mock }
const loadingTask = (component as any).loadingTask as unknown as {
destroy: jest.Mock
}
const resizeObserver = (component as any).resizeObserver as unknown as {
disconnect: jest.Mock
}
const eventBus = (component as any).eventBus as { off: jest.Mock }
jest.spyOn(viewer, 'cleanup')
jest.spyOn(loadingTask, 'destroy')
jest.spyOn(resizeObserver, 'disconnect')
jest.spyOn(eventBus, 'off')
component.ngOnDestroy()
expect(eventBus.off).toHaveBeenCalledWith(
'pagerendered',
expect.any(Function)
)
expect(eventBus.off).toHaveBeenCalledWith('pagesinit', expect.any(Function))
expect(eventBus.off).toHaveBeenCalledWith(
'pagechanging',
expect.any(Function)
)
expect(resizeObserver.disconnect).toHaveBeenCalled()
expect(loadingTask.destroy).toHaveBeenCalled()
expect(viewer.cleanup).toHaveBeenCalled()
expect((component as any).pdfViewer).toBeUndefined()
})
it('skips work when viewer is missing or has no pages', () => {
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
;(component as any).dispatchFindIfReady()
expect(dispatchSpy).not.toHaveBeenCalled()
;(component as any).applyViewerState()
;(component as any).applyScale()
const viewer = new PDFViewer({ eventBus: undefined })
viewer.pagesCount = 0
;(component as any).pdfViewer = viewer
viewer.currentScale = 5
;(component as any).applyScale()
expect(viewer.currentScale).toBe(5)
})
it('returns early on src change in ngOnChanges', () => {
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const initSpy = jest.spyOn(component as any, 'initViewer')
const scaleSpy = jest.spyOn(component as any, 'applyViewerState')
const resizeSpy = jest.spyOn(component as any, 'setupResizeObserver')
component.ngOnChanges({
src: new SimpleChange(undefined, 'test.pdf', true),
zoomScale: new SimpleChange(
PdfZoomScale.PageWidth,
PdfZoomScale.PageFit,
false
),
})
expect(loadSpy).toHaveBeenCalled()
expect(resizeSpy).not.toHaveBeenCalled()
expect(initSpy).not.toHaveBeenCalled()
expect(scaleSpy).not.toHaveBeenCalled()
})
it('applies viewer state after view init when already loaded', () => {
const applySpy = jest.spyOn(component as any, 'applyViewerState')
;(component as any).hasLoaded = true
;(component as any).pdf = { numPages: 1 }
fixture.detectChanges()
expect(applySpy).toHaveBeenCalled()
})
it('skips viewer state after view init when no pdf is available', () => {
const applySpy = jest.spyOn(component as any, 'applyViewerState')
;(component as any).hasLoaded = true
fixture.detectChanges()
expect(applySpy).not.toHaveBeenCalled()
})
it('does not reload when already loaded', async () => {
await initComponent()
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
const callCount = getDocumentSpy.mock.calls.length
await (component as any).loadDocument()
expect(getDocumentSpy).toHaveBeenCalledTimes(callCount)
})
it('runs applyScale on resize observer notifications', async () => {
await initComponent()
const applySpy = jest.spyOn(component as any, 'applyScale')
const resizeObserver = (component as any).resizeObserver as {
trigger: () => void
}
resizeObserver.trigger()
expect(applySpy).toHaveBeenCalled()
})
it('skips page work when no pages are available', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as PDFViewer
viewer.pagesCount = 0
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
component.page = undefined
;(component as any).lastViewerPage = 1
;(component as any).applyViewerState()
expect(applyScaleSpy).not.toHaveBeenCalled()
expect((component as any).lastViewerPage).toBe(1)
})
it('falls back to a default zoom when input is invalid', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as PDFViewer
viewer.currentScale = 3
component.zoom = 'not-a-number' as PdfZoomLevel
;(component as any).applyScale()
expect(viewer.currentScale).toBe(3)
})
it('re-initializes viewer on selectable or render mode changes', async () => {
await initComponent()
const initSpy = jest.spyOn(component as any, 'initViewer')
component.selectable = false
component.renderMode = PdfRenderMode.Single
component.ngOnChanges({
selectable: new SimpleChange(true, false, false),
renderMode: new SimpleChange(
PdfRenderMode.All,
PdfRenderMode.Single,
false
),
})
expect(initSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,266 @@
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core'
import {
getDocument,
GlobalWorkerOptions,
PDFDocumentLoadingTask,
PDFDocumentProxy,
} from 'pdfjs-dist/legacy/build/pdf.mjs'
import {
EventBus,
PDFFindController,
PDFLinkService,
PDFSinglePageViewer,
PDFViewer,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import {
PdfRenderMode,
PdfSource,
PdfZoomLevel,
PdfZoomScale,
PngxPdfDocumentProxy,
} from './pdf-viewer.types'
@Component({
selector: 'pngx-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrl: './pdf-viewer.component.scss',
})
export class PngxPdfViewerComponent
implements AfterViewInit, OnChanges, OnDestroy
{
@Input() src!: PdfSource
@Input() page?: number
@Output() pageChange = new EventEmitter<number>()
@Input() rotation?: number
@Input() renderMode: PdfRenderMode = PdfRenderMode.All
@Input() selectable = true
@Input() searchQuery = ''
@Input() zoom: PdfZoomLevel = PdfZoomLevel.One
@Input() zoomScale: PdfZoomScale = PdfZoomScale.PageWidth
@Output() afterLoadComplete = new EventEmitter<PngxPdfDocumentProxy>()
@Output() rendered = new EventEmitter<void>()
@Output() loadError = new EventEmitter<unknown>()
@ViewChild('container', { static: true })
private readonly container!: ElementRef<HTMLDivElement>
@ViewChild('viewer', { static: true })
private readonly viewer!: ElementRef<HTMLDivElement>
private hasLoaded = false
private loadingTask?: PDFDocumentLoadingTask
private resizeObserver?: ResizeObserver
private pdf?: PDFDocumentProxy
private pdfViewer?: PDFViewer | PDFSinglePageViewer
private hasRenderedPage = false
private lastFindQuery = ''
private lastViewerPage?: number
private readonly eventBus = new EventBus()
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
private readonly findController = new PDFFindController({
eventBus: this.eventBus,
linkService: this.linkService,
updateMatchesCountOnProgress: false,
})
private readonly onPageRendered = () => {
this.hasRenderedPage = true
this.dispatchFindIfReady()
this.rendered.emit()
}
private readonly onPagesInit = () => this.applyScale()
private readonly onPageChanging = (evt: { pageNumber: number }) => {
// Avoid [(page)] two-way binding re-triggers navigation
this.lastViewerPage = evt.pageNumber
this.pageChange.emit(evt.pageNumber)
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['src']) {
this.hasLoaded = false
this.loadDocument()
return
}
if (changes['zoomScale']) {
this.setupResizeObserver()
}
if (changes['selectable'] || changes['renderMode']) {
this.initViewer()
}
if (
changes['page'] ||
changes['zoom'] ||
changes['zoomScale'] ||
changes['rotation']
) {
this.applyViewerState()
}
if (changes['searchQuery']) {
this.dispatchFindIfReady()
}
}
ngAfterViewInit(): void {
this.setupResizeObserver()
this.initViewer()
if (!this.hasLoaded) {
this.loadDocument()
return
}
if (this.pdf) {
this.applyViewerState()
}
}
ngOnDestroy(): void {
this.eventBus.off('pagerendered', this.onPageRendered)
this.eventBus.off('pagesinit', this.onPagesInit)
this.eventBus.off('pagechanging', this.onPageChanging)
this.resizeObserver?.disconnect()
this.loadingTask?.destroy()
this.pdfViewer?.cleanup()
this.pdfViewer = undefined
}
private async loadDocument(): Promise<void> {
if (this.hasLoaded) {
return
}
this.hasLoaded = true
this.hasRenderedPage = false
this.lastFindQuery = ''
this.loadingTask?.destroy()
GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
this.loadingTask = getDocument(this.src)
try {
const pdf = await this.loadingTask.promise
this.pdf = pdf
this.linkService.setDocument(pdf)
this.findController.onIsPageVisible = () => true
this.pdfViewer?.setDocument(pdf)
this.applyViewerState()
this.afterLoadComplete.emit(pdf)
} catch (err) {
this.loadError.emit(err)
}
}
private setupResizeObserver(): void {
this.resizeObserver?.disconnect()
this.resizeObserver = new ResizeObserver(() => {
this.applyScale()
})
this.resizeObserver.observe(this.container.nativeElement)
}
private initViewer(): void {
this.viewer.nativeElement.innerHTML = ''
this.pdfViewer?.cleanup()
this.hasRenderedPage = false
this.lastFindQuery = ''
const textLayerMode = this.selectable === false ? 0 : 1
const options = {
container: this.container.nativeElement,
viewer: this.viewer.nativeElement,
eventBus: this.eventBus,
linkService: this.linkService,
findController: this.findController,
textLayerMode,
removePageBorders: true,
}
this.pdfViewer =
this.renderMode === PdfRenderMode.Single
? new PDFSinglePageViewer(options)
: new PDFViewer(options)
this.linkService.setViewer(this.pdfViewer)
this.eventBus.off('pagerendered', this.onPageRendered)
this.eventBus.off('pagesinit', this.onPagesInit)
this.eventBus.off('pagechanging', this.onPageChanging)
this.eventBus.on('pagerendered', this.onPageRendered)
this.eventBus.on('pagesinit', this.onPagesInit)
this.eventBus.on('pagechanging', this.onPageChanging)
if (this.pdf) {
this.pdfViewer.setDocument(this.pdf)
this.applyViewerState()
}
}
private applyViewerState(): void {
if (!this.pdfViewer) {
return
}
const hasPages = this.pdfViewer.pagesCount > 0
if (typeof this.rotation === 'number' && hasPages) {
this.pdfViewer.pagesRotation = this.rotation
}
if (
typeof this.page === 'number' &&
hasPages &&
this.page !== this.lastViewerPage
) {
this.pdfViewer.currentPageNumber = this.page
}
if (this.page === this.lastViewerPage) {
this.lastViewerPage = undefined
}
if (hasPages) {
this.applyScale()
}
this.dispatchFindIfReady()
}
private applyScale(): void {
if (!this.pdfViewer) {
return
}
if (this.pdfViewer.pagesCount === 0) {
return
}
const zoomFactor = Number(this.zoom) || 1
this.pdfViewer.currentScaleValue = this.zoomScale
if (zoomFactor !== 1) {
this.pdfViewer.currentScale = this.pdfViewer.currentScale * zoomFactor
}
}
private dispatchFindIfReady(): void {
if (!this.hasRenderedPage) {
return
}
const query = this.searchQuery.trim()
if (query === this.lastFindQuery) {
return
}
this.lastFindQuery = query
this.eventBus.dispatch('find', {
query,
caseSensitive: false,
highlightAll: query.length > 0,
phraseSearch: true,
})
}
}

View File

@@ -0,0 +1,25 @@
export type PngxPdfDocumentProxy = {
numPages: number
}
export type PdfSource = string | { url: string; password?: string }
export enum PdfRenderMode {
Single = 'single',
All = 'all',
}
export enum PdfZoomScale {
PageFit = 'page-fit',
PageWidth = 'page-width',
}
export enum PdfZoomLevel {
Quarter = '.25',
Half = '.5',
ThreeQuarters = '.75',
One = '1',
OneAndHalf = '1.5',
Two = '2',
Three = '3',
}

View File

@@ -23,14 +23,12 @@
</div> </div>
} }
@if (!requiresPassword) { @if (!requiresPassword) {
<pdf-viewer <pngx-pdf-viewer
[src]="previewUrl" [src]="previewUrl"
[original-size]="false" [renderMode]="PdfRenderMode.All"
[show-borders]="false" [searchQuery]="documentService.searchQuery"
[show-all]="true" (loadError)="onError($event)">
(text-layer-rendered)="onPageRendered()" </pngx-pdf-viewer>
(error)="onError($event)" #pdfViewer>
</pdf-viewer>
} }
} }
} }

View File

@@ -12,6 +12,7 @@ import { of, throwError } from 'rxjs'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import { PreviewPopupComponent } from './preview-popup.component' import { PreviewPopupComponent } from './preview-popup.component'
const doc = { const doc = {
@@ -78,7 +79,7 @@ describe('PreviewPopupComponent', () => {
component.popover.open() component.popover.open()
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).toBeNull() expect(fixture.debugElement.query(By.css('object'))).toBeNull()
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull() expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
}) })
it('should show lock icon on password error', () => { it('should show lock icon on password error', () => {
@@ -159,23 +160,15 @@ describe('PreviewPopupComponent', () => {
expect(component.popover.isOpen()).toBeFalsy() expect(component.popover.isOpen()).toBeFalsy()
}) })
it('should dispatch find event on viewer loaded if searchQuery set', () => { it('should pass searchQuery to viewer', () => {
documentService.searchQuery = 'test' documentService.searchQuery = 'test'
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
component.popover.open() component.popover.open()
jest.advanceTimersByTime(1000)
fixture.detectChanges() fixture.detectChanges()
// normally setup by pdf-viewer const viewer = fixture.debugElement.query(
jest.replaceProperty(component.pdfViewer, 'eventBus', { By.directive(PngxPdfViewerComponent)
dispatch: jest.fn(), )
} as any) expect(viewer).not.toBeNull()
const dispatchSpy = jest.spyOn(component.pdfViewer.eventBus, 'dispatch') expect(viewer.componentInstance.searchQuery).toBe('test')
component.onPageRendered()
expect(dispatchSpy).toHaveBeenCalledWith('find', {
query: 'test',
caseSensitive: false,
highlightAll: true,
phraseSearch: true,
})
}) })
}) })

View File

@@ -1,7 +1,6 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core' import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, Subject, takeUntil } from 'rxjs' import { first, Subject, takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
@@ -10,6 +9,8 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import { PdfRenderMode } from '../pdf-viewer/pdf-viewer.types'
@Component({ @Component({
selector: 'pngx-preview-popup', selector: 'pngx-preview-popup',
@@ -18,14 +19,15 @@ import { SettingsService } from 'src/app/services/settings.service'
imports: [ imports: [
NgbPopoverModule, NgbPopoverModule,
DocumentTitlePipe, DocumentTitlePipe,
PdfViewerModule, PngxPdfViewerComponent,
SafeUrlPipe, SafeUrlPipe,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
], ],
}) })
export class PreviewPopupComponent implements OnDestroy { export class PreviewPopupComponent implements OnDestroy {
PdfRenderMode = PdfRenderMode
private settingsService = inject(SettingsService) private settingsService = inject(SettingsService)
private documentService = inject(DocumentService) public readonly documentService = inject(DocumentService)
private http = inject(HttpClient) private http = inject(HttpClient)
private _document: Document private _document: Document
@@ -61,8 +63,6 @@ export class PreviewPopupComponent implements OnDestroy {
@ViewChild('popover') popover: NgbPopover @ViewChild('popover') popover: NgbPopover
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
mouseOnPreview: boolean = false mouseOnPreview: boolean = false
popoverClass: string = 'shadow popover-preview' popoverClass: string = 'shadow popover-preview'
@@ -114,18 +114,6 @@ export class PreviewPopupComponent implements OnDestroy {
} }
} }
onPageRendered() {
// Only triggered by the pngx pdf viewer
if (this.documentService.searchQuery) {
this.pdfViewer.eventBus.dispatch('find', {
query: this.documentService.searchQuery,
caseSensitive: false,
highlightAll: true,
phraseSearch: true,
})
}
}
mouseEnterPreview() { mouseEnterPreview() {
this.mouseOnPreview = true this.mouseOnPreview = true
if (!this.popover.isOpen()) { if (!this.popover.isOpen()) {

View File

@@ -456,17 +456,15 @@
@case (ContentRenderType.PDF) { @case (ContentRenderType.PDF) {
@if (!useNativePdfViewer) { @if (!useNativePdfViewer) {
<div class="preview-sticky pdf-viewer-container"> <div class="preview-sticky pdf-viewer-container">
<pdf-viewer <pngx-pdf-viewer
[src]="{ url: previewUrl, password: password }" [src]="{ url: previewUrl, password: password }"
[original-size]="false" [renderMode]="PdfRenderMode.All"
[show-borders]="true"
[show-all]="true"
[(page)]="previewCurrentPage" [(page)]="previewCurrentPage"
[zoom-scale]="previewZoomScale" [zoomScale]="previewZoomScale"
[zoom]="previewZoomSetting" [zoom]="previewZoomSetting"
(error)="onError($event)" (loadError)="onError($event)"
(after-load-complete)="pdfPreviewLoaded($event)"> (afterLoadComplete)="pdfPreviewLoaded($event)">
</pdf-viewer> </pngx-pdf-viewer>
</div> </div>
} @else { } @else {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>

View File

@@ -5,20 +5,15 @@
} }
.pdf-viewer-container { .pdf-viewer-container {
padding-top: 10px; padding: 8px;
background-color: gray; background-color: gray;
pdf-viewer { pngx-pdf-viewer {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
::ng-deep .ng2-pdf-viewer-container .page {
--page-margin: 0 auto 10px;
--page-border: 0;
}
.btn-group .dropdown-toggle-split { .btn-group .dropdown-toggle-split {
border-top-right-radius: inherit; border-top-right-radius: inherit;
border-bottom-right-radius: inherit; border-bottom-right-radius: inherit;

View File

@@ -69,8 +69,11 @@ import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import {
PdfZoomLevel,
PdfZoomScale,
} from '../common/pdf-viewer/pdf-viewer.types'
import { DocumentDetailComponent } from './document-detail.component' import { DocumentDetailComponent } from './document-detail.component'
import { ZoomSetting } from './zoom-setting'
const doc: Document = { const doc: Document = {
id: 3, id: 3,
@@ -860,7 +863,7 @@ describe('DocumentDetailComponent', () => {
it('should support zoom controls', () => { it('should support zoom controls', () => {
initNormally() initNormally()
component.setZoom(ZoomSetting.One) // from select component.setZoom(PdfZoomLevel.One) // from select
expect(component.previewZoomSetting).toEqual('1') expect(component.previewZoomSetting).toEqual('1')
component.increaseZoom() component.increaseZoom()
expect(component.previewZoomSetting).toEqual('1.5') expect(component.previewZoomSetting).toEqual('1.5')
@@ -868,18 +871,18 @@ describe('DocumentDetailComponent', () => {
expect(component.previewZoomSetting).toEqual('2') expect(component.previewZoomSetting).toEqual('2')
component.decreaseZoom() component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('1.5') expect(component.previewZoomSetting).toEqual('1.5')
component.setZoom(ZoomSetting.One) // from select component.setZoom(PdfZoomLevel.One) // from select
component.decreaseZoom() component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('.75') expect(component.previewZoomSetting).toEqual('.75')
component.setZoom(ZoomSetting.PageFit) // from select component.setZoom(PdfZoomScale.PageFit) // from select
expect(component.previewZoomScale).toEqual('page-fit') expect(component.previewZoomScale).toEqual('page-fit')
expect(component.previewZoomSetting).toEqual('1') expect(component.previewZoomSetting).toEqual('1')
component.increaseZoom() component.increaseZoom()
expect(component.previewZoomSetting).toEqual('1.5') expect(component.previewZoomSetting).toEqual('1.5')
expect(component.previewZoomScale).toEqual('page-width') expect(component.previewZoomScale).toEqual('page-width')
component.setZoom(ZoomSetting.PageFit) // from select component.setZoom(PdfZoomScale.PageFit) // from select
expect(component.previewZoomScale).toEqual('page-fit') expect(component.previewZoomScale).toEqual('page-fit')
expect(component.previewZoomSetting).toEqual('1') expect(component.previewZoomSetting).toEqual('1')
component.decreaseZoom() component.decreaseZoom()
@@ -889,10 +892,10 @@ describe('DocumentDetailComponent', () => {
it('should select correct zoom setting in dropdown', () => { it('should select correct zoom setting in dropdown', () => {
initNormally() initNormally()
component.setZoom(ZoomSetting.PageFit) component.setZoom(PdfZoomScale.PageFit)
expect(component.currentZoom).toEqual(ZoomSetting.PageFit) expect(component.currentZoom).toEqual(PdfZoomScale.PageFit)
component.setZoom(ZoomSetting.Quarter) component.setZoom(PdfZoomLevel.Quarter)
expect(component.currentZoom).toEqual(ZoomSetting.Quarter) expect(component.currentZoom).toEqual(PdfZoomLevel.Quarter)
}) })
it('should support updating notes dynamically', () => { it('should support updating notes dynamically', () => {
@@ -1017,7 +1020,7 @@ describe('DocumentDetailComponent', () => {
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
expect(component.useNativePdfViewer).toBeFalsy() expect(component.useNativePdfViewer).toBeFalsy()
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull() expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
}) })
it('should display native pdf viewer if enabled', () => { it('should display native pdf viewer if enabled', () => {

View File

@@ -18,7 +18,6 @@ import {
NgbNavModule, NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector' import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs' import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
@@ -108,13 +107,19 @@ import { UrlComponent } from '../common/input/url/url.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component' import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode' import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component' import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
import { PngxPdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
import {
PdfRenderMode,
PdfZoomLevel,
PdfZoomScale,
PngxPdfDocumentProxy,
} from '../common/pdf-viewer/pdf-viewer.types'
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component' import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component' import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
import { DocumentHistoryComponent } from '../document-history/document-history.component' import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component' import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component' import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
import { ZoomSetting } from './zoom-setting'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@@ -168,16 +173,17 @@ enum ContentRenderType {
NgbNavModule, NgbNavModule,
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
PdfViewerModule,
TextAreaComponent, TextAreaComponent,
RouterModule, RouterModule,
PngxPdfViewerComponent,
], ],
}) })
export class DocumentDetailComponent export class DocumentDetailComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent implements OnInit, OnDestroy, DirtyComponent
{ {
private documentsService = inject(DocumentService) PdfRenderMode = PdfRenderMode
documentsService = inject(DocumentService)
private route = inject(ActivatedRoute) private route = inject(ActivatedRoute)
private tagService = inject(TagService) private tagService = inject(TagService)
private correspondentService = inject(CorrespondentService) private correspondentService = inject(CorrespondentService)
@@ -246,8 +252,8 @@ export class DocumentDetailComponent
previewCurrentPage: number = 1 previewCurrentPage: number = 1
previewNumPages: number previewNumPages: number
previewZoomSetting: ZoomSetting = ZoomSetting.One previewZoomSetting: PdfZoomLevel = PdfZoomLevel.One
previewZoomScale: ZoomSetting = ZoomSetting.PageWidth previewZoomScale: PdfZoomScale = PdfZoomScale.PageWidth
store: BehaviorSubject<any> store: BehaviorSubject<any>
isDirty$: Observable<boolean> isDirty$: Observable<boolean>
@@ -503,7 +509,9 @@ export class DocumentDetailComponent
} }
ngOnInit(): void { ngOnInit(): void {
this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING)) this.setZoom(
this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
)
this.documentForm.valueChanges this.documentForm.valueChanges
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((values) => { .subscribe((values) => {
@@ -1204,7 +1212,7 @@ export class DocumentDetailComponent
}) })
} }
pdfPreviewLoaded(pdf: PDFDocumentProxy) { pdfPreviewLoaded(pdf: PngxPdfDocumentProxy) {
this.previewNumPages = pdf.numPages this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false if (this.password) this.requiresPassword = false
setTimeout(() => { setTimeout(() => {
@@ -1225,31 +1233,33 @@ export class DocumentDetailComponent
} }
} }
setZoom(setting: ZoomSetting) { setZoom(setting: PdfZoomScale | PdfZoomLevel) {
if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) { if (
setting === PdfZoomScale.PageFit ||
setting === PdfZoomScale.PageWidth
) {
this.previewZoomScale = setting this.previewZoomScale = setting
this.previewZoomSetting = ZoomSetting.One this.previewZoomSetting = PdfZoomLevel.One
} else { return
this.previewZoomSetting = setting
this.previewZoomScale = ZoomSetting.PageWidth
} }
this.previewZoomSetting = setting
this.previewZoomScale = PdfZoomScale.PageWidth
} }
get zoomSettings() { get zoomSettings() {
return Object.values(ZoomSetting).filter( return [PdfZoomScale.PageFit, ...Object.values(PdfZoomLevel)]
(setting) => setting !== ZoomSetting.PageWidth
)
} }
get currentZoom() { get currentZoom() {
if (this.previewZoomScale === ZoomSetting.PageFit) { if (this.previewZoomScale === PdfZoomScale.PageFit) {
return ZoomSetting.PageFit return PdfZoomScale.PageFit
} else return this.previewZoomSetting }
return this.previewZoomSetting
} }
getZoomSettingTitle(setting: ZoomSetting): string { getZoomSettingTitle(setting: PdfZoomScale | PdfZoomLevel): string {
switch (setting) { switch (setting) {
case ZoomSetting.PageFit: case PdfZoomScale.PageFit:
return $localize`Page Fit` return $localize`Page Fit`
default: default:
return `${parseFloat(setting) * 100}%` return `${parseFloat(setting) * 100}%`
@@ -1257,25 +1267,24 @@ export class DocumentDetailComponent
} }
increaseZoom(): void { increaseZoom(): void {
let currentIndex = Object.values(ZoomSetting).indexOf( const zoomLevels = Object.values(PdfZoomLevel)
this.previewZoomSetting let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
) if (this.previewZoomScale === PdfZoomScale.PageFit) {
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 5 currentIndex = zoomLevels.indexOf(PdfZoomLevel.One)
this.previewZoomScale = ZoomSetting.PageWidth }
this.previewZoomScale = PdfZoomScale.PageWidth
this.previewZoomSetting = this.previewZoomSetting =
Object.values(ZoomSetting)[ zoomLevels[Math.min(zoomLevels.length - 1, currentIndex + 1)]
Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
]
} }
decreaseZoom(): void { decreaseZoom(): void {
let currentIndex = Object.values(ZoomSetting).indexOf( const zoomLevels = Object.values(PdfZoomLevel)
this.previewZoomSetting let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
) if (this.previewZoomScale === PdfZoomScale.PageFit) {
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 4 currentIndex = zoomLevels.indexOf(PdfZoomLevel.ThreeQuarters)
this.previewZoomScale = ZoomSetting.PageWidth }
this.previewZoomSetting = this.previewZoomScale = PdfZoomScale.PageWidth
Object.values(ZoomSetting)[Math.max(2, currentIndex - 1)] this.previewZoomSetting = zoomLevels[Math.max(0, currentIndex - 1)]
} }
get showPermissions(): boolean { get showPermissions(): boolean {

View File

@@ -1,11 +0,0 @@
export enum ZoomSetting {
PageFit = 'page-fit',
PageWidth = 'page-width',
Quarter = '.25',
Half = '.5',
ThreeQuarters = '.75',
One = '1',
OneAndHalf = '1.5',
Two = '2',
Three = '3',
}

View File

@@ -1,5 +1,5 @@
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode' import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
import { ZoomSetting } from '../components/document-detail/zoom-setting' import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
import { User } from './user' import { User } from './user'
export interface UiSettings { export interface UiSettings {
@@ -310,7 +310,7 @@ export const SETTINGS: UiSetting[] = [
{ {
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING, key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
type: 'string', type: 'string',
default: ZoomSetting.PageWidth, default: PdfZoomScale.PageWidth,
}, },
{ {
key: SETTINGS_KEYS.AI_ENABLED, key: SETTINGS_KEYS.AI_ENABLED,

View File

@@ -70,4 +70,26 @@ describe('LocalizedDateParserFormatter', () => {
dateStr = dateParserFormatter.format(dateStruct) dateStr = dateParserFormatter.format(dateStruct)
expect(dateStr).toEqual('04.05.2023') expect(dateStr).toEqual('04.05.2023')
}) })
it('should handle years when current year % 100 < 50', () => {
jest.useFakeTimers()
jest.setSystemTime(new Date(2026, 5, 15))
let val = dateParserFormatter.parse('5/4/26')
expect(val).toEqual({ day: 4, month: 5, year: 2026 })
val = dateParserFormatter.parse('5/4/75')
expect(val).toEqual({ day: 4, month: 5, year: 2075 })
val = dateParserFormatter.parse('5/4/99')
expect(val).toEqual({ day: 4, month: 5, year: 1999 })
jest.useRealTimers()
})
it('should handle years when current year % 100 >= 50', () => {
jest.useFakeTimers()
jest.setSystemTime(new Date(2076, 5, 15))
const val = dateParserFormatter.parse('5/4/00')
expect(val).toEqual({ day: 4, month: 5, year: 2100 })
jest.useRealTimers()
})
}) })

View File

@@ -106,15 +106,25 @@ export class LocalizedDateParserFormatter extends NgbDateParserFormatter {
value = this.preformatDateInput(value) value = this.preformatDateInput(value)
let match = this.getDateParseRegex().exec(value) let match = this.getDateParseRegex().exec(value)
if (match) { if (match) {
const currentYear = new Date().getFullYear()
const currentCentury = currentYear - (currentYear % 100)
let year = +match.groups.year
if (year < 100) {
let fourDigitYear = currentCentury + year
// Mimic python-dateutil: keep result within -50/+49 years of current year
if (fourDigitYear > currentYear + 49) {
fourDigitYear -= 100
} else if (fourDigitYear <= currentYear - 50) {
fourDigitYear += 100
}
year = fourDigitYear
}
let dateStruct = { let dateStruct = {
day: +match.groups.day, day: +match.groups.day,
month: +match.groups.month, month: +match.groups.month,
year: +match.groups.year, year,
}
if (dateStruct.year <= new Date().getFullYear() - 2000) {
dateStruct.year += 2000
} else if (dateStruct.year < 100) {
dateStruct.year += 1900
} }
return dateStruct return dateStruct
} else { } else {

View File

@@ -21,7 +21,6 @@ import {
NgbModule, NgbModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { import {
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
airplane, airplane,
@@ -371,7 +370,6 @@ bootstrapApplication(AppComponent, {
NgbModule, NgbModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
PdfViewerModule,
NgSelectModule, NgSelectModule,
ColorSliderModule, ColorSliderModule,
DragDropModule, DragDropModule,

View File

@@ -0,0 +1,24 @@
export class PDFDocumentProxy {
numPages = 1
}
export class PDFDocumentLoadingTask {
promise: Promise<PDFDocumentProxy>
destroyed = false
constructor(promise: Promise<PDFDocumentProxy>) {
this.promise = promise
}
destroy(): void {
this.destroyed = true
}
}
export const GlobalWorkerOptions = {
workerSrc: '',
}
export const getDocument = (_src: unknown): PDFDocumentLoadingTask => {
return new PDFDocumentLoadingTask(Promise.resolve(new PDFDocumentProxy()))
}

View File

@@ -0,0 +1,79 @@
type EventHandler = (event?: unknown) => void
export class EventBus {
private readonly listeners = new Map<string, Set<EventHandler>>()
on(eventName: string, listener: EventHandler): void {
let listeners = this.listeners.get(eventName)
if (!listeners) {
listeners = new Set()
this.listeners.set(eventName, listeners)
}
listeners.add(listener)
}
off(eventName: string, listener: EventHandler): void {
this.listeners.get(eventName)?.delete(listener)
}
dispatch(eventName: string, event?: unknown): void {
this.listeners.get(eventName)?.forEach((listener) => listener(event))
}
}
export class PDFFindController {
onIsPageVisible?: () => boolean
}
export class PDFLinkService {
private document?: unknown
private viewer?: unknown
setDocument(document: unknown): void {
this.document = document
}
setViewer(viewer: unknown): void {
this.viewer = viewer
}
}
class BaseViewer {
pagesCount = 0
currentScale = 1
currentScaleValue: string | number = 1
pagesRotation = 0
readonly options: Record<string, unknown>
private readonly eventBus?: EventBus
private _currentPageNumber = 1
constructor(options: { eventBus?: EventBus }) {
this.options = options
this.eventBus = options.eventBus
}
setDocument(document: { numPages?: number } | null | undefined): void {
this.pagesCount = document?.numPages ?? 1
this.eventBus?.dispatch('pagesinit', {})
this.eventBus?.dispatch('pagerendered', {
pageNumber: this._currentPageNumber,
})
}
cleanup(): void {
this.pagesCount = 0
}
get currentPageNumber(): number {
return this._currentPageNumber
}
set currentPageNumber(value: number) {
this._currentPageNumber = value
this.eventBus?.dispatch('pagechanging', { pageNumber: value })
}
}
export class PDFViewer extends BaseViewer {}
export class PDFSinglePageViewer extends BaseViewer {}

View File

@@ -17,6 +17,7 @@
], ],
"include": [ "include": [
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.d.ts" "src/**/*.d.ts",
"src/test/**/*.ts"
] ]
} }

View File

@@ -1,10 +1,14 @@
import logging import logging
import multiprocessing import multiprocessing
import tqdm
from django import db from django import db
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.management.commands.mixins import MultiProcessMixin from documents.management.commands.mixins import MultiProcessMixin
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
@@ -75,20 +79,24 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
try: try:
logging.getLogger().handlers[0].level = logging.ERROR logging.getLogger().handlers[0].level = logging.ERROR
if self.process_count == 1: with Progress(
for doc_id in document_ids: TextColumn("[progress.description]{task.description}"),
update_document_content_maybe_archive_file(doc_id) BarColumn(),
else: # pragma: no cover TaskProgressColumn(),
with multiprocessing.Pool(self.process_count) as pool: TimeRemainingColumn(),
list( disable=self.no_progress_bar,
tqdm.tqdm( ) as progress:
pool.imap_unordered( task = progress.add_task("Archiving documents", total=len(document_ids))
update_document_content_maybe_archive_file, if self.process_count == 1:
document_ids, for doc_id in document_ids:
), update_document_content_maybe_archive_file(doc_id)
total=len(document_ids), progress.update(task, advance=1)
disable=self.no_progress_bar, else: # pragma: no cover
), with multiprocessing.Pool(self.process_count) as pool:
) for _ in pool.imap_unordered(
update_document_content_maybe_archive_file,
document_ids,
):
progress.update(task, advance=1)
except KeyboardInterrupt: except KeyboardInterrupt:
self.stdout.write(self.style.NOTICE("Aborting...")) self.stdout.write(self.style.NOTICE("Aborting..."))

View File

@@ -6,7 +6,6 @@ import tempfile
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import tqdm
from allauth.mfa.models import Authenticator from allauth.mfa.models import Authenticator
from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.models import SocialApp from allauth.socialaccount.models import SocialApp
@@ -24,6 +23,11 @@ from django.utils import timezone
from filelock import FileLock from filelock import FileLock
from guardian.models import GroupObjectPermission from guardian.models import GroupObjectPermission
from guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models import QuerySet from django.db.models import QuerySet
@@ -309,54 +313,64 @@ class Command(CryptMixin, BaseCommand):
document_manifest = manifest_dict["documents"] document_manifest = manifest_dict["documents"]
# 3. Export files from each document # 3. Export files from each document
for index, document_dict in tqdm.tqdm( with Progress(
enumerate(document_manifest), TextColumn("[progress.description]{task.description}"),
total=len(document_manifest), BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=self.no_progress_bar, disable=self.no_progress_bar,
): ) as progress:
document = document_map[document_dict["pk"]] task = progress.add_task(
"Exporting documents",
# 3.1. generate a unique filename total=len(document_manifest),
base_name = self.generate_base_name(document)
# 3.2. write filenames into manifest
original_target, thumbnail_target, archive_target = (
self.generate_document_targets(document, base_name, document_dict)
) )
for index, document_dict in enumerate(document_manifest):
document = document_map[document_dict["pk"]]
# 3.3. write files to target folder # 3.1. generate a unique filename
if not self.data_only: base_name = self.generate_base_name(document)
self.copy_document_files(
document, # 3.2. write filenames into manifest
original_target, original_target, thumbnail_target, archive_target = (
thumbnail_target, self.generate_document_targets(document, base_name, document_dict)
archive_target,
) )
if self.split_manifest: # 3.3. write files to target folder
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json") if not self.data_only:
if self.use_folder_prefix: self.copy_document_files(
manifest_name = Path("json") / manifest_name document,
manifest_name = (self.target / manifest_name).resolve() original_target,
manifest_name.parent.mkdir(parents=True, exist_ok=True) thumbnail_target,
content = [document_manifest[index]] archive_target,
content += list( )
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
manifest_dict["notes"],
),
)
content += list(
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
manifest_dict["custom_field_instances"],
),
)
self.check_and_write_json( if self.split_manifest:
content, manifest_name = base_name.with_name(
manifest_name, f"{base_name.stem}-manifest.json",
) )
if self.use_folder_prefix:
manifest_name = Path("json") / manifest_name
manifest_name = (self.target / manifest_name).resolve()
manifest_name.parent.mkdir(parents=True, exist_ok=True)
content = [document_manifest[index]]
content += list(
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
manifest_dict["notes"],
),
)
content += list(
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
manifest_dict["custom_field_instances"],
),
)
self.check_and_write_json(
content,
manifest_name,
)
progress.update(task, advance=1)
# These were exported already # These were exported already
if self.split_manifest: if self.split_manifest:

View File

@@ -3,9 +3,13 @@ import multiprocessing
from typing import Final from typing import Final
import rapidfuzz import rapidfuzz
import tqdm
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.core.management import CommandError from django.core.management import CommandError
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.management.commands.mixins import MultiProcessMixin from documents.management.commands.mixins import MultiProcessMixin
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
@@ -106,19 +110,25 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
work_pkgs.append(_WorkPackage(first_doc, second_doc)) work_pkgs.append(_WorkPackage(first_doc, second_doc))
# Don't spin up a pool of 1 process # Don't spin up a pool of 1 process
if self.process_count == 1: with Progress(
results = [] TextColumn("[progress.description]{task.description}"),
for work in tqdm.tqdm(work_pkgs, disable=self.no_progress_bar): BarColumn(),
results.append(_process_and_match(work)) TaskProgressColumn(),
else: # pragma: no cover TimeRemainingColumn(),
with multiprocessing.Pool(processes=self.process_count) as pool: disable=self.no_progress_bar,
results = list( ) as progress:
tqdm.tqdm( task = progress.add_task("Fuzzy matching documents", total=len(work_pkgs))
pool.imap_unordered(_process_and_match, work_pkgs), if self.process_count == 1:
total=len(work_pkgs), results = []
disable=self.no_progress_bar, for work in work_pkgs:
), results.append(_process_and_match(work))
) progress.update(task, advance=1)
else: # pragma: no cover
with multiprocessing.Pool(processes=self.process_count) as pool:
results = []
for result in pool.imap_unordered(_process_and_match, work_pkgs):
results.append(result)
progress.update(task, advance=1)
# Check results # Check results
messages = [] messages = []

View File

@@ -5,10 +5,10 @@ import tempfile
from collections.abc import Generator from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from zipfile import ZipFile from zipfile import ZipFile
from zipfile import is_zipfile from zipfile import is_zipfile
import tqdm
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -23,6 +23,11 @@ from django.db import transaction
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save from django.db.models.signals import post_save
from filelock import FileLock from filelock import FileLock
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.file_handling import create_source_path_directory from documents.file_handling import create_source_path_directory
from documents.management.commands.mixins import CryptMixin from documents.management.commands.mixins import CryptMixin
@@ -365,58 +370,72 @@ class Command(CryptMixin, BaseCommand):
filter(lambda r: r["model"] == "documents.document", self.manifest), filter(lambda r: r["model"] == "documents.document", self.manifest),
) )
for record in tqdm.tqdm(manifest_documents, disable=self.no_progress_bar): with Progress(
document = Document.objects.get(pk=record["pk"]) TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=self.no_progress_bar,
) as progress:
task = progress.add_task(
"Importing documents",
total=len(manifest_documents),
)
for record in manifest_documents:
document = Document.objects.get(pk=record["pk"])
doc_file = record[EXPORTER_FILE_NAME] doc_file = record[EXPORTER_FILE_NAME]
document_path = self.source / doc_file document_path = self.source / doc_file
if EXPORTER_THUMBNAIL_NAME in record: if EXPORTER_THUMBNAIL_NAME in record:
thumb_file = record[EXPORTER_THUMBNAIL_NAME] thumb_file = record[EXPORTER_THUMBNAIL_NAME]
thumbnail_path = (self.source / thumb_file).resolve() thumbnail_path = (self.source / thumb_file).resolve()
else: else:
thumbnail_path = None thumbnail_path = None
if EXPORTER_ARCHIVE_NAME in record: if EXPORTER_ARCHIVE_NAME in record:
archive_file = record[EXPORTER_ARCHIVE_NAME] archive_file = record[EXPORTER_ARCHIVE_NAME]
archive_path = self.source / archive_file archive_path = self.source / archive_file
else: else:
archive_path = None archive_path = None
with FileLock(settings.MEDIA_LOCK): with FileLock(settings.MEDIA_LOCK):
if Path(document.source_path).is_file(): if Path(document.source_path).is_file():
raise FileExistsError(document.source_path) raise FileExistsError(document.source_path)
create_source_path_directory(document.source_path) create_source_path_directory(document.source_path)
copy_file_with_basic_stats(document_path, document.source_path) copy_file_with_basic_stats(document_path, document.source_path)
if thumbnail_path: if thumbnail_path:
if thumbnail_path.suffix in {".png", ".PNG"}: if thumbnail_path.suffix in {".png", ".PNG"}:
run_convert( run_convert(
density=300, density=300,
scale="500x5000>", scale="500x5000>",
alpha="remove", alpha="remove",
strip=True, strip=True,
trim=False, trim=False,
auto_orient=True, auto_orient=True,
input_file=f"{thumbnail_path}[0]", input_file=f"{thumbnail_path}[0]",
output_file=str(document.thumbnail_path), output_file=str(document.thumbnail_path),
) )
else: else:
copy_file_with_basic_stats( copy_file_with_basic_stats(
thumbnail_path, thumbnail_path,
document.thumbnail_path, document.thumbnail_path,
) )
if archive_path: if archive_path:
create_source_path_directory(document.archive_path) if TYPE_CHECKING:
# TODO: this assumes that the export is valid and assert document.archive_path is not None
# archive_filename is present on all documents with create_source_path_directory(document.archive_path)
# archived files # TODO: this assumes that the export is valid and
copy_file_with_basic_stats(archive_path, document.archive_path) # archive_filename is present on all documents with
# archived files
copy_file_with_basic_stats(archive_path, document.archive_path)
document.save() document.save()
progress.update(task, advance=1)
def decrypt_secret_fields(self) -> None: def decrypt_secret_fields(self) -> None:
""" """

View File

@@ -1,8 +1,12 @@
import logging import logging
import tqdm
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models.signals import post_save from django.db.models.signals import post_save
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
from documents.models import Document from documents.models import Document
@@ -18,8 +22,15 @@ class Command(ProgressBarMixin, BaseCommand):
self.handle_progress_bar_mixin(**options) self.handle_progress_bar_mixin(**options)
logging.getLogger().handlers[0].level = logging.ERROR logging.getLogger().handlers[0].level = logging.ERROR
for document in tqdm.tqdm( documents = Document.objects.all()
Document.objects.all(), with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=self.no_progress_bar, disable=self.no_progress_bar,
): ) as progress:
post_save.send(Document, instance=document, created=False) task = progress.add_task("Renaming documents", total=documents.count())
for document in documents:
post_save.send(Document, instance=document, created=False)
progress.update(task, advance=1)

View File

@@ -1,7 +1,11 @@
import logging import logging
import tqdm
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.classifier import load_classifier from documents.classifier import load_classifier
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
@@ -84,53 +88,62 @@ class Command(ProgressBarMixin, BaseCommand):
classifier = load_classifier() classifier = load_classifier()
for document in tqdm.tqdm(documents, disable=self.no_progress_bar): with Progress(
if options["correspondent"]: TextColumn("[progress.description]{task.description}"),
set_correspondent( BarColumn(),
sender=None, TaskProgressColumn(),
document=document, TimeRemainingColumn(),
classifier=classifier, disable=self.no_progress_bar,
replace=options["overwrite"], ) as progress:
use_first=options["use_first"], task = progress.add_task("Retagging documents", total=documents.count())
suggest=options["suggest"], for document in documents:
base_url=options["base_url"], if options["correspondent"]:
stdout=self.stdout, set_correspondent(
style_func=self.style, sender=None,
) document=document,
classifier=classifier,
replace=options["overwrite"],
use_first=options["use_first"],
suggest=options["suggest"],
base_url=options["base_url"],
stdout=self.stdout,
style_func=self.style,
)
if options["document_type"]: if options["document_type"]:
set_document_type( set_document_type(
sender=None, sender=None,
document=document, document=document,
classifier=classifier, classifier=classifier,
replace=options["overwrite"], replace=options["overwrite"],
use_first=options["use_first"], use_first=options["use_first"],
suggest=options["suggest"], suggest=options["suggest"],
base_url=options["base_url"], base_url=options["base_url"],
stdout=self.stdout, stdout=self.stdout,
style_func=self.style, style_func=self.style,
) )
if options["tags"]: if options["tags"]:
set_tags( set_tags(
sender=None, sender=None,
document=document, document=document,
classifier=classifier, classifier=classifier,
replace=options["overwrite"], replace=options["overwrite"],
suggest=options["suggest"], suggest=options["suggest"],
base_url=options["base_url"], base_url=options["base_url"],
stdout=self.stdout, stdout=self.stdout,
style_func=self.style, style_func=self.style,
) )
if options["storage_path"]: if options["storage_path"]:
set_storage_path( set_storage_path(
sender=None, sender=None,
document=document, document=document,
classifier=classifier, classifier=classifier,
replace=options["overwrite"], replace=options["overwrite"],
use_first=options["use_first"], use_first=options["use_first"],
suggest=options["suggest"], suggest=options["suggest"],
base_url=options["base_url"], base_url=options["base_url"],
stdout=self.stdout, stdout=self.stdout,
style_func=self.style, style_func=self.style,
) )
progress.update(task, advance=1)

View File

@@ -2,9 +2,13 @@ import logging
import multiprocessing import multiprocessing
import shutil import shutil
import tqdm
from django import db from django import db
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.management.commands.mixins import MultiProcessMixin from documents.management.commands.mixins import MultiProcessMixin
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
@@ -70,15 +74,19 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
# with postgres. # with postgres.
db.connections.close_all() db.connections.close_all()
if self.process_count == 1: with Progress(
for doc_id in ids: TextColumn("[progress.description]{task.description}"),
_process_document(doc_id) BarColumn(),
else: # pragma: no cover TaskProgressColumn(),
with multiprocessing.Pool(processes=self.process_count) as pool: TimeRemainingColumn(),
list( disable=self.no_progress_bar,
tqdm.tqdm( ) as progress:
pool.imap_unordered(_process_document, ids), task = progress.add_task("Generating thumbnails", total=len(ids))
total=len(ids), if self.process_count == 1:
disable=self.no_progress_bar, for doc_id in ids:
), _process_document(doc_id)
) progress.update(task, advance=1)
else: # pragma: no cover
with multiprocessing.Pool(processes=self.process_count) as pool:
for _ in pool.imap_unordered(_process_document, ids):
progress.update(task, advance=1)

View File

@@ -1,7 +1,12 @@
from auditlog.models import LogEntry from auditlog.models import LogEntry
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from tqdm import tqdm from rich.console import Console
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.management.commands.mixins import ProgressBarMixin from documents.management.commands.mixins import ProgressBarMixin
@@ -18,22 +23,37 @@ class Command(BaseCommand, ProgressBarMixin):
def handle(self, **options): def handle(self, **options):
self.handle_progress_bar_mixin(**options) self.handle_progress_bar_mixin(**options)
console = Console()
with transaction.atomic(): with transaction.atomic():
for log_entry in tqdm(LogEntry.objects.all(), disable=self.no_progress_bar): log_entries = LogEntry.objects.all()
model_class = log_entry.content_type.model_class() with Progress(
# use global_objects for SoftDeleteModel TextColumn("[progress.description]{task.description}"),
objects = ( BarColumn(),
model_class.global_objects TaskProgressColumn(),
if hasattr(model_class, "global_objects") TimeRemainingColumn(),
else model_class.objects console=console,
disable=self.no_progress_bar,
) as progress:
task = progress.add_task(
"Pruning audit logs",
total=log_entries.count(),
) )
if ( for log_entry in log_entries:
log_entry.object_id model_class = log_entry.content_type.model_class()
and not objects.filter(pk=log_entry.object_id).exists() # use global_objects for SoftDeleteModel
): objects = (
log_entry.delete() model_class.global_objects
tqdm.write( if hasattr(model_class, "global_objects")
self.style.NOTICE( else model_class.objects
f"Deleted audit log entry for {model_class.__name__} #{log_entry.object_id}",
),
) )
if (
log_entry.object_id
and not objects.filter(pk=log_entry.object_id).exists()
):
log_entry.delete()
console.print(
self.style.NOTICE(
f"Deleted audit log entry for {model_class.__name__} #{log_entry.object_id}",
),
)
progress.update(task, advance=1)

View File

@@ -150,7 +150,15 @@ def run_convert(
) -> None: ) -> None:
environment = os.environ.copy() environment = os.environ.copy()
if settings.CONVERT_MEMORY_LIMIT: if settings.CONVERT_MEMORY_LIMIT:
# MAGICK_MEMORY_LIMIT sets the maximum amount of RAM the pixel cache can use.
# MAGICK_MAP_LIMIT sets the maximum amount of memory-mapped I/O allowed.
#
# For large-format documents ImageMagick will hit the RAM limit and
# immediately try to "map" the remaining data. If MAGICK_MAP_LIMIT isn't
# also set, the process may trigger an OOM kill because the default
# system/policy map limit is often too restrictive for these massive bitmaps.
environment["MAGICK_MEMORY_LIMIT"] = settings.CONVERT_MEMORY_LIMIT environment["MAGICK_MEMORY_LIMIT"] = settings.CONVERT_MEMORY_LIMIT
environment["MAGICK_MAP_LIMIT"] = settings.CONVERT_MEMORY_LIMIT
if settings.CONVERT_TMPDIR: if settings.CONVERT_TMPDIR:
environment["MAGICK_TMPDIR"] = settings.CONVERT_TMPDIR environment["MAGICK_TMPDIR"] = settings.CONVERT_TMPDIR

View File

@@ -8,7 +8,11 @@ from typing import Final
from celery import states from celery import states
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from tqdm import tqdm from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.models import Document from documents.models import Document
from documents.models import PaperlessTask from documents.models import PaperlessTask
@@ -92,76 +96,99 @@ def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
if logo_file in present_files: if logo_file in present_files:
present_files.remove(logo_file) present_files.remove(logo_file)
for doc in tqdm(Document.global_objects.all(), disable=not progress): documents = Document.global_objects.all()
# Check sanity of the thumbnail with Progress(
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve() TextColumn("[progress.description]{task.description}"),
if not thumbnail_path.exists() or not thumbnail_path.is_file(): BarColumn(),
messages.error(doc.pk, "Thumbnail of document does not exist.") TaskProgressColumn(),
else: TimeRemainingColumn(),
if thumbnail_path in present_files: disable=not progress,
present_files.remove(thumbnail_path) ) as progress_bar:
try: task = progress_bar.add_task(
_ = thumbnail_path.read_bytes() "Checking document sanity",
except OSError as e: total=documents.count(),
messages.error(doc.pk, f"Cannot read thumbnail file of document: {e}") )
for doc in documents:
# Check sanity of the original file # Check sanity of the thumbnail
# TODO: extract method thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()
source_path: Final[Path] = Path(doc.source_path).resolve() if not thumbnail_path.exists() or not thumbnail_path.is_file():
if not source_path.exists() or not source_path.is_file(): messages.error(doc.pk, "Thumbnail of document does not exist.")
messages.error(doc.pk, "Original of document does not exist.")
else:
if source_path in present_files:
present_files.remove(source_path)
try:
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
except OSError as e:
messages.error(doc.pk, f"Cannot read original file of document: {e}")
else: else:
if checksum != doc.checksum: if thumbnail_path in present_files:
messages.error( present_files.remove(thumbnail_path)
doc.pk,
"Checksum mismatch. "
f"Stored: {doc.checksum}, actual: {checksum}.",
)
# Check sanity of the archive file.
if doc.archive_checksum is not None and doc.archive_filename is None:
messages.error(
doc.pk,
"Document has an archive file checksum, but no archive filename.",
)
elif doc.archive_checksum is None and doc.archive_filename is not None:
messages.error(
doc.pk,
"Document has an archive file, but its checksum is missing.",
)
elif doc.has_archive_version:
archive_path: Final[Path] = Path(doc.archive_path).resolve()
if not archive_path.exists() or not archive_path.is_file():
messages.error(doc.pk, "Archived version of document does not exist.")
else:
if archive_path in present_files:
present_files.remove(archive_path)
try: try:
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest() _ = thumbnail_path.read_bytes()
except OSError as e: except OSError as e:
messages.error( messages.error(
doc.pk, doc.pk,
f"Cannot read archive file of document : {e}", f"Cannot read thumbnail file of document: {e}",
)
# Check sanity of the original file
# TODO: extract method
source_path: Final[Path] = Path(doc.source_path).resolve()
if not source_path.exists() or not source_path.is_file():
messages.error(doc.pk, "Original of document does not exist.")
else:
if source_path in present_files:
present_files.remove(source_path)
try:
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
except OSError as e:
messages.error(
doc.pk,
f"Cannot read original file of document: {e}",
) )
else: else:
if checksum != doc.archive_checksum: if checksum != doc.checksum:
messages.error( messages.error(
doc.pk, doc.pk,
"Checksum mismatch of archived document. " "Checksum mismatch. "
f"Stored: {doc.archive_checksum}, " f"Stored: {doc.checksum}, actual: {checksum}.",
f"actual: {checksum}.",
) )
# other document checks # Check sanity of the archive file.
if not doc.content: if doc.archive_checksum is not None and doc.archive_filename is None:
messages.info(doc.pk, "Document contains no OCR data") messages.error(
doc.pk,
"Document has an archive file checksum, but no archive filename.",
)
elif doc.archive_checksum is None and doc.archive_filename is not None:
messages.error(
doc.pk,
"Document has an archive file, but its checksum is missing.",
)
elif doc.has_archive_version:
archive_path: Final[Path] = Path(doc.archive_path).resolve()
if not archive_path.exists() or not archive_path.is_file():
messages.error(
doc.pk,
"Archived version of document does not exist.",
)
else:
if archive_path in present_files:
present_files.remove(archive_path)
try:
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest()
except OSError as e:
messages.error(
doc.pk,
f"Cannot read archive file of document : {e}",
)
else:
if checksum != doc.archive_checksum:
messages.error(
doc.pk,
"Checksum mismatch of archived document. "
f"Stored: {doc.archive_checksum}, "
f"actual: {checksum}.",
)
# other document checks
if not doc.content:
messages.info(doc.pk, "Document contains no OCR data")
progress_bar.update(task, advance=1)
for extra_file in present_files: for extra_file in present_files:
messages.warning(None, f"Orphaned file in media dir: {extra_file}") messages.warning(None, f"Orphaned file in media dir: {extra_file}")

View File

@@ -936,7 +936,7 @@ def task_postrun_handler(
task_instance = PaperlessTask.objects.filter(task_id=task_id).first() task_instance = PaperlessTask.objects.filter(task_id=task_id).first()
if task_instance is not None: if task_instance is not None:
task_instance.status = state task_instance.status = state or states.FAILURE
task_instance.result = retval task_instance.result = retval
task_instance.date_done = timezone.now() task_instance.date_done = timezone.now()
task_instance.save() task_instance.save()

View File

@@ -8,7 +8,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from tempfile import mkstemp from tempfile import mkstemp
import tqdm
from celery import Task from celery import Task
from celery import shared_task from celery import shared_task
from celery import states from celery import states
@@ -19,6 +18,11 @@ from django.db import transaction
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils import timezone from django.utils import timezone
from filelock import FileLock from filelock import FileLock
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from whoosh.writing import AsyncWriter from whoosh.writing import AsyncWriter
from documents import index from documents import index
@@ -83,9 +87,20 @@ def index_reindex(*, progress_bar_disable=False) -> None:
ix = index.open_index(recreate=True) ix = index.open_index(recreate=True)
with AsyncWriter(ix) as writer: with (
for document in tqdm.tqdm(documents, disable=progress_bar_disable): AsyncWriter(ix) as writer,
Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=progress_bar_disable,
) as progress,
):
task = progress.add_task("Reindexing documents", total=documents.count())
for document in documents:
index.update_document(writer, document) index.update_document(writer, document)
progress.update(task, advance=1)
@shared_task @shared_task

View File

@@ -5,7 +5,6 @@ from pathlib import Path
import faiss import faiss
import llama_index.core.settings as llama_settings import llama_index.core.settings as llama_settings
import tqdm
from celery import states from celery import states
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@@ -22,6 +21,11 @@ from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.core.storage.index_store import SimpleIndexStore from llama_index.core.storage.index_store import SimpleIndexStore
from llama_index.core.text_splitter import TokenTextSplitter from llama_index.core.text_splitter import TokenTextSplitter
from llama_index.vector_stores.faiss import FaissVectorStore from llama_index.vector_stores.faiss import FaissVectorStore
from rich.progress import BarColumn
from rich.progress import Progress
from rich.progress import TaskProgressColumn
from rich.progress import TextColumn
from rich.progress import TimeRemainingColumn
from documents.models import Document from documents.models import Document
from documents.models import PaperlessTask from documents.models import PaperlessTask
@@ -176,9 +180,18 @@ def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str:
embed_model = get_embedding_model() embed_model = get_embedding_model()
llama_settings.Settings.embed_model = embed_model llama_settings.Settings.embed_model = embed_model
storage_context = get_or_create_storage_context(rebuild=True) storage_context = get_or_create_storage_context(rebuild=True)
for document in tqdm.tqdm(documents, disable=progress_bar_disable): with Progress(
document_nodes = build_document_node(document) TextColumn("[progress.description]{task.description}"),
nodes.extend(document_nodes) BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=progress_bar_disable,
) as progress:
task = progress.add_task("Building document nodes", total=documents.count())
for document in documents:
document_nodes = build_document_node(document)
nodes.extend(document_nodes)
progress.update(task, advance=1)
index = VectorStoreIndex( index = VectorStoreIndex(
nodes=nodes, nodes=nodes,
@@ -196,23 +209,33 @@ def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str:
for node in index.docstore.get_nodes(all_node_ids) for node in index.docstore.get_nodes(all_node_ids)
} }
for document in tqdm.tqdm(documents, disable=progress_bar_disable): with Progress(
doc_id = str(document.id) TextColumn("[progress.description]{task.description}"),
document_modified = document.modified.isoformat() BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
disable=progress_bar_disable,
) as progress:
task = progress.add_task("Updating index nodes", total=documents.count())
for document in documents:
doc_id = str(document.id)
document_modified = document.modified.isoformat()
if doc_id in existing_nodes: if doc_id in existing_nodes:
node = existing_nodes[doc_id] node = existing_nodes[doc_id]
node_modified = node.metadata.get("modified") node_modified = node.metadata.get("modified")
if node_modified == document_modified: if node_modified == document_modified:
continue progress.update(task, advance=1)
continue
# Again, delete from docstore, FAISS IndexFlatL2 are append-only # Again, delete from docstore, FAISS IndexFlatL2 are append-only
index.docstore.delete_document(node.node_id) index.docstore.delete_document(node.node_id)
nodes.extend(build_document_node(document)) nodes.extend(build_document_node(document))
else: else:
# New document, add it # New document, add it
nodes.extend(build_document_node(document)) nodes.extend(build_document_node(document))
progress.update(task, advance=1)
if nodes: if nodes:
msg = "LLM index updated successfully." msg = "LLM index updated successfully."

View File

@@ -76,6 +76,7 @@ def test_update_llm_index(
mock_queryset = MagicMock() mock_queryset = MagicMock()
mock_queryset.exists.return_value = True mock_queryset.exists.return_value = True
mock_queryset.__iter__.return_value = iter([real_document]) mock_queryset.__iter__.return_value = iter([real_document])
mock_queryset.count.return_value = 1
mock_all.return_value = mock_queryset mock_all.return_value = mock_queryset
indexing.update_llm_index(rebuild=True) indexing.update_llm_index(rebuild=True)
@@ -97,6 +98,7 @@ def test_update_llm_index_removes_meta(
mock_queryset = MagicMock() mock_queryset = MagicMock()
mock_queryset.exists.return_value = True mock_queryset.exists.return_value = True
mock_queryset.__iter__.return_value = iter([real_document]) mock_queryset.__iter__.return_value = iter([real_document])
mock_queryset.count.return_value = 1
mock_all.return_value = mock_queryset mock_all.return_value = mock_queryset
indexing.update_llm_index(rebuild=True) indexing.update_llm_index(rebuild=True)
@@ -129,6 +131,7 @@ def test_update_llm_index_partial_update(
mock_queryset = MagicMock() mock_queryset = MagicMock()
mock_queryset.exists.return_value = True mock_queryset.exists.return_value = True
mock_queryset.__iter__.return_value = iter([real_document, doc2]) mock_queryset.__iter__.return_value = iter([real_document, doc2])
mock_queryset.count.return_value = 2
mock_all.return_value = mock_queryset mock_all.return_value = mock_queryset
indexing.update_llm_index(rebuild=True) indexing.update_llm_index(rebuild=True)
@@ -149,6 +152,7 @@ def test_update_llm_index_partial_update(
mock_queryset = MagicMock() mock_queryset = MagicMock()
mock_queryset.exists.return_value = True mock_queryset.exists.return_value = True
mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3]) mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3])
mock_queryset.count.return_value = 3
mock_all.return_value = mock_queryset mock_all.return_value = mock_queryset
# assert logs "Updating LLM index with %d new nodes and removing %d old nodes." # assert logs "Updating LLM index with %d new nodes and removing %d old nodes."

241
uv.lock generated
View File

@@ -322,20 +322,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
] ]
[[package]]
name = "backrefs"
version = "6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" },
{ url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" },
{ url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" },
{ url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
]
[[package]] [[package]]
name = "banks" name = "banks"
version = "2.3.0" version = "2.3.0"
@@ -944,7 +930,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
] ]
[[package]] [[package]]
name = "deprecated" name = "deprecated"
version = "1.3.1" version = "1.3.1"
@@ -1507,18 +1492,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
] ]
[[package]]
name = "ghp-import"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
]
[[package]] [[package]]
name = "gotenberg-client" name = "gotenberg-client"
version = "0.13.1" version = "0.13.1"
@@ -2302,15 +2275,15 @@ wheels = [
[[package]] [[package]]
name = "llama-index-llms-openai" name = "llama-index-llms-openai"
version = "0.6.17" version = "0.6.18"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/94/55e9855f565b30604fe9e154a7c3df1f4437ce62ed8926a8d6b5112cf73b/llama_index_llms_openai-0.6.17.tar.gz", hash = "sha256:63294c1d8d221d2009023b2c294c919f6141f4cdce9cd48bffe700d8be2a37e0", size = 25941, upload-time = "2026-02-03T11:03:26.355Z" } sdist = { url = "https://files.pythonhosted.org/packages/56/78/298de76242aee7f5fdd65a0bffb541b3f81759613de1e8ebc719eec8e8af/llama_index_llms_openai-0.6.18.tar.gz", hash = "sha256:36c0256a7a211bbbc5ecc00d3f2caa9730eea1971ced3b68b7c94025c0448020", size = 25946, upload-time = "2026-02-06T12:01:03.095Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/85/1dee39a45b31528611cc6139777fc5631b9946fe085b37cd981062aa6d50/llama_index_llms_openai-0.6.17-py3-none-any.whl", hash = "sha256:d275f7e218611afc6bb157fe722f6e8590e8e20ac3ea1392cf560ac674c175af", size = 26940, upload-time = "2026-02-03T11:03:25.059Z" }, { url = "https://files.pythonhosted.org/packages/39/46/5a4b62108fb94febe27d35c8476dea042d7a609ee4bf14f5b61f03d5a75a/llama_index_llms_openai-0.6.18-py3-none-any.whl", hash = "sha256:73bbbf233d38116d48350391a3649884829564f4c8f6168c8fa3f3ae1b557376", size = 26945, upload-time = "2026-02-06T12:01:01.25Z" },
] ]
[[package]] [[package]]
@@ -2558,15 +2531,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "mergedeep"
version = "1.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
]
[[package]] [[package]]
name = "microsoft-python-type-stubs" name = "microsoft-python-type-stubs"
version = "0" version = "0"
@@ -3054,15 +3018,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
] ]
[[package]]
name = "paginate"
version = "0.5.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
]
[[package]] [[package]]
name = "paperless-ngx" name = "paperless-ngx"
version = "2.20.6" version = "2.20.6"
@@ -3128,7 +3083,6 @@ dependencies = [
{ name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3211,7 +3165,6 @@ typing = [
{ name = "types-pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
[package.metadata] [package.metadata]
@@ -3282,7 +3235,6 @@ requires-dist = [
{ name = "setproctitle", specifier = "~=1.3.4" }, { name = "setproctitle", specifier = "~=1.3.4" },
{ name = "tika-client", specifier = "~=0.10.0" }, { name = "tika-client", specifier = "~=0.10.0" },
{ name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" },
{ name = "tqdm", specifier = "~=4.67.1" },
{ name = "watchfiles", specifier = ">=1.1.1" }, { name = "watchfiles", specifier = ">=1.1.1" },
{ name = "whitenoise", specifier = "~=6.11" }, { name = "whitenoise", specifier = "~=6.11" },
{ name = "whoosh-reloaded", specifier = ">=2.7.5" }, { name = "whoosh-reloaded", specifier = ">=2.7.5" },
@@ -3310,9 +3262,7 @@ dev = [
{ name = "ruff", specifier = "~=0.15.0" }, { name = "ruff", specifier = "~=0.15.0" },
{ name = "zensical", specifier = ">=0.0.21" }, { name = "zensical", specifier = ">=0.0.21" },
] ]
docs = [ docs = [{ name = "zensical", specifier = ">=0.0.21" }]
{ name = "zensical", specifier = ">=0.0.21" },
]
lint = [ lint = [
{ name = "prek", specifier = "~=0.3.0" }, { name = "prek", specifier = "~=0.3.0" },
{ name = "ruff", specifier = "~=0.15.0" }, { name = "ruff", specifier = "~=0.15.0" },
@@ -3351,7 +3301,6 @@ typing = [
{ name = "types-pytz" }, { name = "types-pytz" },
{ name = "types-redis" }, { name = "types-redis" },
{ name = "types-setuptools" }, { name = "types-setuptools" },
{ name = "types-tqdm" },
] ]
[[package]] [[package]]
@@ -3591,23 +3540,23 @@ wheels = [
[[package]] [[package]]
name = "prek" name = "prek"
version = "0.3.1" version = "0.3.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/62/4b91c8a343e21fcefabc569a91d08cf8756c554332521af78294acef7c27/prek-0.3.1.tar.gz", hash = "sha256:45abc4ffd3cb2d39c478f47e92e88f050e5a4b7a20915d78e54b1a3d3619ebe4", size = 323141, upload-time = "2026-01-31T13:25:58.128Z" } sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/c1/e0e545048e4190245fb4ae375d67684108518f3c69b67b81d62e1cd855c6/prek-0.3.1-py3-none-linux_armv6l.whl", hash = "sha256:1f77d0845cc63cad9c447f7f7d554c1ad188d07dbe02741823d20d58c7312eaf", size = 4285460, upload-time = "2026-01-31T13:25:42.066Z" }, { url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" },
{ url = "https://files.pythonhosted.org/packages/10/fe/7636d10e2dafdf2a4a927c989f32ce3f08e99d62ebad7563f0272e74b7f4/prek-0.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e21142300d139e8c7f3970dd8aa66391cb43cd17c0c4ee65ff1af48856bb6a4b", size = 4287085, upload-time = "2026-01-31T13:25:40.193Z" }, { url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" },
{ url = "https://files.pythonhosted.org/packages/a3/7f/62ed57340071e04c02057d64276ec3986baca3ad4759523e2f433dc9be55/prek-0.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c09391de7d1994f9402c46cb31671800ea309b1668d839614965706206da3655", size = 3936188, upload-time = "2026-01-31T13:25:47.314Z" }, { url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" },
{ url = "https://files.pythonhosted.org/packages/6b/17/cb24f462c255f76d130ca110f4fcec09b041c3c770e43960cc3fc9dcc9ce/prek-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a0b0a012ef6ef28dee019cf81a95c5759b2c03287c32c1f2adcb5f5fb097138e", size = 4275214, upload-time = "2026-01-31T13:25:38.053Z" }, { url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" },
{ url = "https://files.pythonhosted.org/packages/f2/85/db155b59d73cf972c8467e4d95def635f9976d5fcbcc790a4bbe9d2e049a/prek-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4a0e40785d39b8feae0d7ecf5534789811a2614114ab47f4e344a2ebd75ac10", size = 4197982, upload-time = "2026-01-31T13:25:50.034Z" }, { url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" },
{ url = "https://files.pythonhosted.org/packages/06/cf/d35c32436692928a9ca53eed3334e30148a60f0faa33c42e8d11b6028fa6/prek-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5c2f5e377e3cc5a5ea191deb8255a5823fbaa01b424417fe18ff12c7c800f3", size = 4458572, upload-time = "2026-01-31T13:25:51.46Z" }, { url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c0/eb36fecb21fe30baa72fb87ccf3a791c32932786c287f95f8972402e9245/prek-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fe70e97f4dfca57ce142caecd77b90a435abd1c855f9e96041078415d80e89a", size = 4999230, upload-time = "2026-01-31T13:25:44.055Z" }, { url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/e4/f3/ad1a25ea16320e6acd1fedd6bd96a0d22526f5132d9b5adc045996ccca4c/prek-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b28e921d893771bdd7533cd94d46a10e0cf2855c5e6bf6809b598b5e45baa73", size = 4510376, upload-time = "2026-01-31T13:25:48.563Z" }, { url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" },
{ url = "https://files.pythonhosted.org/packages/39/b7/91afdd24be808ccf3b9119f4cf2bd6d02e30683143a62a08f432a3435861/prek-0.3.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:555610a53c4541976f4fe8602177ecd7a86a931dcb90a89e5826cfc4a6c8f2cb", size = 4273229, upload-time = "2026-01-31T13:25:56.362Z" }, { url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" },
{ url = "https://files.pythonhosted.org/packages/5a/bb/636c77c5c9fc5eadcc2af975a95b48eeeff2dc833021e222b0e9479b9b47/prek-0.3.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:663a15a31b705db5d01a1c9eb28c6ea417628e6cb68de2dc7b3016ca2f959a99", size = 4301166, upload-time = "2026-01-31T13:25:36.281Z" }, { url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" },
{ url = "https://files.pythonhosted.org/packages/4e/cf/c928a36173e71b21b252943404a5b3d1ddc1f08c9e0f1d7282a2c62c7325/prek-0.3.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1d03fb5fa37177dc37ccbe474252473adcbde63287a63e9fa3d5745470f95bd8", size = 4188473, upload-time = "2026-01-31T13:25:53.341Z" }, { url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" },
{ url = "https://files.pythonhosted.org/packages/98/4c/af8f6a40cb094e88225514b89e8ae05ac69fc479d6b500e4b984f9ef8ae3/prek-0.3.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3e20a5b704c06944dca9555a39f1de3622edc8ed2becaf8b3564925d1b7c1c0d", size = 4342239, upload-time = "2026-01-31T13:25:55.179Z" }, { url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" },
{ url = "https://files.pythonhosted.org/packages/b7/ba/6b0f725c0cf96182ab9622b4d122a01f04de9b2b6e4a6516874390218510/prek-0.3.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6889acf0c9b0dd7b9cd3510ec36af10a739826d2b9de1e2d021ae6a9f130959a", size = 4618674, upload-time = "2026-01-31T13:25:59.175Z" }, { url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" },
] ]
[[package]] [[package]]
@@ -4330,18 +4279,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
] ]
[[package]]
name = "pyyaml-env-tag"
version = "1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
]
[[package]] [[package]]
name = "pyzbar" name = "pyzbar"
version = "0.1.9" version = "0.1.9"
@@ -4898,50 +4835,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" },
] ]
[[package]]
name = "selectolax"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/c5/959b8661d200d9fd3cf52061ce6f1d7087ec94823bb324fe1ca76c80b250/selectolax-0.4.6.tar.gz", hash = "sha256:bd9326cfc9bbd5bfcda980b0452b9761b3cf134015154e95d83fa32cbfbb751b", size = 4793248, upload-time = "2025-12-06T12:35:48.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/b6/a4753cdde612caf292abb3519b8770a004cb696fac0aea40b403f5e27672/selectolax-0.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e870cc935a36b8e37945c5e2055191df84e7af278dd386e5ff9f04552e5da6b", size = 2052715, upload-time = "2025-12-06T12:34:08.527Z" },
{ url = "https://files.pythonhosted.org/packages/3a/03/a3b21e54fbe5fee47d5e27fdb5696467c7d9adb7308d1bc85f1468a4d08e/selectolax-0.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9008e4c5f708634baa6dbeb4cd8922edc8467eb842cf12da5cdf03399a969f3", size = 2051040, upload-time = "2025-12-06T12:34:10.365Z" },
{ url = "https://files.pythonhosted.org/packages/b2/ad/3984fa06ece9c773acc7938ccbee922d437d5396fb331f22a122da6fea8d/selectolax-0.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c7120afee58104f9d708b62b4e14caa9dc88c23e3f752b62a43dac953963362", size = 2237979, upload-time = "2025-12-06T12:34:12.053Z" },
{ url = "https://files.pythonhosted.org/packages/00/f2/ed47a855ca70db65b1c5d90e6cbb63132687a0b623a2edca5de26a68cadc/selectolax-0.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ceb1af4892aafd26612b1974c49abf524b5f69b2a9d521381d97dc8ca5c0e3d7", size = 2274003, upload-time = "2025-12-06T12:34:13.807Z" },
{ url = "https://files.pythonhosted.org/packages/13/74/840b0cd0a178718c815610fbe704803b4cf98aa1a8eb218788b77c020811/selectolax-0.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a7a28857b7bd72e904c3e2fce9a21ceca60093622b3ceaf8e231a9ec24a0ca1", size = 2251372, upload-time = "2025-12-06T12:34:15.611Z" },
{ url = "https://files.pythonhosted.org/packages/a6/56/07bdcadb0180541b36c3f5dccd1d5243503ab695c2df6030e533ea098529/selectolax-0.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59162e0b78d82c8b60e1318b3e72b49eed5858c2640f1df759d3f60584e4f215", size = 2280044, upload-time = "2025-12-06T12:34:17.382Z" },
{ url = "https://files.pythonhosted.org/packages/a0/76/9b5358d82353b68c5828cc8ceae4ff1073495462979d2035c1089f4421dd/selectolax-0.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:751f727c9963584fcfa101b2696e0dd31236142901bbe7866a8e39f2ec346cc4", size = 2052057, upload-time = "2025-12-06T12:34:24.448Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d1/9f8c8841f414a1b72174acef916d4ed38cc0fa041d3e933013e2b3213f30/selectolax-0.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6f45737b638836b9fe2a4e7ccfbcd48a315ebb96da76a79a04b8227c1a967ee", size = 2050379, upload-time = "2025-12-06T12:34:26.383Z" },
{ url = "https://files.pythonhosted.org/packages/bc/c4/8e5ab9275a185a31d06b5ea58e3ba61d57e57700964cbd56fe074dbe458c/selectolax-0.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15a22c5cd9c23f09227b2b9c227d986396125a9b0eb21be655f22fe4ccc5679a", size = 2238005, upload-time = "2025-12-06T12:34:28.2Z" },
{ url = "https://files.pythonhosted.org/packages/ea/af/f4299d931a8e11ba1998cac70d407c5338436978325232eb252ac7f5aba2/selectolax-0.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64f771807f1a7353f4d6878c303bfbd6c6fe58897e301aa6d0de7e5e60cef61", size = 2272927, upload-time = "2025-12-06T12:34:29.955Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5e/9afbb0e8495846bf97fa7725a6f97622ad85efc654cb3cbf4c881bd345de/selectolax-0.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0144a37fbf01695ccf2d0d24caaa058a28449cecb2c754bb9e616f339468d74", size = 2250901, upload-time = "2025-12-06T12:34:31.442Z" },
{ url = "https://files.pythonhosted.org/packages/d5/6b/914cc9c977fd21949af5776d25b9c011b6e72fb38569161ab6ca83d6130a/selectolax-0.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c9bdd4a9b3f71f28a0ee558a105451debd33cbe1ed350ebba7ad6b68d62815c", size = 2279842, upload-time = "2025-12-06T12:34:32.739Z" },
{ url = "https://files.pythonhosted.org/packages/ee/81/1fdf6633df840afd9d7054a3441f04cfb1fc9d098c6c9f3bd46a64fb632e/selectolax-0.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20615062d6062b197b61fd646e667591e987be3a894e8a8408d2a482ccddc747", size = 2051021, upload-time = "2025-12-06T12:34:38.495Z" },
{ url = "https://files.pythonhosted.org/packages/cc/54/d59738d090cb4df3a3a6297b7ec216b86d3ba7f320013c4bc8d4841c9f5d/selectolax-0.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c436006e2af6ade80b96411cf46652c11ced4f230032e25e1f5210b7522a4fe3", size = 2047409, upload-time = "2025-12-06T12:34:39.875Z" },
{ url = "https://files.pythonhosted.org/packages/fc/67/3b163ec18a128df3a3b59ce676a2dacfb26e714da81ba8a98e184b4ef187/selectolax-0.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:705a70b6f4e553e8c5299881269d3735a7df8a23711927a33caa16b4eaef580f", size = 2237052, upload-time = "2025-12-06T12:34:41.24Z" },
{ url = "https://files.pythonhosted.org/packages/f0/04/c3ae4a77e8cfa647b9177e727a7e80f64b160b65ad0db0dcb3738a4ef4a0/selectolax-0.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c04fd180689ed9450ad2453a3cba74cff2475a4281f76db9e18a658b7823e594", size = 2275615, upload-time = "2025-12-06T12:34:43.114Z" },
{ url = "https://files.pythonhosted.org/packages/12/de/aaa016c44e63a1efd5525f6da6eac807388a06c70671091c735d93f13b74/selectolax-0.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb33eb0809e70ba4a475105d164c3f90a4bb711744ca69e20037298256b8e9d7", size = 2249186, upload-time = "2025-12-06T12:34:44.84Z" },
{ url = "https://files.pythonhosted.org/packages/76/9a/a9cf9f0158b0804c7ea404d790af031830eb6452a4948853f7582eea6c51/selectolax-0.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97f30b7c731f9f3328e9c6aef7ca3c17fbcbc4495e286a2cdad9a77bcadfadf1", size = 2282041, upload-time = "2025-12-06T12:34:46.19Z" },
{ url = "https://files.pythonhosted.org/packages/4d/87/7ed053cce7de8b29746c602851c67654287e25ec404d575911c6f40b671f/selectolax-0.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8303af0eeef6ab2f06f2b18e3c825df4f6984405a862c8e9e654621c26558aca", size = 2050412, upload-time = "2025-12-06T12:34:53.086Z" },
{ url = "https://files.pythonhosted.org/packages/44/74/e8cd9b9b53da0e849b27a2ef68580915321ee52a662f015275a1cf6cca85/selectolax-0.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8b1fb5507d90e719de705d93eaa3bdd5f2d69facf012fb67b7da8a9cd20ea6b", size = 2046513, upload-time = "2025-12-06T12:34:54.583Z" },
{ url = "https://files.pythonhosted.org/packages/a0/ca/c1cc7f03b681d272dbb0dc47cf9d0df857ae156d7ea54d88cc25ec23c8e9/selectolax-0.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ac51bd5bfcd6bffe58720b1fc5f97666681f0793d79d292069b3a3f8599ef0", size = 2234404, upload-time = "2025-12-06T12:34:55.971Z" },
{ url = "https://files.pythonhosted.org/packages/df/00/a6e5c4d65243147fbd8837951267f098d9bee66ada4dc0c99d97259e052c/selectolax-0.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfee4d1269fd462256641abdf6c2ee4dd799ba82c578acab0bcde07547637826", size = 2272678, upload-time = "2025-12-06T12:34:57.3Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a2/05351e0f0da62d1bc01b7820a990f1e4dec688206e0278ee8faeeb878940/selectolax-0.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fce09eeb5dd19fba672a829f63e3c40238af4a929ce1e5fd16dcbc4fd253e300", size = 2247471, upload-time = "2025-12-06T12:34:59.048Z" },
{ url = "https://files.pythonhosted.org/packages/ea/95/1ab9e3ad2d53dbcd7141af149c7259c693dc2dc46e27d72b7a68f17f3364/selectolax-0.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4e97ca71d545cab9c57ba3b18358cbc96ef0dcd01920ea903a531386ca1f849", size = 2278271, upload-time = "2025-12-06T12:35:00.487Z" },
{ url = "https://files.pythonhosted.org/packages/d2/38/93b4907c596487e13f8261129b1663559c96fc37ea2c973c98a393307484/selectolax-0.4.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:961c037ac41a12bf9db35a95d6076461f9b127d361c9ef26a77f76c45b1056eb", size = 2069131, upload-time = "2025-12-06T12:35:06.737Z" },
{ url = "https://files.pythonhosted.org/packages/f6/04/d29769a8313ccb9db6278401840e79662f341b716d268f7468b6270d15e1/selectolax-0.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2be076a69a12657a38933b99d31c604d95d63911e0799f89305da8e89918874", size = 2066134, upload-time = "2025-12-06T12:35:08.163Z" },
{ url = "https://files.pythonhosted.org/packages/29/a2/1c26b4cc6a708d234e39199bd75acdc1cfdcf4f3138e16d25e3e7aa0295d/selectolax-0.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ae5dd7c20035312211de43952934981e8ff4e1502467ce78665f57bc5eaf7f", size = 2240810, upload-time = "2025-12-06T12:35:09.556Z" },
{ url = "https://files.pythonhosted.org/packages/1f/de/b021342d306bd08c92d0d9e9072a3ed6a3038f78387187d37fb775196fa0/selectolax-0.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe1274930affe2b986fd20cbf2916ddf4a760edf30a7eeb9151b31e8cbe6027", size = 2274401, upload-time = "2025-12-06T12:35:11.023Z" },
{ url = "https://files.pythonhosted.org/packages/29/77/b2816be2f4878f4e86fabca5932610401dc88d956948d97a82947e27d8bc/selectolax-0.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e52174a75236c1321f0f0353b5d97ba575c61428f16c2d78159cb154fa407e97", size = 2271054, upload-time = "2025-12-06T12:35:12.878Z" },
{ url = "https://files.pythonhosted.org/packages/1f/3a/ae48b9febf2af21a0fdd4fba07861ae3e13bd7235df3fa39918d33004058/selectolax-0.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5032187ed8f8d77da6dee5467a87fe9b1553c10c63671fe830e87a2d347ef8ae", size = 2297773, upload-time = "2025-12-06T12:35:14.383Z" },
{ url = "https://files.pythonhosted.org/packages/54/ce/f3fa904517745ecd289b6ce18845ae968cf7d0b17e65ab781259f2746254/selectolax-0.4.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:48a9bce389c281fec8bf3b5a36bd376f1ad1f36ff072dcedaa977188b3882be1", size = 2088107, upload-time = "2025-12-06T12:35:21.061Z" },
{ url = "https://files.pythonhosted.org/packages/4f/18/359da9723d3ec1235819cea0958bc417ce6a12699977920854b300ad4529/selectolax-0.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff1632c1767f6005adc2dff7b72ea9d0b6493702e86e04ee5cf934ab630172aa", size = 2090836, upload-time = "2025-12-06T12:35:22.421Z" },
{ url = "https://files.pythonhosted.org/packages/e2/20/199f2a05ca8dfd9bc146af03fbfaa1e2050601d3141267070a2a023ea68f/selectolax-0.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64a56dd91c22e809305e127bbd4cd75ad1016dd329a4e945a176c6f3376d00e2", size = 2248608, upload-time = "2025-12-06T12:35:23.946Z" },
{ url = "https://files.pythonhosted.org/packages/f9/67/261c06cdae29fad287008d39f51a80431f3fce66c2865b880e61d04cdfd2/selectolax-0.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:223471d2c9095406d69faf461aa217782575d62362d36809e350f6d3c2f69f4e", size = 2275653, upload-time = "2025-12-06T12:35:25.423Z" },
{ url = "https://files.pythonhosted.org/packages/10/80/f2519487a86b5366acadd9e07c212b38fb657bb62b9e01de9fb24c3ada27/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c1043e61df29f4f8ef49f9f623b3d710f0c545d9a7939eee52c49987b06aef7", size = 2283495, upload-time = "2025-12-06T12:35:26.843Z" },
{ url = "https://files.pythonhosted.org/packages/66/cc/35a0fd896371e0b5a84365b03f88c7dbe8984862e34ce32baed81ee6f486/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b323ad4ebcf2edad2488022402fbc73ee158ffe81607ec1ce5eb1039eab94086", size = 2300207, upload-time = "2025-12-06T12:35:28.23Z" },
]
[[package]] [[package]]
name = "sentence-transformers" name = "sentence-transformers"
version = "5.2.2" version = "5.2.2"
@@ -5687,18 +5580,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" }, { url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" },
] ]
[[package]]
name = "types-tqdm"
version = "4.67.2.20260202"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/6a/a9e27944f2715940cf7b43b35a2b464d968d086723414983e47af3261e2d/types_tqdm-4.67.2.20260202.tar.gz", hash = "sha256:c3c494ae8dfd2946fa1fe089ce6931a85bb88d9078da0d65c6b99b6643f7561d", size = 17418, upload-time = "2026-02-02T04:11:26.288Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/f4/d24e5216630081356fd9427cb9fb4c94e3c6cfde4d4d46db2c6717e286ae/types_tqdm-4.67.2.20260202-py3-none-any.whl", hash = "sha256:01b31780b2a2cdad8465a3e53a06fdf53a47abab0d46d69c1b2039197ffd55b0", size = 23893, upload-time = "2026-02-02T04:11:25.007Z" },
]
[[package]] [[package]]
name = "types-webencodings" name = "types-webencodings"
version = "0.5.0.20251108" version = "0.5.0.20251108"
@@ -5901,35 +5782,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
] ]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
{ url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
{ url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
{ url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
]
[[package]] [[package]]
name = "watchfiles" name = "watchfiles"
version = "1.1.1" version = "1.1.1"
@@ -6235,6 +6087,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
] ]
[[package]]
name = "zensical"
version = "0.0.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "deepmerge", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/50/2655b5f72d0c72f4366be580f5e2354ff05280d047ea986fe89570e44589/zensical-0.0.21.tar.gz", hash = "sha256:c13563836fa63a3cabeffd83fe3a770ca740cfa5ae7b85df85d89837e31b3b4a", size = 3819731, upload-time = "2026-02-04T17:47:59.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/98/90710d232cb35b633815fa7b493da542391b89283b6103a5bb4ae9fc0dd9/zensical-0.0.21-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:67404cc70c330246dfb7269bcdb60a25be0bb60a212a09c9c50229a1341b1f84", size = 12237120, upload-time = "2026-02-04T17:47:28.615Z" },
{ url = "https://files.pythonhosted.org/packages/97/fb/4280b3781157e8f051711732192f949bf29beeafd0df3e33c1c8bf9b7a1a/zensical-0.0.21-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4fd253ccfbf5af56434124f13bac01344e456c020148369b18d8836b6537c3c", size = 12118047, upload-time = "2026-02-04T17:47:31.369Z" },
{ url = "https://files.pythonhosted.org/packages/74/b3/b7f85ae9cf920cf9f17bf157ae6c274919477148feb7716bf735636caa0e/zensical-0.0.21-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440e40cdc30a29bf7466bcd6f43ed7bd1c54ea3f1a0fefca65619358b481a5bc", size = 12473440, upload-time = "2026-02-04T17:47:33.577Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ac/1dc6e98f79ed19b9f103c88a0bd271f9140565d7d26b64bc1542b3ef6d91/zensical-0.0.21-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:368e832fc8068e75dc45cab59379db4cefcd81eb116f48d058db8fb7b7aa8d14", size = 12412588, upload-time = "2026-02-04T17:47:36.491Z" },
{ url = "https://files.pythonhosted.org/packages/bd/76/16a580f6dd32b387caa4a41615451e7dddd1917a2ff2e5b08744f41b4e11/zensical-0.0.21-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ab962d47f9dd73510eed168469326c7a452554dfbfdb9cdf85efc7140244df", size = 12749438, upload-time = "2026-02-04T17:47:38.969Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/4baaa1c910eee61db5f49d0d45f2e550a0027218c618f3dd7f8da966a019/zensical-0.0.21-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b846d53dfce007f056ff31848f87f3f2a388228e24d4851c0cafdce0fa204c9b", size = 12514504, upload-time = "2026-02-04T17:47:41.31Z" },
{ url = "https://files.pythonhosted.org/packages/76/77/931fccae5580b94409a0448a26106f922dcfa7822e7b93cacd2876dd63a8/zensical-0.0.21-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:daac1075552d230d52d621d2e4754ba24d5afcaa201a7a991f1a8d57e320c9de", size = 12647832, upload-time = "2026-02-04T17:47:44.073Z" },
{ url = "https://files.pythonhosted.org/packages/5b/82/3cf75de64340829d55c87c36704f4d1d8c952bd2cdc8a7bc48cbfb8ab333/zensical-0.0.21-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b380f545adb6d40896f9bd698eb0e1540ed4258d35b83f55f91658d0fdae312", size = 12678537, upload-time = "2026-02-04T17:47:46.899Z" },
{ url = "https://files.pythonhosted.org/packages/77/91/6f4938dceeaa241f78bbfaf58a94acef10ba18be3468795173e3087abeb6/zensical-0.0.21-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c2227fdab64616bea94b40b8340bafe00e2e23631cc58eeea1e7267167e6ac5", size = 12822164, upload-time = "2026-02-04T17:47:49.231Z" },
{ url = "https://files.pythonhosted.org/packages/a2/4e/a9c9d25ef0766f767db7b4f09da68da9b3d8a28c3d68cfae01f8e3f9e297/zensical-0.0.21-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2e0f5154d236ed0f98662ee68785b67e8cd2138ea9d5e26070649e93c22eeee0", size = 12785632, upload-time = "2026-02-04T17:47:52.613Z" },
]
[[package]] [[package]]
name = "zope-interface" name = "zope-interface"
version = "8.2" version = "8.2"
@@ -6268,34 +6147,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" },
] ]
[[package]]
name = "zensical"
version = "0.0.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "deepmerge", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/50/2655b5f72d0c72f4366be580f5e2354ff05280d047ea986fe89570e44589/zensical-0.0.21.tar.gz", hash = "sha256:c13563836fa63a3cabeffd83fe3a770ca740cfa5ae7b85df85d89837e31b3b4a", size = 3819731, upload-time = "2026-02-04T17:47:59.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/98/90710d232cb35b633815fa7b493da542391b89283b6103a5bb4ae9fc0dd9/zensical-0.0.21-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:67404cc70c330246dfb7269bcdb60a25be0bb60a212a09c9c50229a1341b1f84", size = 12237120, upload-time = "2026-02-04T17:47:28.615Z" },
{ url = "https://files.pythonhosted.org/packages/97/fb/4280b3781157e8f051711732192f949bf29beeafd0df3e33c1c8bf9b7a1a/zensical-0.0.21-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4fd253ccfbf5af56434124f13bac01344e456c020148369b18d8836b6537c3c", size = 12118047, upload-time = "2026-02-04T17:47:31.369Z" },
{ url = "https://files.pythonhosted.org/packages/74/b3/b7f85ae9cf920cf9f17bf157ae6c274919477148feb7716bf735636caa0e/zensical-0.0.21-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440e40cdc30a29bf7466bcd6f43ed7bd1c54ea3f1a0fefca65619358b481a5bc", size = 12473440, upload-time = "2026-02-04T17:47:33.577Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ac/1dc6e98f79ed19b9f103c88a0bd271f9140565d7d26b64bc1542b3ef6d91/zensical-0.0.21-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:368e832fc8068e75dc45cab59379db4cefcd81eb116f48d058db8fb7b7aa8d14", size = 12412588, upload-time = "2026-02-04T17:47:36.491Z" },
{ url = "https://files.pythonhosted.org/packages/bd/76/16a580f6dd32b387caa4a41615451e7dddd1917a2ff2e5b08744f41b4e11/zensical-0.0.21-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ab962d47f9dd73510eed168469326c7a452554dfbfdb9cdf85efc7140244df", size = 12749438, upload-time = "2026-02-04T17:47:38.969Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/4baaa1c910eee61db5f49d0d45f2e550a0027218c618f3dd7f8da966a019/zensical-0.0.21-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b846d53dfce007f056ff31848f87f3f2a388228e24d4851c0cafdce0fa204c9b", size = 12514504, upload-time = "2026-02-04T17:47:41.31Z" },
{ url = "https://files.pythonhosted.org/packages/76/77/931fccae5580b94409a0448a26106f922dcfa7822e7b93cacd2876dd63a8/zensical-0.0.21-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:daac1075552d230d52d621d2e4754ba24d5afcaa201a7a991f1a8d57e320c9de", size = 12647832, upload-time = "2026-02-04T17:47:44.073Z" },
{ url = "https://files.pythonhosted.org/packages/5b/82/3cf75de64340829d55c87c36704f4d1d8c952bd2cdc8a7bc48cbfb8ab333/zensical-0.0.21-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b380f545adb6d40896f9bd698eb0e1540ed4258d35b83f55f91658d0fdae312", size = 12678537, upload-time = "2026-02-04T17:47:46.899Z" },
{ url = "https://files.pythonhosted.org/packages/77/91/6f4938dceeaa241f78bbfaf58a94acef10ba18be3468795173e3087abeb6/zensical-0.0.21-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c2227fdab64616bea94b40b8340bafe00e2e23631cc58eeea1e7267167e6ac5", size = 12822164, upload-time = "2026-02-04T17:47:49.231Z" },
{ url = "https://files.pythonhosted.org/packages/a2/4e/a9c9d25ef0766f767db7b4f09da68da9b3d8a28c3d68cfae01f8e3f9e297/zensical-0.0.21-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2e0f5154d236ed0f98662ee68785b67e8cd2138ea9d5e26070649e93c22eeee0", size = 12785632, upload-time = "2026-02-04T17:47:52.613Z" },
]
[[package]] [[package]]
name = "zstandard" name = "zstandard"
version = "0.25.0" version = "0.25.0"