mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-24 02:05:48 -06:00
Compare commits
58 Commits
feature-au
...
0537e87cb5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0537e87cb5 | ||
|
|
cac48c9855 | ||
|
|
3fda648f37 | ||
|
|
95736eebc4 | ||
|
|
b4da5c3cd1 | ||
|
|
85027dbffd | ||
|
|
251b0fb3d6 | ||
|
|
32bdf11f7f | ||
|
|
0627ca69f5 | ||
|
|
f5525bbdff | ||
|
|
a21a2a41a8 | ||
|
|
cc73ed8b86 | ||
|
|
0c706b2316 | ||
|
|
85b7b6874d | ||
|
|
56b26185fa | ||
|
|
6537fade7b | ||
|
|
9f8090816f | ||
|
|
1de7c52478 | ||
|
|
9aaaa6f069 | ||
|
|
c3a20b7797 | ||
|
|
476556379b | ||
|
|
e5cafff043 | ||
|
|
8e0d574e99 | ||
|
|
8a5820328e | ||
|
|
809d62a2f4 | ||
|
|
0d87f94b9b | ||
|
|
315b90f8e5 | ||
|
|
47b2d2964b | ||
|
|
e05639ae4e | ||
|
|
f400a8cb2f | ||
|
|
26abcf5612 | ||
|
|
afde52430d | ||
|
|
716f2da652 | ||
|
|
c54073b7c2 | ||
|
|
247e6f39dc | ||
|
|
1e6dfc4481 | ||
|
|
7cc0750066 | ||
|
|
bd6585d3b4 | ||
|
|
717e828a1d | ||
|
|
07381d48e6 | ||
|
|
dd0ffaf312 | ||
|
|
264504affc | ||
|
|
4feedf2add | ||
|
|
2f76cf9831 | ||
|
|
1002d37f6b | ||
|
|
d260a94740 | ||
|
|
88c69b83ea | ||
|
|
2557ee2014 | ||
|
|
3c75deed80 | ||
|
|
d05343c927 | ||
|
|
e7972b7eaf | ||
|
|
75a091cc0d | ||
|
|
dca74803fd | ||
|
|
3cf3d868d0 | ||
|
|
bf4fc6604a | ||
|
|
e8c1eb86fa | ||
|
|
c3dad3cf69 | ||
|
|
811bd66088 |
@@ -32,7 +32,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.4-python3.12-bookworm-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.9.7-python3.12-bookworm-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -1794,3 +1794,23 @@ password. All of these options come from their similarly-named [Django settings]
|
|||||||
#### [`PAPERLESS_EMAIL_USE_SSL=<bool>`](#PAPERLESS_EMAIL_USE_SSL) {#PAPERLESS_EMAIL_USE_SSL}
|
#### [`PAPERLESS_EMAIL_USE_SSL=<bool>`](#PAPERLESS_EMAIL_USE_SSL) {#PAPERLESS_EMAIL_USE_SSL}
|
||||||
|
|
||||||
: Defaults to false.
|
: Defaults to false.
|
||||||
|
|
||||||
|
## Remote OCR
|
||||||
|
|
||||||
|
#### [`PAPERLESS_REMOTE_OCR_ENGINE=<str>`](#PAPERLESS_REMOTE_OCR_ENGINE) {#PAPERLESS_REMOTE_OCR_ENGINE}
|
||||||
|
|
||||||
|
: The remote OCR engine to use. Currently only Azure AI is supported as "azureai".
|
||||||
|
|
||||||
|
Defaults to None, which disables remote OCR.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_REMOTE_OCR_API_KEY=<str>`](#PAPERLESS_REMOTE_OCR_API_KEY) {#PAPERLESS_REMOTE_OCR_API_KEY}
|
||||||
|
|
||||||
|
: The API key to use for the remote OCR engine.
|
||||||
|
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_REMOTE_OCR_ENDPOINT=<str>`](#PAPERLESS_REMOTE_OCR_ENDPOINT) {#PAPERLESS_REMOTE_OCR_ENDPOINT}
|
||||||
|
|
||||||
|
: The endpoint to use for the remote OCR engine. This is required for Azure AI.
|
||||||
|
|
||||||
|
Defaults to None.
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ physical documents into a searchable online archive so you can keep, well, _less
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
|
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
|
||||||
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way.
|
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way, unless you explicitly choose to do so.
|
||||||
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
|
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
|
||||||
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
|
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
|
||||||
|
- _New!_ Supports remote OCR with Azure AI (opt-in).
|
||||||
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
|
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
|
||||||
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
|
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
|
||||||
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more.
|
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more.
|
||||||
|
|||||||
@@ -891,6 +891,21 @@ how regularly you intend to scan documents and use paperless.
|
|||||||
performed the task associated with the document, move it to the
|
performed the task associated with the document, move it to the
|
||||||
inbox.
|
inbox.
|
||||||
|
|
||||||
|
## Remote OCR
|
||||||
|
|
||||||
|
!!! important
|
||||||
|
|
||||||
|
This feature is disabled by default and will always remain strictly "opt-in".
|
||||||
|
|
||||||
|
Paperless-ngx supports performing OCR on documents using remote services. At the moment, this is limited to
|
||||||
|
[Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence).
|
||||||
|
This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with
|
||||||
|
Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing
|
||||||
|
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details.
|
||||||
|
|
||||||
|
Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size
|
||||||
|
or page limitations (e.g. with a free tier).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Paperless-ngx consists of the following components:
|
Paperless-ngx consists of the following components:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.19.4"
|
version = "2.19.5"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -16,6 +16,7 @@ classifiers = [
|
|||||||
# This will allow testing to not install a webserver, mysql, etc
|
# This will allow testing to not install a webserver, mysql, etc
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"azure-ai-documentintelligence>=1.0.2",
|
||||||
"babel>=2.17",
|
"babel>=2.17",
|
||||||
"bleach~=6.2.0",
|
"bleach~=6.2.0",
|
||||||
"celery[redis]~=5.5.1",
|
"celery[redis]~=5.5.1",
|
||||||
@@ -252,6 +253,7 @@ testpaths = [
|
|||||||
"src/paperless_tesseract/tests/",
|
"src/paperless_tesseract/tests/",
|
||||||
"src/paperless_tika/tests",
|
"src/paperless_tika/tests",
|
||||||
"src/paperless_text/tests/",
|
"src/paperless_text/tests/",
|
||||||
|
"src/paperless_remote/tests/",
|
||||||
]
|
]
|
||||||
addopts = [
|
addopts = [
|
||||||
"--pythonwarnings=all",
|
"--pythonwarnings=all",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "2.19.4",
|
"version": "2.19.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -354,5 +354,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
model.removeElement(atom)
|
model.removeElement(atom)
|
||||||
expect(completeSpy).toHaveBeenCalled()
|
expect(completeSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should subscribe to existing elements when queries are assigned', () => {
|
||||||
|
const expression = new CustomFieldQueryExpression()
|
||||||
|
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||||
|
model.queries = [expression]
|
||||||
|
expression.changed.next(expression)
|
||||||
|
expect(nextSpy).toHaveBeenCalledWith(model)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, Subscription, takeUntil } from 'rxjs'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||||
@@ -41,10 +41,27 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
|
|||||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||||
|
|
||||||
export class CustomFieldQueriesModel {
|
export class CustomFieldQueriesModel {
|
||||||
public queries: CustomFieldQueryElement[] = []
|
private _queries: CustomFieldQueryElement[] = []
|
||||||
|
private rootSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
|
public get queries(): CustomFieldQueryElement[] {
|
||||||
|
return this._queries
|
||||||
|
}
|
||||||
|
|
||||||
|
public set queries(value: CustomFieldQueryElement[]) {
|
||||||
|
this.teardownRootSubscriptions()
|
||||||
|
this._queries = value ?? []
|
||||||
|
for (const element of this._queries) {
|
||||||
|
this.rootSubscriptions.push(
|
||||||
|
element.changed.subscribe(() => {
|
||||||
|
this.changed.next(this)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public clear(fireEvent = true) {
|
public clear(fireEvent = true) {
|
||||||
this.queries = []
|
this.queries = []
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
@@ -107,14 +124,14 @@ export class CustomFieldQueriesModel {
|
|||||||
public addExpression(
|
public addExpression(
|
||||||
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
||||||
) {
|
) {
|
||||||
if (this.queries.length > 0) {
|
if (this.queries.length === 0) {
|
||||||
;(
|
this.queries = [expression]
|
||||||
(this.queries[0] as CustomFieldQueryExpression)
|
return
|
||||||
.value as CustomFieldQueryElement[]
|
|
||||||
).push(expression)
|
|
||||||
} else {
|
|
||||||
this.queries.push(expression)
|
|
||||||
}
|
}
|
||||||
|
;(
|
||||||
|
(this.queries[0] as CustomFieldQueryExpression)
|
||||||
|
.value as CustomFieldQueryElement[]
|
||||||
|
).push(expression)
|
||||||
expression.changed.subscribe(() => {
|
expression.changed.subscribe(() => {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
})
|
})
|
||||||
@@ -166,6 +183,13 @@ export class CustomFieldQueriesModel {
|
|||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private teardownRootSubscriptions() {
|
||||||
|
for (const subscription of this.rootSubscriptions) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
this.rootSubscriptions = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { fakeAsync, tick } from '@angular/core/testing'
|
|
||||||
import {
|
import {
|
||||||
CustomFieldQueryElementType,
|
CustomFieldQueryElementType,
|
||||||
CustomFieldQueryLogicalOperator,
|
CustomFieldQueryLogicalOperator,
|
||||||
@@ -111,13 +110,38 @@ describe('CustomFieldQueryAtom', () => {
|
|||||||
expect(atom.serialize()).toEqual([1, 'operator', 'value'])
|
expect(atom.serialize()).toEqual([1, 'operator', 'value'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should emit changed on value change after debounce', fakeAsync(() => {
|
it('should emit changed on value change immediately', () => {
|
||||||
const atom = new CustomFieldQueryAtom()
|
const atom = new CustomFieldQueryAtom()
|
||||||
const changeSpy = jest.spyOn(atom.changed, 'next')
|
const changeSpy = jest.spyOn(atom.changed, 'next')
|
||||||
atom.value = 'new value'
|
atom.value = 'new value'
|
||||||
tick(1000)
|
|
||||||
expect(changeSpy).toHaveBeenCalled()
|
expect(changeSpy).toHaveBeenCalled()
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
it('should ignore duplicate array emissions', () => {
|
||||||
|
const atom = new CustomFieldQueryAtom()
|
||||||
|
atom.operator = CustomFieldQueryOperator.In
|
||||||
|
const changeSpy = jest.fn()
|
||||||
|
atom.changed.subscribe(changeSpy)
|
||||||
|
|
||||||
|
atom.value = [1, 2]
|
||||||
|
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
changeSpy.mockClear()
|
||||||
|
atom.value = [1, 2]
|
||||||
|
expect(changeSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit when array values differ while length matches', () => {
|
||||||
|
const atom = new CustomFieldQueryAtom()
|
||||||
|
atom.operator = CustomFieldQueryOperator.In
|
||||||
|
const changeSpy = jest.fn()
|
||||||
|
atom.changed.subscribe(changeSpy)
|
||||||
|
|
||||||
|
atom.value = [1, 2]
|
||||||
|
changeSpy.mockClear()
|
||||||
|
atom.value = [1, 3]
|
||||||
|
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('CustomFieldQueryExpression', () => {
|
describe('CustomFieldQueryExpression', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
|
import { Subject, distinctUntilChanged } from 'rxjs'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import {
|
import {
|
||||||
CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR,
|
CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR,
|
||||||
@@ -110,7 +110,22 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
|
|||||||
|
|
||||||
protected override connectValueModelChanged(): void {
|
protected override connectValueModelChanged(): void {
|
||||||
this.valueModelChanged
|
this.valueModelChanged
|
||||||
.pipe(debounceTime(1000), distinctUntilChanged())
|
.pipe(
|
||||||
|
distinctUntilChanged((previous, current) => {
|
||||||
|
if (Array.isArray(previous) && Array.isArray(current)) {
|
||||||
|
if (previous.length !== current.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < previous.length; i++) {
|
||||||
|
if (previous[i] !== current[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return previous === current
|
||||||
|
})
|
||||||
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const environment = {
|
|||||||
apiVersion: '9', // match src/paperless/settings.py
|
apiVersion: '9', // match src/paperless/settings.py
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
tag: 'prod',
|
tag: 'prod',
|
||||||
version: '2.19.4',
|
version: '2.19.5',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||||
|
|||||||
@@ -764,7 +764,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">17</context>
|
<context context-type="linenumber">17</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">lines</target>
|
<target state="translated">línies</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8838884664569764142" datatype="html">
|
<trans-unit id="8838884664569764142" datatype="html">
|
||||||
<source>Auto refresh</source>
|
<source>Auto refresh</source>
|
||||||
@@ -8700,7 +8700,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">18</context>
|
<context context-type="linenumber">18</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Select:</target>
|
<target state="translated">Selecciona:</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6252070156626006029" datatype="html">
|
<trans-unit id="6252070156626006029" datatype="html">
|
||||||
<source>None</source>
|
<source>None</source>
|
||||||
|
|||||||
@@ -764,7 +764,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">17</context>
|
<context context-type="linenumber">17</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">lines</target>
|
<target state="translated">lignes</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8838884664569764142" datatype="html" approved="yes">
|
<trans-unit id="8838884664569764142" datatype="html" approved="yes">
|
||||||
<source>Auto refresh</source>
|
<source>Auto refresh</source>
|
||||||
@@ -8699,7 +8699,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">18</context>
|
<context context-type="linenumber">18</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Select:</target>
|
<target state="translated">Sélectionner :</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6252070156626006029" datatype="html" approved="yes">
|
<trans-unit id="6252070156626006029" datatype="html" approved="yes">
|
||||||
<source>None</source>
|
<source>None</source>
|
||||||
|
|||||||
@@ -8700,7 +8700,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">18</context>
|
<context context-type="linenumber">18</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Select:</target>
|
<target state="translated">選取:</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6252070156626006029" datatype="html">
|
<trans-unit id="6252070156626006029" datatype="html">
|
||||||
<source>None</source>
|
<source>None</source>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-28 18:07\n"
|
"PO-Revision-Date: 2025-11-04 12:15\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Catalan\n"
|
"Language-Team: Catalan\n"
|
||||||
"Language: ca_ES\n"
|
"Language: ca_ES\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||||
"PO-Revision-Date: 2025-11-03 17:49\n"
|
"PO-Revision-Date: 2025-11-06 00:35\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-29 12:14\n"
|
"PO-Revision-Date: 2025-11-06 12:14\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: French\n"
|
"Language-Team: French\n"
|
||||||
"Language: fr_FR\n"
|
"Language: fr_FR\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-28 18:07\n"
|
"PO-Revision-Date: 2025-11-06 00:35\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Traditional\n"
|
"Language-Team: Chinese Traditional\n"
|
||||||
"Language: zh_TW\n"
|
"Language: zh_TW\n"
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ INSTALLED_APPS = [
|
|||||||
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
||||||
"paperless_text.apps.PaperlessTextConfig",
|
"paperless_text.apps.PaperlessTextConfig",
|
||||||
"paperless_mail.apps.PaperlessMailConfig",
|
"paperless_mail.apps.PaperlessMailConfig",
|
||||||
|
"paperless_remote.apps.PaperlessRemoteParserConfig",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"rest_framework.authtoken",
|
"rest_framework.authtoken",
|
||||||
@@ -1401,3 +1402,10 @@ WEBHOOKS_ALLOW_INTERNAL_REQUESTS = __get_boolean(
|
|||||||
"PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS",
|
"PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS",
|
||||||
"true",
|
"true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Remote Parser #
|
||||||
|
###############################################################################
|
||||||
|
REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE")
|
||||||
|
REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY")
|
||||||
|
REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[tuple[int, int, int]] = (2, 19, 4)
|
__version__: Final[tuple[int, int, int]] = (2, 19, 5)
|
||||||
# Version string like X.Y.Z
|
# Version string like X.Y.Z
|
||||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||||
# Version string like X.Y
|
# Version string like X.Y
|
||||||
|
|||||||
4
src/paperless_remote/__init__.py
Normal file
4
src/paperless_remote/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# this is here so that django finds the checks.
|
||||||
|
from paperless_remote.checks import check_remote_parser_configured
|
||||||
|
|
||||||
|
__all__ = ["check_remote_parser_configured"]
|
||||||
14
src/paperless_remote/apps.py
Normal file
14
src/paperless_remote/apps.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from paperless_remote.signals import remote_consumer_declaration
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessRemoteParserConfig(AppConfig):
|
||||||
|
name = "paperless_remote"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from documents.signals import document_consumer_declaration
|
||||||
|
|
||||||
|
document_consumer_declaration.connect(remote_consumer_declaration)
|
||||||
|
|
||||||
|
AppConfig.ready(self)
|
||||||
17
src/paperless_remote/checks.py
Normal file
17
src/paperless_remote/checks.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.checks import Error
|
||||||
|
from django.core.checks import register
|
||||||
|
|
||||||
|
|
||||||
|
@register()
|
||||||
|
def check_remote_parser_configured(app_configs, **kwargs):
|
||||||
|
if settings.REMOTE_OCR_ENGINE == "azureai" and not (
|
||||||
|
settings.REMOTE_OCR_ENDPOINT and settings.REMOTE_OCR_API_KEY
|
||||||
|
):
|
||||||
|
return [
|
||||||
|
Error(
|
||||||
|
"Azure AI remote parser requires endpoint and API key to be configured.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return []
|
||||||
113
src/paperless_remote/parsers.py
Normal file
113
src/paperless_remote/parsers.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from paperless_tesseract.parsers import RasterisedDocumentParser
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteEngineConfig:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
engine: str,
|
||||||
|
api_key: str | None = None,
|
||||||
|
endpoint: str | None = None,
|
||||||
|
):
|
||||||
|
self.engine = engine
|
||||||
|
self.api_key = api_key
|
||||||
|
self.endpoint = endpoint
|
||||||
|
|
||||||
|
def engine_is_valid(self):
|
||||||
|
valid = self.engine in ["azureai"] and self.api_key is not None
|
||||||
|
if self.engine == "azureai":
|
||||||
|
valid = valid and self.endpoint is not None
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteDocumentParser(RasterisedDocumentParser):
|
||||||
|
"""
|
||||||
|
This parser uses a remote OCR engine to parse documents. Currently, it supports Azure AI Vision
|
||||||
|
as this is the only service that provides a remote OCR API with text-embedded PDF output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logging_name = "paperless.parsing.remote"
|
||||||
|
|
||||||
|
def get_settings(self) -> RemoteEngineConfig:
|
||||||
|
"""
|
||||||
|
Returns the configuration for the remote OCR engine, loaded from Django settings.
|
||||||
|
"""
|
||||||
|
return RemoteEngineConfig(
|
||||||
|
engine=settings.REMOTE_OCR_ENGINE,
|
||||||
|
api_key=settings.REMOTE_OCR_API_KEY,
|
||||||
|
endpoint=settings.REMOTE_OCR_ENDPOINT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def supported_mime_types(self):
|
||||||
|
if self.settings.engine_is_valid():
|
||||||
|
return {
|
||||||
|
"application/pdf": ".pdf",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/tiff": ".tiff",
|
||||||
|
"image/bmp": ".bmp",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def azure_ai_vision_parse(
|
||||||
|
self,
|
||||||
|
file: Path,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Uses Azure AI Vision to parse the document and return the text content.
|
||||||
|
It requests a searchable PDF output with embedded text.
|
||||||
|
The PDF is saved to the archive_path attribute.
|
||||||
|
Returns the text content extracted from the document.
|
||||||
|
If the parsing fails, it returns None.
|
||||||
|
"""
|
||||||
|
from azure.ai.documentintelligence import DocumentIntelligenceClient
|
||||||
|
from azure.ai.documentintelligence.models import AnalyzeDocumentRequest
|
||||||
|
from azure.ai.documentintelligence.models import AnalyzeOutputOption
|
||||||
|
from azure.ai.documentintelligence.models import DocumentContentFormat
|
||||||
|
from azure.core.credentials import AzureKeyCredential
|
||||||
|
|
||||||
|
client = DocumentIntelligenceClient(
|
||||||
|
endpoint=self.settings.endpoint,
|
||||||
|
credential=AzureKeyCredential(self.settings.api_key),
|
||||||
|
)
|
||||||
|
|
||||||
|
with file.open("rb") as f:
|
||||||
|
analyze_request = AnalyzeDocumentRequest(bytes_source=f.read())
|
||||||
|
poller = client.begin_analyze_document(
|
||||||
|
model_id="prebuilt-read",
|
||||||
|
body=analyze_request,
|
||||||
|
output_content_format=DocumentContentFormat.TEXT,
|
||||||
|
output=[AnalyzeOutputOption.PDF], # request searchable PDF output
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
poller.wait()
|
||||||
|
result_id = poller.details["operation_id"]
|
||||||
|
result = poller.result()
|
||||||
|
|
||||||
|
# Download the PDF with embedded text
|
||||||
|
self.archive_path = self.tempdir / "archive.pdf"
|
||||||
|
with self.archive_path.open("wb") as f:
|
||||||
|
for chunk in client.get_analyze_result_pdf(
|
||||||
|
model_id="prebuilt-read",
|
||||||
|
result_id=result_id,
|
||||||
|
):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return result.content
|
||||||
|
|
||||||
|
def parse(self, document_path: Path, mime_type, file_name=None):
|
||||||
|
if not self.settings.engine_is_valid():
|
||||||
|
self.log.warning(
|
||||||
|
"No valid remote parser engine is configured, content will be empty.",
|
||||||
|
)
|
||||||
|
self.text = ""
|
||||||
|
elif self.settings.engine == "azureai":
|
||||||
|
self.text = self.azure_ai_vision_parse(document_path)
|
||||||
18
src/paperless_remote/signals.py
Normal file
18
src/paperless_remote/signals.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
def get_parser(*args, **kwargs):
|
||||||
|
from paperless_remote.parsers import RemoteDocumentParser
|
||||||
|
|
||||||
|
return RemoteDocumentParser(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_supported_mime_types():
|
||||||
|
from paperless_remote.parsers import RemoteDocumentParser
|
||||||
|
|
||||||
|
return RemoteDocumentParser(None).supported_mime_types()
|
||||||
|
|
||||||
|
|
||||||
|
def remote_consumer_declaration(sender, **kwargs):
|
||||||
|
return {
|
||||||
|
"parser": get_parser,
|
||||||
|
"weight": 5,
|
||||||
|
"mime_types": get_supported_mime_types(),
|
||||||
|
}
|
||||||
0
src/paperless_remote/tests/__init__.py
Normal file
0
src/paperless_remote/tests/__init__.py
Normal file
BIN
src/paperless_remote/tests/samples/simple-digital.pdf
Normal file
BIN
src/paperless_remote/tests/samples/simple-digital.pdf
Normal file
Binary file not shown.
24
src/paperless_remote/tests/test_checks.py
Normal file
24
src/paperless_remote/tests/test_checks.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from paperless_remote import check_remote_parser_configured
|
||||||
|
|
||||||
|
|
||||||
|
class TestChecks(TestCase):
|
||||||
|
@override_settings(REMOTE_OCR_ENGINE=None)
|
||||||
|
def test_no_engine(self):
|
||||||
|
msgs = check_remote_parser_configured(None)
|
||||||
|
self.assertEqual(len(msgs), 0)
|
||||||
|
|
||||||
|
@override_settings(REMOTE_OCR_ENGINE="azureai")
|
||||||
|
@override_settings(REMOTE_OCR_API_KEY="somekey")
|
||||||
|
@override_settings(REMOTE_OCR_ENDPOINT=None)
|
||||||
|
def test_azure_no_endpoint(self):
|
||||||
|
msgs = check_remote_parser_configured(None)
|
||||||
|
self.assertEqual(len(msgs), 1)
|
||||||
|
self.assertTrue(
|
||||||
|
msgs[0].msg.startswith(
|
||||||
|
"Azure AI remote parser requires endpoint and API key to be configured.",
|
||||||
|
),
|
||||||
|
)
|
||||||
101
src/paperless_remote/tests/test_parser.py
Normal file
101
src/paperless_remote/tests/test_parser.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_remote.parsers import RemoteDocumentParser
|
||||||
|
from paperless_remote.signals import get_parser
|
||||||
|
|
||||||
|
|
||||||
|
class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
|
SAMPLE_FILES = Path(__file__).resolve().parent / "samples"
|
||||||
|
|
||||||
|
def assertContainsStrings(self, content: str, strings: list[str]):
|
||||||
|
# Asserts that all strings appear in content, in the given order.
|
||||||
|
indices = []
|
||||||
|
for s in strings:
|
||||||
|
if s in content:
|
||||||
|
indices.append(content.index(s))
|
||||||
|
else:
|
||||||
|
self.fail(f"'{s}' is not in '{content}'")
|
||||||
|
self.assertListEqual(indices, sorted(indices))
|
||||||
|
|
||||||
|
@mock.patch("paperless_tesseract.parsers.run_subprocess")
|
||||||
|
@mock.patch("azure.ai.documentintelligence.DocumentIntelligenceClient")
|
||||||
|
def test_get_text_with_azure(self, mock_client_cls, mock_subprocess):
|
||||||
|
# Arrange mock Azure client
|
||||||
|
mock_client = mock.Mock()
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
# Simulate poller result and its `.details`
|
||||||
|
mock_poller = mock.Mock()
|
||||||
|
mock_poller.wait.return_value = None
|
||||||
|
mock_poller.details = {"operation_id": "fake-op-id"}
|
||||||
|
mock_client.begin_analyze_document.return_value = mock_poller
|
||||||
|
mock_poller.result.return_value.content = "This is a test document."
|
||||||
|
|
||||||
|
# Return dummy PDF bytes
|
||||||
|
mock_client.get_analyze_result_pdf.return_value = [
|
||||||
|
b"%PDF-",
|
||||||
|
b"1.7 ",
|
||||||
|
b"FAKEPDF",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Simulate pdftotext by writing dummy text to sidecar file
|
||||||
|
def fake_run(cmd, *args, **kwargs):
|
||||||
|
with Path(cmd[-1]).open("w", encoding="utf-8") as f:
|
||||||
|
f.write("This is a test document.")
|
||||||
|
|
||||||
|
mock_subprocess.side_effect = fake_run
|
||||||
|
|
||||||
|
with override_settings(
|
||||||
|
REMOTE_OCR_ENGINE="azureai",
|
||||||
|
REMOTE_OCR_API_KEY="somekey",
|
||||||
|
REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com",
|
||||||
|
):
|
||||||
|
parser = get_parser(uuid.uuid4())
|
||||||
|
parser.parse(
|
||||||
|
self.SAMPLE_FILES / "simple-digital.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContainsStrings(
|
||||||
|
parser.text.strip(),
|
||||||
|
["This is a test document."],
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
REMOTE_OCR_ENGINE="azureai",
|
||||||
|
REMOTE_OCR_API_KEY="key",
|
||||||
|
REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com",
|
||||||
|
)
|
||||||
|
def test_supported_mime_types_valid_config(self):
|
||||||
|
parser = RemoteDocumentParser(uuid.uuid4())
|
||||||
|
expected_types = {
|
||||||
|
"application/pdf": ".pdf",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/tiff": ".tiff",
|
||||||
|
"image/bmp": ".bmp",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
}
|
||||||
|
self.assertEqual(parser.supported_mime_types(), expected_types)
|
||||||
|
|
||||||
|
def test_supported_mime_types_invalid_config(self):
|
||||||
|
parser = get_parser(uuid.uuid4())
|
||||||
|
self.assertEqual(parser.supported_mime_types(), {})
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
REMOTE_OCR_ENGINE=None,
|
||||||
|
REMOTE_OCR_API_KEY=None,
|
||||||
|
REMOTE_OCR_ENDPOINT=None,
|
||||||
|
)
|
||||||
|
def test_parse_with_invalid_config(self):
|
||||||
|
parser = get_parser(uuid.uuid4())
|
||||||
|
parser.parse(self.SAMPLE_FILES / "simple-digital.pdf", "application/pdf")
|
||||||
|
self.assertEqual(parser.text, "")
|
||||||
41
uv.lock
generated
41
uv.lock
generated
@@ -95,6 +95,34 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "azure-ai-documentintelligence"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "azure-core"
|
||||||
|
version = "1.33.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "babel"
|
name = "babel"
|
||||||
version = "2.17.0"
|
version = "2.17.0"
|
||||||
@@ -1451,6 +1479,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isodate"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.6"
|
||||||
@@ -2115,9 +2152,10 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.19.4"
|
version = "2.19.5"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -2254,6 +2292,7 @@ typing = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "azure-ai-documentintelligence", specifier = ">=1.0.2" },
|
||||||
{ name = "babel", specifier = ">=2.17" },
|
{ name = "babel", specifier = ">=2.17" },
|
||||||
{ name = "bleach", specifier = "~=6.2.0" },
|
{ name = "bleach", specifier = "~=6.2.0" },
|
||||||
{ name = "celery", extras = ["redis"], specifier = "~=5.5.1" },
|
{ name = "celery", extras = ["redis"], specifier = "~=5.5.1" },
|
||||||
|
|||||||
Reference in New Issue
Block a user