mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-24 00:59:35 -06:00
Compare commits
13 Commits
dependabot
...
feature-da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02d4fcfe0b | ||
|
|
5db46cbc3a | ||
|
|
3c9bbb50b3 | ||
|
|
480a406e8a | ||
|
|
b26546c806 | ||
|
|
7dc5fdc1d8 | ||
|
|
bd1dce2412 | ||
|
|
e9de03303e | ||
|
|
d0164e68cf | ||
|
|
814f57b099 | ||
|
|
be7f1c6233 | ||
|
|
d6cd6d0311 | ||
|
|
095ea3cbd3 |
@@ -39,3 +39,6 @@ max_line_length = off
|
|||||||
|
|
||||||
[Dockerfile*]
|
[Dockerfile*]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
[*.toml]
|
||||||
|
indent_style = space
|
||||||
|
|||||||
2
.github/workflows/ci-backend.yml
vendored
2
.github/workflows/ci-backend.yml
vendored
@@ -129,6 +129,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv pip list
|
uv pip list
|
||||||
- name: Check typing (pyrefly)
|
- name: Check typing (pyrefly)
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
uv run pyrefly \
|
uv run pyrefly \
|
||||||
check \
|
check \
|
||||||
@@ -143,6 +144,7 @@ jobs:
|
|||||||
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
||||||
${{ runner.os }}-mypy-
|
${{ runner.os }}-mypy-
|
||||||
- name: Check typing (mypy)
|
- name: Check typing (mypy)
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
uv run mypy \
|
uv run mypy \
|
||||||
--show-error-codes \
|
--show-error-codes \
|
||||||
|
|||||||
@@ -784,9 +784,17 @@ below.
|
|||||||
|
|
||||||
### Document Splitting {#document-splitting}
|
### Document Splitting {#document-splitting}
|
||||||
|
|
||||||
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
If document splitting is enabled, Paperless splits _after_ a separator barcode by default.
|
||||||
starting from the next page. The page with the barcode on it will _not_ be retained. It
|
This means:
|
||||||
is expected to be a page existing only for triggering the split.
|
|
||||||
|
- any page containing the configured separator barcode starts a new document, starting with the **next** page
|
||||||
|
- pages containing the separator barcode are discarded
|
||||||
|
|
||||||
|
This is intended for dedicated separator sheets such as PATCH-T pages.
|
||||||
|
|
||||||
|
If [`PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES`](configuration.md#PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES)
|
||||||
|
is enabled, the page containing the separator barcode is retained instead. In this mode,
|
||||||
|
each page containing the separator barcode becomes the **first** page of a new document.
|
||||||
|
|
||||||
### Archive Serial Number Assignment
|
### Archive Serial Number Assignment
|
||||||
|
|
||||||
@@ -795,8 +803,9 @@ archive serial number, allowing quick reference back to the original, paper docu
|
|||||||
|
|
||||||
If document splitting via barcode is also enabled, documents will be split when an ASN
|
If document splitting via barcode is also enabled, documents will be split when an ASN
|
||||||
barcode is located. However, differing from the splitting, the page with the
|
barcode is located. However, differing from the splitting, the page with the
|
||||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
barcode _will_ be retained. Each detected ASN barcode starts a new document _starting with
|
||||||
one which holds data to keep in the document.
|
that page_. This allows placing ASN barcodes on content pages that should remain part of
|
||||||
|
the document.
|
||||||
|
|
||||||
### Tag Assignment
|
### Tag Assignment
|
||||||
|
|
||||||
|
|||||||
@@ -51,137 +51,163 @@ matcher.
|
|||||||
### Database
|
### Database
|
||||||
|
|
||||||
By default, Paperless uses **SQLite** with a database stored at `data/db.sqlite3`.
|
By default, Paperless uses **SQLite** with a database stored at `data/db.sqlite3`.
|
||||||
To switch to **PostgreSQL** or **MariaDB**, set [`PAPERLESS_DBHOST`](#PAPERLESS_DBHOST) and optionally configure other
|
For multi-user or higher-throughput deployments, **PostgreSQL** (recommended) or
|
||||||
database-related environment variables.
|
**MariaDB** can be used instead by setting [`PAPERLESS_DBENGINE`](#PAPERLESS_DBENGINE)
|
||||||
|
and the relevant connection variables.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_DBENGINE=<engine>`](#PAPERLESS_DBENGINE) {#PAPERLESS_DBENGINE}
|
||||||
|
|
||||||
|
: Specifies the database engine to use. Accepted values are `sqlite`, `postgresql`,
|
||||||
|
and `mariadb`.
|
||||||
|
|
||||||
|
Defaults to `sqlite` if not set.
|
||||||
|
|
||||||
|
PostgreSQL and MariaDB both require [`PAPERLESS_DBHOST`](#PAPERLESS_DBHOST) to be
|
||||||
|
set. SQLite does not use any other connection variables; the database file is always
|
||||||
|
located at `<PAPERLESS_DATA_DIR>/db.sqlite3`.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Using MariaDB comes with some caveats.
|
||||||
|
See [MySQL Caveats](advanced_usage.md#mysql-caveats).
|
||||||
|
|
||||||
#### [`PAPERLESS_DBHOST=<hostname>`](#PAPERLESS_DBHOST) {#PAPERLESS_DBHOST}
|
#### [`PAPERLESS_DBHOST=<hostname>`](#PAPERLESS_DBHOST) {#PAPERLESS_DBHOST}
|
||||||
|
|
||||||
: If unset, Paperless uses **SQLite** by default.
|
: Hostname of the PostgreSQL or MariaDB database server. Required when
|
||||||
|
`PAPERLESS_DBENGINE` is `postgresql` or `mariadb`.
|
||||||
Set `PAPERLESS_DBHOST` to switch to PostgreSQL or MariaDB instead.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DBENGINE=<engine_name>`](#PAPERLESS_DBENGINE) {#PAPERLESS_DBENGINE}
|
|
||||||
|
|
||||||
: Optional. Specifies the database engine to use when connecting to a remote database.
|
|
||||||
Available options are `postgresql` and `mariadb`.
|
|
||||||
|
|
||||||
Defaults to `postgresql` if `PAPERLESS_DBHOST` is set.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
|
|
||||||
Using MariaDB comes with some caveats. See [MySQL Caveats](advanced_usage.md#mysql-caveats).
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DBPORT=<port>`](#PAPERLESS_DBPORT) {#PAPERLESS_DBPORT}
|
#### [`PAPERLESS_DBPORT=<port>`](#PAPERLESS_DBPORT) {#PAPERLESS_DBPORT}
|
||||||
|
|
||||||
: Port to use when connecting to PostgreSQL or MariaDB.
|
: Port to use when connecting to PostgreSQL or MariaDB.
|
||||||
|
|
||||||
Default is `5432` for PostgreSQL and `3306` for MariaDB.
|
Defaults to `5432` for PostgreSQL and `3306` for MariaDB.
|
||||||
|
|
||||||
#### [`PAPERLESS_DBNAME=<name>`](#PAPERLESS_DBNAME) {#PAPERLESS_DBNAME}
|
#### [`PAPERLESS_DBNAME=<name>`](#PAPERLESS_DBNAME) {#PAPERLESS_DBNAME}
|
||||||
|
|
||||||
: Name of the database to connect to when using PostgreSQL or MariaDB.
|
: Name of the PostgreSQL or MariaDB database to connect to.
|
||||||
|
|
||||||
Defaults to "paperless".
|
Defaults to `paperless`.
|
||||||
|
|
||||||
#### [`PAPERLESS_DBUSER=<name>`](#PAPERLESS_DBUSER) {#PAPERLESS_DBUSER}
|
#### [`PAPERLESS_DBUSER=<user>`](#PAPERLESS_DBUSER) {#PAPERLESS_DBUSER}
|
||||||
|
|
||||||
: Username for authenticating with the PostgreSQL or MariaDB database.
|
: Username for authenticating with the PostgreSQL or MariaDB database.
|
||||||
|
|
||||||
Defaults to "paperless".
|
Defaults to `paperless`.
|
||||||
|
|
||||||
#### [`PAPERLESS_DBPASS=<password>`](#PAPERLESS_DBPASS) {#PAPERLESS_DBPASS}
|
#### [`PAPERLESS_DBPASS=<password>`](#PAPERLESS_DBPASS) {#PAPERLESS_DBPASS}
|
||||||
|
|
||||||
: Password for the PostgreSQL or MariaDB database user.
|
: Password for the PostgreSQL or MariaDB database user.
|
||||||
|
|
||||||
Defaults to "paperless".
|
Defaults to `paperless`.
|
||||||
|
|
||||||
#### [`PAPERLESS_DBSSLMODE=<mode>`](#PAPERLESS_DBSSLMODE) {#PAPERLESS_DBSSLMODE}
|
#### [`PAPERLESS_DB_OPTIONS=<options>`](#PAPERLESS_DB_OPTIONS) {#PAPERLESS_DB_OPTIONS}
|
||||||
|
|
||||||
: SSL mode to use when connecting to PostgreSQL or MariaDB.
|
: Advanced database connection options as a semicolon-delimited key-value string.
|
||||||
|
Keys and values are separated by `=`. Dot-notation produces nested option
|
||||||
|
dictionaries; for example, `pool.max_size=20` sets
|
||||||
|
`OPTIONS["pool"]["max_size"] = 20`.
|
||||||
|
|
||||||
See [the official documentation about
|
Options specified here are merged over the engine defaults. Unrecognised keys
|
||||||
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
are passed through to the underlying database driver without validation, so a
|
||||||
|
typo will be silently ignored rather than producing an error.
|
||||||
|
|
||||||
See [the official documentation about
|
Refer to your database driver's documentation for the full set of accepted keys:
|
||||||
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode).
|
|
||||||
|
|
||||||
*Note*: SSL mode values differ between PostgreSQL and MariaDB.
|
- PostgreSQL: [libpq connection parameters](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)
|
||||||
|
- MariaDB: [MariaDB Connector/Python](https://mariadb.com/kb/en/mariadb-connector-python/)
|
||||||
|
- SQLite: [SQLite PRAGMA statements](https://www.sqlite.org/pragma.html)
|
||||||
|
|
||||||
Default is `prefer` for PostgreSQL and `PREFERRED` for MariaDB.
|
**Examples:**
|
||||||
|
|
||||||
#### [`PAPERLESS_DBSSLROOTCERT=<ca-path>`](#PAPERLESS_DBSSLROOTCERT) {#PAPERLESS_DBSSLROOTCERT}
|
```bash
|
||||||
|
# PostgreSQL: require SSL, set a custom CA certificate, and limit the pool size
|
||||||
|
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=5"
|
||||||
|
|
||||||
: Path to the SSL root certificate used to verify the database server.
|
# MariaDB: require SSL with a custom CA certificate
|
||||||
|
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED;ssl.ca=/certs/ca.pem"
|
||||||
|
|
||||||
See [the official documentation about
|
# PostgreSQL: set a connection timeout
|
||||||
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
PAPERLESS_DB_OPTIONS="connect_timeout=10"
|
||||||
Changes the location of `root.crt`.
|
```
|
||||||
|
|
||||||
See [the official documentation about
|
!!! note "PostgreSQL connection pooling"
|
||||||
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-ca).
|
Pool size is controlled via `pool.min_size` and `pool.max_size`. When
|
||||||
|
configuring pooling, ensure your PostgreSQL `max_connections` is large enough
|
||||||
|
to handle all pool connections across all workers:
|
||||||
|
`(web_workers + celery_workers) * pool.max_size + safety_margin`.
|
||||||
|
|
||||||
Defaults to unset, using the standard location in the home directory.
|
#### ~~[`PAPERLESS_DBSSLMODE`](#PAPERLESS_DBSSLMODE)~~ {#PAPERLESS_DBSSLMODE}
|
||||||
|
|
||||||
#### [`PAPERLESS_DBSSLCERT=<client-cert-path>`](#PAPERLESS_DBSSLCERT) {#PAPERLESS_DBSSLCERT}
|
!!! failure "Removed in v3"
|
||||||
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
: Path to the client SSL certificate used when connecting securely.
|
```bash
|
||||||
|
# PostgreSQL
|
||||||
|
PAPERLESS_DB_OPTIONS="sslmode=require"
|
||||||
|
|
||||||
See [the official documentation about
|
# MariaDB
|
||||||
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED"
|
||||||
|
```
|
||||||
|
|
||||||
See [the official documentation about
|
#### ~~[`PAPERLESS_DBSSLROOTCERT`](#PAPERLESS_DBSSLROOTCERT)~~ {#PAPERLESS_DBSSLROOTCERT}
|
||||||
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-cert).
|
|
||||||
|
|
||||||
Changes the location of `postgresql.crt`.
|
!!! failure "Removed in v3"
|
||||||
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
Defaults to unset, using the standard location in the home directory.
|
```bash
|
||||||
|
# PostgreSQL
|
||||||
|
PAPERLESS_DB_OPTIONS="sslrootcert=/path/to/ca.pem"
|
||||||
|
|
||||||
#### [`PAPERLESS_DBSSLKEY=<client-cert-key>`](#PAPERLESS_DBSSLKEY) {#PAPERLESS_DBSSLKEY}
|
# MariaDB
|
||||||
|
PAPERLESS_DB_OPTIONS="ssl.ca=/path/to/ca.pem"
|
||||||
|
```
|
||||||
|
|
||||||
: Path to the client SSL private key used when connecting securely.
|
#### ~~[`PAPERLESS_DBSSLCERT`](#PAPERLESS_DBSSLCERT)~~ {#PAPERLESS_DBSSLCERT}
|
||||||
|
|
||||||
See [the official documentation about
|
!!! failure "Removed in v3"
|
||||||
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
See [the official documentation about
|
```bash
|
||||||
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-key).
|
# PostgreSQL
|
||||||
|
PAPERLESS_DB_OPTIONS="sslcert=/path/to/client.crt"
|
||||||
|
|
||||||
Changes the location of `postgresql.key`.
|
# MariaDB
|
||||||
|
PAPERLESS_DB_OPTIONS="ssl.cert=/path/to/client.crt"
|
||||||
|
```
|
||||||
|
|
||||||
Defaults to unset, using the standard location in the home directory.
|
#### ~~[`PAPERLESS_DBSSLKEY`](#PAPERLESS_DBSSLKEY)~~ {#PAPERLESS_DBSSLKEY}
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_TIMEOUT=<int>`](#PAPERLESS_DB_TIMEOUT) {#PAPERLESS_DB_TIMEOUT}
|
!!! failure "Removed in v3"
|
||||||
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
: Sets how long a database connection should wait before timing out.
|
```bash
|
||||||
|
# PostgreSQL
|
||||||
|
PAPERLESS_DB_OPTIONS="sslkey=/path/to/client.key"
|
||||||
|
|
||||||
For SQLite, this sets how long to wait if the database is locked.
|
# MariaDB
|
||||||
For PostgreSQL or MariaDB, this sets the connection timeout.
|
PAPERLESS_DB_OPTIONS="ssl.key=/path/to/client.key"
|
||||||
|
```
|
||||||
|
|
||||||
Defaults to unset, which uses Django’s built-in defaults.
|
#### ~~[`PAPERLESS_DB_TIMEOUT`](#PAPERLESS_DB_TIMEOUT)~~ {#PAPERLESS_DB_TIMEOUT}
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
|
!!! failure "Removed in v3"
|
||||||
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
: Defines the maximum number of database connections to keep in the pool.
|
```bash
|
||||||
|
# SQLite
|
||||||
|
PAPERLESS_DB_OPTIONS="timeout=30"
|
||||||
|
|
||||||
Only applies to PostgreSQL. This setting is ignored for other database engines.
|
# PostgreSQL or MariaDB
|
||||||
|
PAPERLESS_DB_OPTIONS="connect_timeout=30"
|
||||||
|
```
|
||||||
|
|
||||||
The value must be greater than or equal to 1 to be used.
|
#### ~~[`PAPERLESS_DB_POOLSIZE`](#PAPERLESS_DB_POOLSIZE)~~ {#PAPERLESS_DB_POOLSIZE}
|
||||||
Defaults to unset, which disables connection pooling.
|
|
||||||
|
|
||||||
!!! note
|
!!! failure "Removed in v3"
|
||||||
|
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
|
||||||
|
|
||||||
A pool of 8-10 connections per worker is typically sufficient.
|
```bash
|
||||||
If you encounter error messages such as `couldn't get a connection`
|
PAPERLESS_DB_OPTIONS="pool.max_size=10"
|
||||||
or database connection timeouts, you probably need to increase the pool size.
|
```
|
||||||
|
|
||||||
!!! warning
|
|
||||||
Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools:
|
|
||||||
`(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with
|
|
||||||
4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`,
|
|
||||||
so `max_connections = 60` (or even more) is appropriate.
|
|
||||||
|
|
||||||
This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications,
|
|
||||||
you should increase `max_connections` accordingly.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,58 @@ The `CONSUMER_BARCODE_SCANNER` setting has been removed. zxing-cpp is now the on
|
|||||||
reliability.
|
reliability.
|
||||||
- The `libzbar0` / `libzbar-dev` system packages are no longer required and can be removed from any custom Docker
|
- The `libzbar0` / `libzbar-dev` system packages are no longer required and can be removed from any custom Docker
|
||||||
images or host installations.
|
images or host installations.
|
||||||
|
|
||||||
|
## Database Engine
|
||||||
|
|
||||||
|
`PAPERLESS_DBENGINE` is now required to use PostgreSQL or MariaDB. Previously, the
|
||||||
|
engine was inferred from the presence of `PAPERLESS_DBHOST`, with `PAPERLESS_DBENGINE`
|
||||||
|
only needed to select MariaDB over PostgreSQL.
|
||||||
|
|
||||||
|
SQLite users require no changes, though they may explicitly set their engine if desired.
|
||||||
|
|
||||||
|
#### Action Required
|
||||||
|
|
||||||
|
PostgreSQL and MariaDB users must add `PAPERLESS_DBENGINE` to their environment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# v2 (PostgreSQL inferred from PAPERLESS_DBHOST)
|
||||||
|
PAPERLESS_DBHOST: postgres
|
||||||
|
|
||||||
|
# v3 (engine must be explicit)
|
||||||
|
PAPERLESS_DBENGINE: postgresql
|
||||||
|
PAPERLESS_DBHOST: postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) for accepted values.
|
||||||
|
|
||||||
|
## Database Advanced Options
|
||||||
|
|
||||||
|
The individual SSL, timeout, and pooling variables have been removed in favour of a
|
||||||
|
single [`PAPERLESS_DB_OPTIONS`](configuration.md#PAPERLESS_DB_OPTIONS) string. This
|
||||||
|
consolidates a growing set of engine-specific variables into one place, and allows
|
||||||
|
any option supported by the underlying database driver to be set without requiring a
|
||||||
|
dedicated environment variable for each.
|
||||||
|
|
||||||
|
The removed variables and their replacements are:
|
||||||
|
|
||||||
|
| Removed Variable | Replacement in `PAPERLESS_DB_OPTIONS` |
|
||||||
|
| ------------------------- | ---------------------------------------------------------------------------- |
|
||||||
|
| `PAPERLESS_DBSSLMODE` | `sslmode=<value>` (PostgreSQL) or `ssl_mode=<value>` (MariaDB) |
|
||||||
|
| `PAPERLESS_DBSSLROOTCERT` | `sslrootcert=<path>` (PostgreSQL) or `ssl.ca=<path>` (MariaDB) |
|
||||||
|
| `PAPERLESS_DBSSLCERT` | `sslcert=<path>` (PostgreSQL) or `ssl.cert=<path>` (MariaDB) |
|
||||||
|
| `PAPERLESS_DBSSLKEY` | `sslkey=<path>` (PostgreSQL) or `ssl.key=<path>` (MariaDB) |
|
||||||
|
| `PAPERLESS_DB_POOLSIZE` | `pool.max_size=<value>` (PostgreSQL only) |
|
||||||
|
| `PAPERLESS_DB_TIMEOUT` | `timeout=<value>` (SQLite) or `connect_timeout=<value>` (PostgreSQL/MariaDB) |
|
||||||
|
|
||||||
|
The deprecated variables will continue to function for now but will be removed in a
|
||||||
|
future release. A deprecation warning is logged at startup for each deprecated variable
|
||||||
|
that is still set.
|
||||||
|
|
||||||
|
#### Action Required
|
||||||
|
|
||||||
|
Users with any of the deprecated variables set should migrate to `PAPERLESS_DB_OPTIONS`.
|
||||||
|
Multiple options are combined in a single value:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=10"
|
||||||
|
```
|
||||||
|
|||||||
@@ -8490,7 +8490,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">315</context>
|
<context context-type="linenumber">323</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
||||||
@@ -8505,7 +8505,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">308</context>
|
<context context-type="linenumber">316</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
||||||
@@ -8771,49 +8771,49 @@
|
|||||||
<source>Reset filters / selection</source>
|
<source>Reset filters / selection</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">304</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4135055128446167640" datatype="html">
|
<trans-unit id="4135055128446167640" datatype="html">
|
||||||
<source>Open first [selected] document</source>
|
<source>Open first [selected] document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">324</context>
|
<context context-type="linenumber">332</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3629960544875360046" datatype="html">
|
<trans-unit id="3629960544875360046" datatype="html">
|
||||||
<source>Previous page</source>
|
<source>Previous page</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">340</context>
|
<context context-type="linenumber">348</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3337301694210287595" datatype="html">
|
<trans-unit id="3337301694210287595" datatype="html">
|
||||||
<source>Next page</source>
|
<source>Next page</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">352</context>
|
<context context-type="linenumber">360</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2155249406916744630" datatype="html">
|
<trans-unit id="2155249406916744630" datatype="html">
|
||||||
<source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
|
<source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">385</context>
|
<context context-type="linenumber">393</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4646273665293421938" datatype="html">
|
<trans-unit id="4646273665293421938" datatype="html">
|
||||||
<source>Failed to save view "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>".</source>
|
<source>Failed to save view "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">391</context>
|
<context context-type="linenumber">399</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6837554170707123455" datatype="html">
|
<trans-unit id="6837554170707123455" datatype="html">
|
||||||
<source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
|
<source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">437</context>
|
<context context-type="linenumber">445</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="739880801667335279" datatype="html">
|
<trans-unit id="739880801667335279" datatype="html">
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
||||||
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||||
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -147,21 +147,21 @@ describe('DocumentListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show score sort fields on fulltext queries', () => {
|
it('should show score sort fields on fulltext queries', () => {
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_HAS_TAGS_ANY,
|
rule_type: FILTER_HAS_TAGS_ANY,
|
||||||
value: '10',
|
value: '10',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
||||||
|
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_FULLTEXT_QUERY,
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
value: 'foo',
|
value: 'foo',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(
|
expect(component.getSortFields()).toEqual(
|
||||||
documentListService.sortFieldsFullText
|
documentListService.sortFieldsFullText
|
||||||
@@ -170,12 +170,12 @@ describe('DocumentListComponent', () => {
|
|||||||
|
|
||||||
it('should determine if filtered, support reset', () => {
|
it('should determine if filtered, support reset', () => {
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_HAS_TAGS_ANY,
|
rule_type: FILTER_HAS_TAGS_ANY,
|
||||||
value: '10',
|
value: '10',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
documentListService.isReloading = false
|
documentListService.isReloading = false
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.isFiltered).toBeTruthy()
|
expect(component.isFiltered).toBeTruthy()
|
||||||
@@ -185,6 +185,20 @@ describe('DocumentListComponent', () => {
|
|||||||
expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(1)
|
expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should apply filter rule changes via list service', () => {
|
||||||
|
const setFilterRulesSpy = jest.spyOn(documentListService, 'setFilterRules')
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '10' }]
|
||||||
|
component.onFilterRulesChange(rules)
|
||||||
|
expect(setFilterRulesSpy).toHaveBeenCalledWith(rules)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset filter rules to page one via list service', () => {
|
||||||
|
const setFilterRulesSpy = jest.spyOn(documentListService, 'setFilterRules')
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '10' }]
|
||||||
|
component.onFilterRulesReset(rules)
|
||||||
|
expect(setFilterRulesSpy).toHaveBeenCalledWith(rules, true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should load saved view from URL', () => {
|
it('should load saved view from URL', () => {
|
||||||
const view: SavedView = {
|
const view: SavedView = {
|
||||||
id: 10,
|
id: 10,
|
||||||
@@ -217,7 +231,7 @@ describe('DocumentListComponent', () => {
|
|||||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||||
.mockReturnValue(of(convertToParamMap(queryParams)))
|
.mockReturnValue(of(convertToParamMap(queryParams)))
|
||||||
activatedRoute.snapshot.queryParams = queryParams
|
activatedRoute.snapshot.queryParams = queryParams
|
||||||
fixture.detectChanges()
|
component.ngOnInit()
|
||||||
expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
|
expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
|
||||||
expect(activateSavedViewSpy).toHaveBeenCalledWith(
|
expect(activateSavedViewSpy).toHaveBeenCalledWith(
|
||||||
view,
|
view,
|
||||||
|
|||||||
@@ -212,6 +212,14 @@ export class DocumentListComponent
|
|||||||
this.list.setSort(event.column, event.reverse)
|
this.list.setSort(event.column, event.reverse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFilterRulesChange(filterRules: FilterRule[]) {
|
||||||
|
this.list.setFilterRules(filterRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterRulesReset(filterRules: FilterRule[]) {
|
||||||
|
this.list.setFilterRules(filterRules, true)
|
||||||
|
}
|
||||||
|
|
||||||
get isBulkEditing(): boolean {
|
get isBulkEditing(): boolean {
|
||||||
return this.list.selected.size > 0
|
return this.list.selected.size > 0
|
||||||
}
|
}
|
||||||
@@ -300,7 +308,7 @@ export class DocumentListComponent
|
|||||||
if (this.list.selected.size > 0) {
|
if (this.list.selected.size > 0) {
|
||||||
this.list.selectNone()
|
this.list.selectNone()
|
||||||
} else if (this.isFiltered) {
|
} else if (this.isFiltered) {
|
||||||
this.filterEditor.resetSelected()
|
this.resetFilters()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2107,6 +2107,22 @@ describe('FilterEditorComponent', () => {
|
|||||||
expect(component.filterRules).toEqual(rules)
|
expect(component.filterRules).toEqual(rules)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should emit reset filter rules when resetting', () => {
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '2' }]
|
||||||
|
component.unmodifiedFilterRules = rules
|
||||||
|
component.filterRules = [
|
||||||
|
{ rule_type: FILTER_DOES_NOT_HAVE_TAG, value: '2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const resetFilterRulesSpy = jest.spyOn(component.resetFilterRules, 'next')
|
||||||
|
const filterRulesChangeSpy = jest.spyOn(component.filterRulesChange, 'next')
|
||||||
|
|
||||||
|
component.resetSelected()
|
||||||
|
|
||||||
|
expect(resetFilterRulesSpy).toHaveBeenCalledWith(rules)
|
||||||
|
expect(filterRulesChangeSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support resetting text field', () => {
|
it('should support resetting text field', () => {
|
||||||
component.textFilter = 'foo'
|
component.textFilter = 'foo'
|
||||||
component.resetTextField()
|
component.resetTextField()
|
||||||
|
|||||||
@@ -1101,6 +1101,9 @@ export class FilterEditorComponent
|
|||||||
@Output()
|
@Output()
|
||||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
resetFilterRules = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selectionData(selectionData: SelectionData) {
|
set selectionData(selectionData: SelectionData) {
|
||||||
this.tagDocumentCounts = selectionData?.selected_tags ?? null
|
this.tagDocumentCounts = selectionData?.selected_tags ?? null
|
||||||
@@ -1244,7 +1247,7 @@ export class FilterEditorComponent
|
|||||||
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
this.documentService.searchQuery = ''
|
this.documentService.searchQuery = ''
|
||||||
this.filterRules = this._unmodifiedFilterRules
|
this.filterRules = this._unmodifiedFilterRules
|
||||||
this.updateRules()
|
this.resetFilterRules.next(this.filterRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTag(tagId: number) {
|
toggleTag(tagId: number) {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe('DocumentListViewService', () => {
|
|||||||
value: tags__id__in,
|
value: tags__id__in,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
documentListViewService.filterRules = filterRulesAny
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
@@ -178,7 +178,7 @@ describe('DocumentListViewService', () => {
|
|||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -210,7 +210,7 @@ describe('DocumentListViewService', () => {
|
|||||||
value: tags__id__in,
|
value: tags__id__in,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
documentListViewService.filterRules = filterRulesAny
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
@@ -218,7 +218,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
||||||
expect(documentListViewService.error).toEqual('Generic error')
|
expect(documentListViewService.error).toEqual('Generic error')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -295,13 +295,41 @@ describe('DocumentListViewService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should use filter rules to update query params', () => {
|
it('should use filter rules to update query params', () => {
|
||||||
documentListViewService.filterRules = filterRules
|
documentListViewService.setFilterRules(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support setting filter rules and resetting to page one', () => {
|
||||||
|
documentListViewService.currentPage = 2
|
||||||
|
let req = httpTestingController.expectOne((request) =>
|
||||||
|
request.urlWithParams.startsWith(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('GET')
|
||||||
|
req.flush(full_results)
|
||||||
|
req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/selection_data/`
|
||||||
|
)
|
||||||
|
req.flush([])
|
||||||
|
|
||||||
|
documentListViewService.setFilterRules(filterRules, true)
|
||||||
|
|
||||||
|
const filteredReqs = httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||||
|
)
|
||||||
|
expect(filteredReqs).toHaveLength(1)
|
||||||
|
filteredReqs[0].flush(full_results)
|
||||||
|
req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/selection_data/`
|
||||||
|
)
|
||||||
|
req.flush([])
|
||||||
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support quick filter', () => {
|
it('should support quick filter', () => {
|
||||||
documentListViewService.quickFilter(filterRules)
|
documentListViewService.quickFilter(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
@@ -336,7 +364,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -348,7 +376,7 @@ describe('DocumentListViewService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support navigating next / previous', () => {
|
it('should support navigating next / previous', () => {
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -558,7 +586,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
expect(documentListViewService.selected.size).toEqual(6)
|
expect(documentListViewService.selected.size).toEqual(6)
|
||||||
|
|
||||||
documentListViewService.filterRules = filterRules
|
documentListViewService.setFilterRules(filterRules)
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
@@ -592,7 +620,7 @@ describe('DocumentListViewService', () => {
|
|||||||
|
|
||||||
documentListViewService.loadSavedView(view2)
|
documentListViewService.loadSavedView(view2)
|
||||||
expect(documentListViewService.sortField).toEqual('score')
|
expect(documentListViewService.sortField).toEqual('score')
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
expect(documentListViewService.sortField).toEqual('created')
|
expect(documentListViewService.sortField).toEqual('created')
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export class DocumentListViewService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
set filterRules(filterRules: FilterRule[]) {
|
setFilterRules(filterRules: FilterRule[], resetPage: boolean = false) {
|
||||||
if (
|
if (
|
||||||
!isFullTextFilterRule(filterRules) &&
|
!isFullTextFilterRule(filterRules) &&
|
||||||
this.activeListViewState.sortField == 'score'
|
this.activeListViewState.sortField == 'score'
|
||||||
@@ -350,6 +350,9 @@ export class DocumentListViewService {
|
|||||||
this.activeListViewState.sortField = 'created'
|
this.activeListViewState.sortField = 'created'
|
||||||
}
|
}
|
||||||
this.activeListViewState.filterRules = filterRules
|
this.activeListViewState.filterRules = filterRules
|
||||||
|
if (resetPage) {
|
||||||
|
this.activeListViewState.currentPage = 1
|
||||||
|
}
|
||||||
this.reload()
|
this.reload()
|
||||||
this.reduceSelectionToFilter()
|
this.reduceSelectionToFilter()
|
||||||
this.saveDocumentListView()
|
this.saveDocumentListView()
|
||||||
@@ -479,7 +482,7 @@ export class DocumentListViewService {
|
|||||||
|
|
||||||
quickFilter(filterRules: FilterRule[]) {
|
quickFilter(filterRules: FilterRule[]) {
|
||||||
this._activeSavedViewId = null
|
this._activeSavedViewId = null
|
||||||
this.filterRules = filterRules
|
this.setFilterRules(filterRules)
|
||||||
this.router.navigate(['documents'])
|
this.router.navigate(['documents'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,3 +202,43 @@ def audit_log_check(app_configs, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@register()
|
||||||
|
def check_deprecated_db_settings(
|
||||||
|
app_configs: object,
|
||||||
|
**kwargs: object,
|
||||||
|
) -> list[Warning]:
|
||||||
|
"""Check for deprecated database environment variables.
|
||||||
|
|
||||||
|
Detects legacy advanced options that should be migrated to
|
||||||
|
PAPERLESS_DB_OPTIONS. Returns one Warning per deprecated variable found.
|
||||||
|
"""
|
||||||
|
deprecated_vars: dict[str, str] = {
|
||||||
|
"PAPERLESS_DB_TIMEOUT": "timeout",
|
||||||
|
"PAPERLESS_DB_POOLSIZE": "pool.min_size / pool.max_size",
|
||||||
|
"PAPERLESS_DBSSLMODE": "sslmode",
|
||||||
|
"PAPERLESS_DBSSLROOTCERT": "sslrootcert",
|
||||||
|
"PAPERLESS_DBSSLCERT": "sslcert",
|
||||||
|
"PAPERLESS_DBSSLKEY": "sslkey",
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings: list[Warning] = []
|
||||||
|
|
||||||
|
for var_name, db_option_key in deprecated_vars.items():
|
||||||
|
if not os.getenv(var_name):
|
||||||
|
continue
|
||||||
|
warnings.append(
|
||||||
|
Warning(
|
||||||
|
f"Deprecated environment variable: {var_name}",
|
||||||
|
hint=(
|
||||||
|
f"{var_name} is no longer supported and will be removed in v3.2. "
|
||||||
|
f"Set the equivalent option via PAPERLESS_DB_OPTIONS instead. "
|
||||||
|
f'Example: PAPERLESS_DB_OPTIONS=\'{{"{db_option_key}": "<value>"}}\'. '
|
||||||
|
"See https://docs.paperless-ngx.com/migration/ for the full reference."
|
||||||
|
),
|
||||||
|
id="paperless.W001",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from dateparser.languages.loader import LocaleDataLoader
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from paperless.settings.custom import parse_db_settings
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.settings")
|
logger = logging.getLogger("paperless.settings")
|
||||||
|
|
||||||
# Tap paperless.conf if it's available
|
# Tap paperless.conf if it's available
|
||||||
@@ -722,83 +724,8 @@ EMAIL_CERTIFICATE_FILE = __get_optional_path("PAPERLESS_EMAIL_CERTIFICATE_LOCATI
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Database #
|
# Database #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
def _parse_db_settings() -> dict:
|
|
||||||
databases = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
|
||||||
"NAME": DATA_DIR / "db.sqlite3",
|
|
||||||
"OPTIONS": {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if os.getenv("PAPERLESS_DBHOST"):
|
|
||||||
# Have sqlite available as a second option for management commands
|
|
||||||
# This is important when migrating to/from sqlite
|
|
||||||
databases["sqlite"] = databases["default"].copy()
|
|
||||||
|
|
||||||
databases["default"] = {
|
DATABASES = parse_db_settings(DATA_DIR)
|
||||||
"HOST": os.getenv("PAPERLESS_DBHOST"),
|
|
||||||
"NAME": os.getenv("PAPERLESS_DBNAME", "paperless"),
|
|
||||||
"USER": os.getenv("PAPERLESS_DBUSER", "paperless"),
|
|
||||||
"PASSWORD": os.getenv("PAPERLESS_DBPASS", "paperless"),
|
|
||||||
"OPTIONS": {},
|
|
||||||
}
|
|
||||||
if os.getenv("PAPERLESS_DBPORT"):
|
|
||||||
databases["default"]["PORT"] = os.getenv("PAPERLESS_DBPORT")
|
|
||||||
|
|
||||||
# Leave room for future extensibility
|
|
||||||
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
|
|
||||||
engine = "django.db.backends.mysql"
|
|
||||||
# Contrary to Postgres, Django does not natively support connection pooling for MariaDB.
|
|
||||||
# However, since MariaDB uses threads instead of forks, establishing connections is significantly faster
|
|
||||||
# compared to PostgreSQL, so the lack of pooling is not an issue
|
|
||||||
options = {
|
|
||||||
"read_default_file": "/etc/mysql/my.cnf",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"ssl_mode": os.getenv("PAPERLESS_DBSSLMODE", "PREFERRED"),
|
|
||||||
"ssl": {
|
|
||||||
"ca": os.getenv("PAPERLESS_DBSSLROOTCERT", None),
|
|
||||||
"cert": os.getenv("PAPERLESS_DBSSLCERT", None),
|
|
||||||
"key": os.getenv("PAPERLESS_DBSSLKEY", None),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
else: # Default to PostgresDB
|
|
||||||
engine = "django.db.backends.postgresql"
|
|
||||||
options = {
|
|
||||||
"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer"),
|
|
||||||
"sslrootcert": os.getenv("PAPERLESS_DBSSLROOTCERT", None),
|
|
||||||
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
|
|
||||||
"sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
|
|
||||||
}
|
|
||||||
if int(os.getenv("PAPERLESS_DB_POOLSIZE", 0)) > 0:
|
|
||||||
options.update(
|
|
||||||
{
|
|
||||||
"pool": {
|
|
||||||
"min_size": 1,
|
|
||||||
"max_size": int(os.getenv("PAPERLESS_DB_POOLSIZE")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
databases["default"]["ENGINE"] = engine
|
|
||||||
databases["default"]["OPTIONS"].update(options)
|
|
||||||
|
|
||||||
if os.getenv("PAPERLESS_DB_TIMEOUT") is not None:
|
|
||||||
if databases["default"]["ENGINE"] == "django.db.backends.sqlite3":
|
|
||||||
databases["default"]["OPTIONS"].update(
|
|
||||||
{"timeout": int(os.getenv("PAPERLESS_DB_TIMEOUT"))},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
databases["default"]["OPTIONS"].update(
|
|
||||||
{"connect_timeout": int(os.getenv("PAPERLESS_DB_TIMEOUT"))},
|
|
||||||
)
|
|
||||||
databases["sqlite"]["OPTIONS"].update(
|
|
||||||
{"timeout": int(os.getenv("PAPERLESS_DB_TIMEOUT"))},
|
|
||||||
)
|
|
||||||
return databases
|
|
||||||
|
|
||||||
|
|
||||||
DATABASES = _parse_db_settings()
|
|
||||||
|
|
||||||
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
|
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
|
||||||
# Silence Django error on old MariaDB versions.
|
# Silence Django error on old MariaDB versions.
|
||||||
128
src/paperless/settings/custom.py
Normal file
128
src/paperless/settings/custom.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from paperless.settings.parsers import get_choice_from_env
|
||||||
|
from paperless.settings.parsers import get_int_from_env
|
||||||
|
from paperless.settings.parsers import parse_dict_from_str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_db_settings(data_dir: Path) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Parse database settings from environment variables.
|
||||||
|
|
||||||
|
Core connection variables (no deprecation):
|
||||||
|
- PAPERLESS_DBENGINE (sqlite/postgresql/mariadb)
|
||||||
|
- PAPERLESS_DBHOST, PAPERLESS_DBPORT
|
||||||
|
- PAPERLESS_DBNAME, PAPERLESS_DBUSER, PAPERLESS_DBPASS
|
||||||
|
|
||||||
|
Advanced options can be set via:
|
||||||
|
- Legacy individual env vars (deprecated in v3.0, removed in v3.2)
|
||||||
|
- PAPERLESS_DB_OPTIONS (recommended v3+ approach)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_dir: The data directory path for SQLite database location.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A databases dict suitable for Django DATABASES setting.
|
||||||
|
"""
|
||||||
|
engine = get_choice_from_env(
|
||||||
|
"PAPERLESS_DBENGINE",
|
||||||
|
{"sqlite", "postgresql", "mariadb"},
|
||||||
|
default="sqlite",
|
||||||
|
)
|
||||||
|
|
||||||
|
db_config: dict[str, Any]
|
||||||
|
base_options: dict[str, Any]
|
||||||
|
|
||||||
|
match engine:
|
||||||
|
case "sqlite":
|
||||||
|
db_config = {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": str((data_dir / "db.sqlite3").resolve()),
|
||||||
|
}
|
||||||
|
base_options = {}
|
||||||
|
|
||||||
|
case "postgresql":
|
||||||
|
db_config = {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": os.getenv("PAPERLESS_DBHOST"),
|
||||||
|
"NAME": os.getenv("PAPERLESS_DBNAME", "paperless"),
|
||||||
|
"USER": os.getenv("PAPERLESS_DBUSER", "paperless"),
|
||||||
|
"PASSWORD": os.getenv("PAPERLESS_DBPASS", "paperless"),
|
||||||
|
}
|
||||||
|
|
||||||
|
base_options = {
|
||||||
|
"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer"),
|
||||||
|
"sslrootcert": os.getenv("PAPERLESS_DBSSLROOTCERT"),
|
||||||
|
"sslcert": os.getenv("PAPERLESS_DBSSLCERT"),
|
||||||
|
"sslkey": os.getenv("PAPERLESS_DBSSLKEY"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool_size := get_int_from_env("PAPERLESS_DB_POOLSIZE")) is not None:
|
||||||
|
base_options["pool"] = {
|
||||||
|
"min_size": 1,
|
||||||
|
"max_size": pool_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
case "mariadb":
|
||||||
|
db_config = {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"HOST": os.getenv("PAPERLESS_DBHOST"),
|
||||||
|
"NAME": os.getenv("PAPERLESS_DBNAME", "paperless"),
|
||||||
|
"USER": os.getenv("PAPERLESS_DBUSER", "paperless"),
|
||||||
|
"PASSWORD": os.getenv("PAPERLESS_DBPASS", "paperless"),
|
||||||
|
}
|
||||||
|
|
||||||
|
base_options = {
|
||||||
|
"read_default_file": "/etc/mysql/my.cnf",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"collation": "utf8mb4_unicode_ci",
|
||||||
|
"ssl_mode": os.getenv("PAPERLESS_DBSSLMODE", "PREFERRED"),
|
||||||
|
"ssl": {
|
||||||
|
"ca": os.getenv("PAPERLESS_DBSSLROOTCERT"),
|
||||||
|
"cert": os.getenv("PAPERLESS_DBSSLCERT"),
|
||||||
|
"key": os.getenv("PAPERLESS_DBSSLKEY"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case _: # pragma: no cover
|
||||||
|
raise NotImplementedError(engine)
|
||||||
|
|
||||||
|
# Handle port setting for external databases
|
||||||
|
if (
|
||||||
|
engine in ("postgresql", "mariadb")
|
||||||
|
and (port := get_int_from_env("PAPERLESS_DBPORT")) is not None
|
||||||
|
):
|
||||||
|
db_config["PORT"] = port
|
||||||
|
|
||||||
|
# Handle timeout setting (common across all engines, different key names)
|
||||||
|
if (timeout := get_int_from_env("PAPERLESS_DB_TIMEOUT")) is not None:
|
||||||
|
timeout_key = "timeout" if engine == "sqlite" else "connect_timeout"
|
||||||
|
base_options[timeout_key] = timeout
|
||||||
|
|
||||||
|
# Apply PAPERLESS_DB_OPTIONS overrides
|
||||||
|
db_config["OPTIONS"] = parse_dict_from_str(
|
||||||
|
os.getenv("PAPERLESS_DB_OPTIONS"),
|
||||||
|
defaults=base_options,
|
||||||
|
separator=";",
|
||||||
|
type_map={
|
||||||
|
# SQLite options
|
||||||
|
"timeout": int,
|
||||||
|
# Postgres/MariaDB options
|
||||||
|
"connect_timeout": int,
|
||||||
|
"pool.min_size": int,
|
||||||
|
"pool.max_size": int,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
databases = {"default": db_config}
|
||||||
|
|
||||||
|
# Add SQLite fallback for PostgreSQL/MariaDB
|
||||||
|
# TODO: Is this really useful/used?
|
||||||
|
if engine in ("postgresql", "mariadb"):
|
||||||
|
databases["sqlite"] = {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": str((data_dir / "db.sqlite3").resolve()),
|
||||||
|
"OPTIONS": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases
|
||||||
192
src/paperless/settings/parsers.py
Normal file
192
src/paperless/settings/parsers.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import copy
|
||||||
|
import os
|
||||||
|
from collections.abc import Callable
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_bool(value: str) -> bool:
|
||||||
|
"""
|
||||||
|
Converts a string representation of truth to a boolean value.
|
||||||
|
|
||||||
|
Recognizes 'true', '1', 't', 'y', 'yes' as True, and
|
||||||
|
'false', '0', 'f', 'n', 'no' as False. Case-insensitive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The string to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The boolean representation of the string.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the string is not a recognized boolean value.
|
||||||
|
"""
|
||||||
|
val_lower = value.strip().lower()
|
||||||
|
if val_lower in ("true", "1", "t", "y", "yes"):
|
||||||
|
return True
|
||||||
|
elif val_lower in ("false", "0", "f", "n", "no"):
|
||||||
|
return False
|
||||||
|
raise ValueError(f"Cannot convert '{value}' to a boolean.")
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_int_from_env(key: str) -> int | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_int_from_env(key: str, default: None) -> int | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_int_from_env(key: str, default: int) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
|
def get_int_from_env(key: str, default: int | None = None) -> int | None:
|
||||||
|
"""
|
||||||
|
Return an integer value based on the environment variable.
|
||||||
|
If default is provided, returns that value when key is missing.
|
||||||
|
If default is None, returns None when key is missing.
|
||||||
|
"""
|
||||||
|
if key not in os.environ:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return int(os.environ[key])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dict_from_str(
|
||||||
|
env_str: str | None,
|
||||||
|
defaults: dict[str, Any] | None = None,
|
||||||
|
type_map: Mapping[str, Callable[[str], Any]] | None = None,
|
||||||
|
separator: str = ",",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parses a key-value string into a dictionary, applying defaults and casting types.
|
||||||
|
|
||||||
|
Supports nested keys via dot-notation, e.g.:
|
||||||
|
"database.host=localhost,database.port=5432"
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_str: The string from the environment variable (e.g., "port=9090,debug=true").
|
||||||
|
defaults: A dictionary of default values (can contain nested dicts).
|
||||||
|
type_map: A dictionary mapping keys (dot-notation allowed) to a type or a parsing
|
||||||
|
function (e.g., {'port': int, 'debug': bool, 'database.port': int}).
|
||||||
|
The special `bool` type triggers custom boolean parsing.
|
||||||
|
separator: The character used to separate key-value pairs. Defaults to ','.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary with the parsed and correctly-typed settings.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If a value cannot be cast to its specified type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _set_nested(d: dict, keys: list[str], value: Any) -> None:
|
||||||
|
"""Set a nested value, creating intermediate dicts as needed."""
|
||||||
|
cur = d
|
||||||
|
for k in keys[:-1]:
|
||||||
|
if k not in cur or not isinstance(cur[k], dict):
|
||||||
|
cur[k] = {}
|
||||||
|
cur = cur[k]
|
||||||
|
cur[keys[-1]] = value
|
||||||
|
|
||||||
|
def _get_nested(d: dict, keys: list[str]) -> Any:
|
||||||
|
"""Get nested value or raise KeyError if not present."""
|
||||||
|
cur = d
|
||||||
|
for k in keys:
|
||||||
|
if not isinstance(cur, dict) or k not in cur:
|
||||||
|
raise KeyError
|
||||||
|
cur = cur[k]
|
||||||
|
return cur
|
||||||
|
|
||||||
|
def _has_nested(d: dict, keys: list[str]) -> bool:
|
||||||
|
try:
|
||||||
|
_get_nested(d, keys)
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
settings: dict[str, Any] = copy.deepcopy(defaults) if defaults else {}
|
||||||
|
_type_map = type_map if type_map else {}
|
||||||
|
|
||||||
|
if not env_str:
|
||||||
|
return settings
|
||||||
|
|
||||||
|
# Parse the environment string using the specified separator
|
||||||
|
pairs = [p.strip() for p in env_str.split(separator) if p.strip()]
|
||||||
|
for pair in pairs:
|
||||||
|
if "=" not in pair:
|
||||||
|
# ignore malformed pairs
|
||||||
|
continue
|
||||||
|
key, val = pair.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
val = val.strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
parts = key.split(".")
|
||||||
|
_set_nested(settings, parts, val)
|
||||||
|
|
||||||
|
# Apply type casting to the updated settings (supports nested keys in type_map)
|
||||||
|
for key, caster in _type_map.items():
|
||||||
|
key_parts = key.split(".")
|
||||||
|
if _has_nested(settings, key_parts):
|
||||||
|
raw_val = _get_nested(settings, key_parts)
|
||||||
|
# Only cast if it's a string (i.e. from env parsing). If defaults already provided
|
||||||
|
# a different type we leave it as-is.
|
||||||
|
if isinstance(raw_val, str):
|
||||||
|
try:
|
||||||
|
if caster is bool:
|
||||||
|
parsed = str_to_bool(raw_val)
|
||||||
|
elif caster is Path:
|
||||||
|
parsed = Path(raw_val).resolve()
|
||||||
|
else:
|
||||||
|
parsed = caster(raw_val)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
caster_name = getattr(caster, "__name__", repr(caster))
|
||||||
|
raise ValueError(
|
||||||
|
f"Error casting key '{key}' with value '{raw_val}' "
|
||||||
|
f"to type '{caster_name}'",
|
||||||
|
) from e
|
||||||
|
_set_nested(settings, key_parts, parsed)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_choice_from_env(
|
||||||
|
env_key: str,
|
||||||
|
choices: set[str],
|
||||||
|
default: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Gets and validates an environment variable against a set of allowed choices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_key: The environment variable key to validate
|
||||||
|
choices: Set of valid choices for the environment variable
|
||||||
|
default: Optional default value if environment variable is not set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The validated environment variable value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the environment variable value is not in choices
|
||||||
|
or if no default is provided and env var is missing
|
||||||
|
"""
|
||||||
|
value = os.environ.get(env_key, default)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Environment variable '{env_key}' is required but not set.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if value not in choices:
|
||||||
|
raise ValueError(
|
||||||
|
f"Environment variable '{env_key}' has invalid value '{value}'. "
|
||||||
|
f"Valid choices are: {', '.join(sorted(choices))}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
@@ -2,13 +2,17 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.core.checks import Warning
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless.checks import audit_log_check
|
from paperless.checks import audit_log_check
|
||||||
from paperless.checks import binaries_check
|
from paperless.checks import binaries_check
|
||||||
|
from paperless.checks import check_deprecated_db_settings
|
||||||
from paperless.checks import debug_mode_check
|
from paperless.checks import debug_mode_check
|
||||||
from paperless.checks import paths_check
|
from paperless.checks import paths_check
|
||||||
from paperless.checks import settings_values_check
|
from paperless.checks import settings_values_check
|
||||||
@@ -237,3 +241,157 @@ class TestAuditLogChecks(TestCase):
|
|||||||
("auditlog table was found but audit log is disabled."),
|
("auditlog table was found but audit log is disabled."),
|
||||||
msg.msg,
|
msg.msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEPRECATED_VARS: dict[str, str] = {
|
||||||
|
"PAPERLESS_DB_TIMEOUT": "timeout",
|
||||||
|
"PAPERLESS_DB_POOLSIZE": "pool.min_size / pool.max_size",
|
||||||
|
"PAPERLESS_DBSSLMODE": "sslmode",
|
||||||
|
"PAPERLESS_DBSSLROOTCERT": "sslrootcert",
|
||||||
|
"PAPERLESS_DBSSLCERT": "sslcert",
|
||||||
|
"PAPERLESS_DBSSLKEY": "sslkey",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeprecatedDbSettings:
|
||||||
|
"""Test suite for the check_deprecated_db_settings system check."""
|
||||||
|
|
||||||
|
def test_no_deprecated_vars_returns_empty(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""No warnings when none of the deprecated vars are present."""
|
||||||
|
# clear=True ensures vars from the outer test environment do not leak in
|
||||||
|
mocker.patch.dict(os.environ, {}, clear=True)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("env_var", "db_option_key"),
|
||||||
|
[
|
||||||
|
("PAPERLESS_DB_TIMEOUT", "timeout"),
|
||||||
|
("PAPERLESS_DB_POOLSIZE", "pool.min_size / pool.max_size"),
|
||||||
|
("PAPERLESS_DBSSLMODE", "sslmode"),
|
||||||
|
("PAPERLESS_DBSSLROOTCERT", "sslrootcert"),
|
||||||
|
("PAPERLESS_DBSSLCERT", "sslcert"),
|
||||||
|
("PAPERLESS_DBSSLKEY", "sslkey"),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"db-timeout",
|
||||||
|
"db-poolsize",
|
||||||
|
"ssl-mode",
|
||||||
|
"ssl-rootcert",
|
||||||
|
"ssl-cert",
|
||||||
|
"ssl-key",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_single_deprecated_var_produces_one_warning(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
env_var: str,
|
||||||
|
db_option_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Each deprecated var in isolation produces exactly one warning."""
|
||||||
|
mocker.patch.dict(os.environ, {env_var: "some_value"}, clear=True)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
warning = result[0]
|
||||||
|
assert isinstance(warning, Warning)
|
||||||
|
assert warning.id == "paperless.W001"
|
||||||
|
assert env_var in warning.hint
|
||||||
|
assert db_option_key in warning.hint
|
||||||
|
|
||||||
|
def test_multiple_deprecated_vars_produce_one_warning_each(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Each deprecated var present in the environment gets its own warning."""
|
||||||
|
set_vars = {
|
||||||
|
"PAPERLESS_DB_TIMEOUT": "30",
|
||||||
|
"PAPERLESS_DB_POOLSIZE": "10",
|
||||||
|
"PAPERLESS_DBSSLMODE": "require",
|
||||||
|
}
|
||||||
|
mocker.patch.dict(os.environ, set_vars, clear=True)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == len(set_vars)
|
||||||
|
assert all(isinstance(w, Warning) for w in result)
|
||||||
|
assert all(w.id == "paperless.W001" for w in result)
|
||||||
|
all_hints = " ".join(w.hint for w in result)
|
||||||
|
for var_name in set_vars:
|
||||||
|
assert var_name in all_hints
|
||||||
|
|
||||||
|
def test_all_deprecated_vars_produces_one_warning_each(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""All deprecated vars set simultaneously produces one warning per var."""
|
||||||
|
all_vars = {var: "some_value" for var in DEPRECATED_VARS}
|
||||||
|
mocker.patch.dict(os.environ, all_vars, clear=True)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == len(DEPRECATED_VARS)
|
||||||
|
assert all(isinstance(w, Warning) for w in result)
|
||||||
|
assert all(w.id == "paperless.W001" for w in result)
|
||||||
|
|
||||||
|
def test_unset_vars_not_mentioned_in_warnings(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Vars absent from the environment do not appear in any warning."""
|
||||||
|
mocker.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"PAPERLESS_DB_TIMEOUT": "30"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert "PAPERLESS_DB_TIMEOUT" in result[0].hint
|
||||||
|
unset_vars = [v for v in DEPRECATED_VARS if v != "PAPERLESS_DB_TIMEOUT"]
|
||||||
|
for var_name in unset_vars:
|
||||||
|
assert var_name not in result[0].hint
|
||||||
|
|
||||||
|
def test_empty_string_var_not_treated_as_set(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""A var set to an empty string is not flagged as a deprecated setting."""
|
||||||
|
mocker.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"PAPERLESS_DB_TIMEOUT": ""},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_warning_mentions_migration_target(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Each warning hints at PAPERLESS_DB_OPTIONS as the migration target."""
|
||||||
|
mocker.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"PAPERLESS_DBSSLMODE": "require"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert "PAPERLESS_DB_OPTIONS" in result[0].hint
|
||||||
|
|
||||||
|
def test_warning_message_identifies_var(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""The warning message (not just the hint) identifies the offending var."""
|
||||||
|
mocker.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"PAPERLESS_DBSSLCERT": "/path/to/cert.pem"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
result = check_deprecated_db_settings(None)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert "PAPERLESS_DBSSLCERT" in result[0].msg
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from paperless.settings import _parse_base_paths
|
from paperless.settings import _parse_base_paths
|
||||||
from paperless.settings import _parse_beat_schedule
|
from paperless.settings import _parse_beat_schedule
|
||||||
from paperless.settings import _parse_dateparser_languages
|
from paperless.settings import _parse_dateparser_languages
|
||||||
from paperless.settings import _parse_db_settings
|
|
||||||
from paperless.settings import _parse_ignore_dates
|
from paperless.settings import _parse_ignore_dates
|
||||||
from paperless.settings import _parse_paperless_url
|
from paperless.settings import _parse_paperless_url
|
||||||
from paperless.settings import _parse_redis_url
|
from paperless.settings import _parse_redis_url
|
||||||
from paperless.settings import default_threads_per_worker
|
from paperless.settings import default_threads_per_worker
|
||||||
|
from paperless.settings.custom import parse_db_settings
|
||||||
|
|
||||||
|
|
||||||
class TestIgnoreDateParsing(TestCase):
|
class TestIgnoreDateParsing(TestCase):
|
||||||
@@ -378,62 +380,302 @@ class TestCeleryScheduleParsing(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDBSettings(TestCase):
|
class TestParseDbSettings:
|
||||||
def test_db_timeout_with_sqlite(self) -> None:
|
"""Test suite for parse_db_settings function."""
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- PAPERLESS_DB_TIMEOUT is set
|
|
||||||
WHEN:
|
|
||||||
- Settings are parsed
|
|
||||||
THEN:
|
|
||||||
- PAPERLESS_DB_TIMEOUT set for sqlite
|
|
||||||
"""
|
|
||||||
with mock.patch.dict(
|
|
||||||
os.environ,
|
|
||||||
{
|
|
||||||
"PAPERLESS_DB_TIMEOUT": "10",
|
|
||||||
},
|
|
||||||
):
|
|
||||||
databases = _parse_db_settings()
|
|
||||||
|
|
||||||
self.assertDictEqual(
|
@pytest.mark.parametrize(
|
||||||
|
("env_vars", "expected_database_settings"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
{
|
{
|
||||||
"timeout": 10.0,
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
databases["default"]["OPTIONS"],
|
id="default-sqlite",
|
||||||
)
|
),
|
||||||
|
pytest.param(
|
||||||
def test_db_timeout_with_not_sqlite(self) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- PAPERLESS_DB_TIMEOUT is set but db is not sqlite
|
|
||||||
WHEN:
|
|
||||||
- Settings are parsed
|
|
||||||
THEN:
|
|
||||||
- PAPERLESS_DB_TIMEOUT set correctly in non-sqlite db & for fallback sqlite db
|
|
||||||
"""
|
|
||||||
with mock.patch.dict(
|
|
||||||
os.environ,
|
|
||||||
{
|
|
||||||
"PAPERLESS_DBHOST": "127.0.0.1",
|
|
||||||
"PAPERLESS_DB_TIMEOUT": "10",
|
|
||||||
},
|
|
||||||
):
|
|
||||||
databases = _parse_db_settings()
|
|
||||||
|
|
||||||
self.assertDictEqual(
|
|
||||||
databases["default"]["OPTIONS"],
|
|
||||||
databases["default"]["OPTIONS"]
|
|
||||||
| {
|
|
||||||
"connect_timeout": 10.0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertDictEqual(
|
|
||||||
{
|
{
|
||||||
"timeout": 10.0,
|
"PAPERLESS_DBENGINE": "sqlite",
|
||||||
|
"PAPERLESS_DB_OPTIONS": "timeout=30",
|
||||||
},
|
},
|
||||||
databases["sqlite"]["OPTIONS"],
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {
|
||||||
|
"timeout": 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="sqlite-with-timeout-override",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "postgresql",
|
||||||
|
"PAPERLESS_DBHOST": "localhost",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": "localhost",
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "paperless",
|
||||||
|
"PASSWORD": "paperless",
|
||||||
|
"OPTIONS": {
|
||||||
|
"sslmode": "prefer",
|
||||||
|
"sslrootcert": None,
|
||||||
|
"sslcert": None,
|
||||||
|
"sslkey": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="postgresql-defaults",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "postgresql",
|
||||||
|
"PAPERLESS_DBHOST": "paperless-db-host",
|
||||||
|
"PAPERLESS_DBPORT": "1111",
|
||||||
|
"PAPERLESS_DBNAME": "customdb",
|
||||||
|
"PAPERLESS_DBUSER": "customuser",
|
||||||
|
"PAPERLESS_DBPASS": "custompass",
|
||||||
|
"PAPERLESS_DB_OPTIONS": "pool.max_size=50;pool.min_size=2;sslmode=require",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": "paperless-db-host",
|
||||||
|
"PORT": 1111,
|
||||||
|
"NAME": "customdb",
|
||||||
|
"USER": "customuser",
|
||||||
|
"PASSWORD": "custompass",
|
||||||
|
"OPTIONS": {
|
||||||
|
"sslmode": "require",
|
||||||
|
"sslrootcert": None,
|
||||||
|
"sslcert": None,
|
||||||
|
"sslkey": None,
|
||||||
|
"pool": {
|
||||||
|
"min_size": 2,
|
||||||
|
"max_size": 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="postgresql-overrides",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "postgresql",
|
||||||
|
"PAPERLESS_DBHOST": "pghost",
|
||||||
|
"PAPERLESS_DB_POOLSIZE": "10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": "pghost",
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "paperless",
|
||||||
|
"PASSWORD": "paperless",
|
||||||
|
"OPTIONS": {
|
||||||
|
"sslmode": "prefer",
|
||||||
|
"sslrootcert": None,
|
||||||
|
"sslcert": None,
|
||||||
|
"sslkey": None,
|
||||||
|
"pool": {
|
||||||
|
"min_size": 1,
|
||||||
|
"max_size": 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="postgresql-legacy-poolsize",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "postgresql",
|
||||||
|
"PAPERLESS_DBHOST": "pghost",
|
||||||
|
"PAPERLESS_DBSSLMODE": "require",
|
||||||
|
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.crt",
|
||||||
|
"PAPERLESS_DB_TIMEOUT": "30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": "pghost",
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "paperless",
|
||||||
|
"PASSWORD": "paperless",
|
||||||
|
"OPTIONS": {
|
||||||
|
"sslmode": "require",
|
||||||
|
"sslrootcert": "/certs/ca.crt",
|
||||||
|
"sslcert": None,
|
||||||
|
"sslkey": None,
|
||||||
|
"connect_timeout": 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="postgresql-legacy-ssl-and-timeout",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "mariadb",
|
||||||
|
"PAPERLESS_DBHOST": "localhost",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"HOST": "localhost",
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "paperless",
|
||||||
|
"PASSWORD": "paperless",
|
||||||
|
"OPTIONS": {
|
||||||
|
"read_default_file": "/etc/mysql/my.cnf",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"collation": "utf8mb4_unicode_ci",
|
||||||
|
"ssl_mode": "PREFERRED",
|
||||||
|
"ssl": {
|
||||||
|
"ca": None,
|
||||||
|
"cert": None,
|
||||||
|
"key": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="mariadb-defaults",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "mariadb",
|
||||||
|
"PAPERLESS_DBHOST": "paperless-mariadb-host",
|
||||||
|
"PAPERLESS_DBPORT": "5555",
|
||||||
|
"PAPERLESS_DBUSER": "my-cool-user",
|
||||||
|
"PAPERLESS_DBPASS": "my-secure-password",
|
||||||
|
"PAPERLESS_DB_OPTIONS": "ssl.ca=/path/to/ca.pem;ssl_mode=REQUIRED",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"HOST": "paperless-mariadb-host",
|
||||||
|
"PORT": 5555,
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "my-cool-user",
|
||||||
|
"PASSWORD": "my-secure-password",
|
||||||
|
"OPTIONS": {
|
||||||
|
"read_default_file": "/etc/mysql/my.cnf",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"collation": "utf8mb4_unicode_ci",
|
||||||
|
"ssl_mode": "REQUIRED",
|
||||||
|
"ssl": {
|
||||||
|
"ca": "/path/to/ca.pem",
|
||||||
|
"cert": None,
|
||||||
|
"key": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="mariadb-overrides",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"PAPERLESS_DBENGINE": "mariadb",
|
||||||
|
"PAPERLESS_DBHOST": "mariahost",
|
||||||
|
"PAPERLESS_DBSSLMODE": "REQUIRED",
|
||||||
|
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.pem",
|
||||||
|
"PAPERLESS_DBSSLCERT": "/certs/client.pem",
|
||||||
|
"PAPERLESS_DBSSLKEY": "/certs/client.key",
|
||||||
|
"PAPERLESS_DB_TIMEOUT": "25",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"HOST": "mariahost",
|
||||||
|
"NAME": "paperless",
|
||||||
|
"USER": "paperless",
|
||||||
|
"PASSWORD": "paperless",
|
||||||
|
"OPTIONS": {
|
||||||
|
"read_default_file": "/etc/mysql/my.cnf",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"collation": "utf8mb4_unicode_ci",
|
||||||
|
"ssl_mode": "REQUIRED",
|
||||||
|
"ssl": {
|
||||||
|
"ca": "/certs/ca.pem",
|
||||||
|
"cert": "/certs/client.pem",
|
||||||
|
"key": "/certs/client.key",
|
||||||
|
},
|
||||||
|
"connect_timeout": 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": None, # Will be replaced with tmp_path
|
||||||
|
"OPTIONS": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id="mariadb-legacy-ssl-and-timeout",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_db_settings(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
env_vars: dict[str, str],
|
||||||
|
expected_database_settings: dict[str, dict],
|
||||||
|
) -> None:
|
||||||
|
"""Test various database configurations with defaults and overrides."""
|
||||||
|
# Clear environment and set test vars
|
||||||
|
mocker.patch.dict(os.environ, env_vars, clear=True)
|
||||||
|
|
||||||
|
# Update expected paths with actual tmp_path
|
||||||
|
if (
|
||||||
|
"default" in expected_database_settings
|
||||||
|
and expected_database_settings["default"]["NAME"] is None
|
||||||
|
):
|
||||||
|
expected_database_settings["default"]["NAME"] = str(
|
||||||
|
tmp_path / "db.sqlite3",
|
||||||
)
|
)
|
||||||
|
if "sqlite" in expected_database_settings:
|
||||||
|
expected_database_settings["sqlite"]["NAME"] = str(
|
||||||
|
tmp_path / "db.sqlite3",
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = parse_db_settings(tmp_path)
|
||||||
|
|
||||||
|
assert settings == expected_database_settings
|
||||||
|
|
||||||
|
|
||||||
class TestPaperlessURLSettings(TestCase):
|
class TestPaperlessURLSettings(TestCase):
|
||||||
|
|||||||
136
uv.lock
generated
136
uv.lock
generated
@@ -1139,14 +1139,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-soft-delete"
|
name = "django-soft-delete"
|
||||||
version = "1.0.23"
|
version = "1.0.22"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/98/c7c52a85b070b1703774df817b6460a7714655302a2d503f6447544f1a29/django_soft_delete-1.0.23.tar.gz", hash = "sha256:814659f0d19d4f2afc58b31ff73f88f0af66715ccef3b4fcd8f6b3a011d59b2a", size = 22458, upload-time = "2026-02-21T17:48:41.345Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/98/d1/c990b731676f93bd4594dee4b5133df52f5d0eee1eb8a969b4030014ac54/django_soft_delete-1.0.22.tar.gz", hash = "sha256:32d0bb95f180c28a40163e78a558acc18901fd56011f91f8ee735c171a6d4244", size = 21982, upload-time = "2025-10-25T13:11:46.199Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/9e/77375a163c340fff03d037eac7d970ce006626e6c3aea87b5d159f052f8b/django_soft_delete-1.0.23-py3-none-any.whl", hash = "sha256:dd2133d4925d58308680f389daa2e150abf7b81a4f0abbbf2161a9db3b9f1e74", size = 19308, upload-time = "2026-02-21T17:48:39.974Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/c2/fca2bf69b7ca7e18aed9ac059e89f1043663e207a514e8fb652450e49631/django_soft_delete-1.0.22-py3-none-any.whl", hash = "sha256:81973c541d21452d249151085d617ebbfb5ec463899f47cd6b1306677481e94c", size = 19221, upload-time = "2025-10-25T13:11:44.755Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2181,7 +2181,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llama-index-core"
|
name = "llama-index-core"
|
||||||
version = "0.14.15"
|
version = "0.14.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -2209,15 +2209,14 @@ dependencies = [
|
|||||||
{ name = "sqlalchemy", extra = ["asyncio"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "sqlalchemy", extra = ["asyncio"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "tinytag", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/4f/7c714bdf94dd229707b43e7f8cedf3aed0a99938fd46a9ad8a418c199988/llama_index_core-0.14.15.tar.gz", hash = "sha256:3766aeeb95921b3a2af8c2a51d844f75f404215336e1639098e3652db52c68ce", size = 11593505, upload-time = "2026-02-18T19:05:48.274Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/74/54/d6043a088e5e9c1d62300db7ad0ef417c6b9a92f7b4a5cade066aeafdaca/llama_index_core-0.14.13.tar.gz", hash = "sha256:c3b30d20ae0407e5d0a1d35bb3376a98e242661ebfc22da754b5a3da1f8108c0", size = 11589074, upload-time = "2026-01-21T20:44:16.287Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/9e/262f6465ee4fffa40698b3cc2177e377ce7d945d3bd8b7d9c6b09448625d/llama_index_core-0.14.15-py3-none-any.whl", hash = "sha256:e02b321c10673871a38aaefdc4a93d5ae8ec324cad4408683189e5a1aa1e3d52", size = 11937002, upload-time = "2026-02-18T19:05:45.855Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/59/9769f03f1cccadcc014b3b65c166de18999b51459a0f0a579d80f6c91d80/llama_index_core-0.14.13-py3-none-any.whl", hash = "sha256:392f0a5a09433e9dea786964ef5fe5ca2a2b10aee9f979a9507c19a14da2a20a", size = 11934761, upload-time = "2026-01-21T20:44:18.892Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2275,27 +2274,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llama-index-llms-openai"
|
name = "llama-index-llms-openai"
|
||||||
version = "0.6.19"
|
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/42/f0/810b09cab0d56de6f9476642d0e016c779f2ac3ec7845eb44ddc12a1796d/llama_index_llms_openai-0.6.19.tar.gz", hash = "sha256:a5e0fcddb7da875759406036e09b949cd64a2bb98da709d933147e41e0e6f78a", size = 25956, upload-time = "2026-02-20T11:18:03.527Z" }
|
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/3c/dd/a8d4e90dad458c830f364e9e7614fad4d2eb8b61c46974c760b08053d495/llama_index_llms_openai-0.6.19-py3-none-any.whl", hash = "sha256:0e83126158f6eb51c153f2b1f7b729bb4bfb6af0191d65b33754b4512180befd", size = 26958, upload-time = "2026-02-20T11:18:02.545Z" },
|
{ 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]]
|
||||||
name = "llama-index-vector-stores-faiss"
|
name = "llama-index-vector-stores-faiss"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
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'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/57da31b38d173cd9124fdcdd47487b9a917b69bd49e8f6e551407ccfa860/llama_index_vector_stores_faiss-0.5.3.tar.gz", hash = "sha256:9620b1e27e96233fda88878c453532fba6061cf7ba7a53698a34703faab21ece", size = 6048, upload-time = "2026-02-12T14:22:14.612Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2d/5f/c4ae340f178f202cf09dcc24dd0953a41d9ab24bc33e1f7220544ba86e41/llama_index_vector_stores_faiss-0.5.2.tar.gz", hash = "sha256:924504765e68b1f84ec602feb2d9516be6a6c12fad5e133f19cc5da3b23f4282", size = 5910, upload-time = "2025-12-17T21:01:13.21Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/ad/ad192dd624ca2875b8ca74e55fddf9b083d6614524004f7830379d0a0cfd/llama_index_vector_stores_faiss-0.5.3-py3-none-any.whl", hash = "sha256:ef186e38a820e696a1adca15432c8539d73f2959eb05671011db21091a286c8c", size = 7738, upload-time = "2026-02-12T14:22:13.756Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/c1/c8317250c2a83d1d439814d1a7f41fa34a23c224b3099da898f08a249859/llama_index_vector_stores_faiss-0.5.2-py3-none-any.whl", hash = "sha256:72a3a03d9f25c70bbcc8c61aa860cd1db69f2a8070606ecc3266d767b71ff2a2", size = 7605, upload-time = "2025-12-17T21:01:12.429Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2767,9 +2766,9 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mysqlclient"
|
name = "mysqlclient"
|
||||||
version = "2.2.8"
|
version = "2.2.7"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nest-asyncio"
|
name = "nest-asyncio"
|
||||||
@@ -3538,23 +3537,23 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prek"
|
name = "prek"
|
||||||
version = "0.3.3"
|
version = "0.3.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/f1/7613dc8347a33e40fc5b79eec6bc7d458d8bbc339782333d8433b665f86f/prek-0.3.3.tar.gz", hash = "sha256:117bd46ebeb39def24298ce021ccc73edcf697b81856fcff36d762dd56093f6f", size = 343697, upload-time = "2026-02-15T13:33:28.723Z" }
|
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/2d/8b/dce13d2a3065fd1e8ffce593a0e51c4a79c3cde9c9a15dc0acc8d9d1573d/prek-0.3.3-py3-none-linux_armv6l.whl", hash = "sha256:e8629cac4bdb131be8dc6e5a337f0f76073ad34a8305f3fe2bc1ab6201ede0a4", size = 4644636, upload-time = "2026-02-15T13:33:43.609Z" },
|
{ 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/01/30/06ab4dbe7ce02a8ce833e92deb1d9a8e85ae9d40e33d1959a2070b7494c6/prek-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4b9e819b9e4118e1e785047b1c8bd9aec7e4d836ed034cb58b7db5bcaaf49437", size = 4651410, upload-time = "2026-02-15T13:33:34.277Z" },
|
{ 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/d4/fc/da3bc5cb38471e7192eda06b7a26b7c24ef83e82da2c1dbc145f2bf33640/prek-0.3.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bf29db3b5657c083eb8444c25aadeeec5167dc492e9019e188f87932f01ea50a", size = 4273163, upload-time = "2026-02-15T13:33:42.106Z" },
|
{ 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/b4/74/47839395091e2937beced81a5dd2f8ea9c8239c853da8611aaf78ee21a8b/prek-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ae09736149815b26e64a9d350ca05692bab32c2afdf2939114d3211aaad68a3e", size = 4631808, upload-time = "2026-02-15T13:33:20.076Z" },
|
{ 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/e2/89/3f5ef6f7c928c017cb63b029349d6bc03598ab7f6979d4a770ce02575f82/prek-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:856c2b55c51703c366bb4ce81c6a91102b70573a9fc8637db2ac61c66e4565f9", size = 4548959, upload-time = "2026-02-15T13:33:36.325Z" },
|
{ 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/b2/18/80002c4c4475f90ca025f27739a016927a0e5d905c60612fc95da1c56ab7/prek-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3acdf13a018f685beaff0a71d4b0d2ccbab4eaa1aced6d08fd471c1a654183eb", size = 4862256, upload-time = "2026-02-15T13:33:37.754Z" },
|
{ 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/c5/25/648bf084c2468fa7cfcdbbe9e59956bbb31b81f36e113bc9107d80af26a7/prek-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f035667a8bd0a77b2bfa2b2e125da8cb1793949e9eeef0d8daab7f8ac8b57fe", size = 5404486, upload-time = "2026-02-15T13:33:39.239Z" },
|
{ 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/8b/43/261fb60a11712a327da345912bd8b338dc5a050199de800faafa278a6133/prek-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09b2ad14332eede441d977de08eb57fb3f61226ed5fd2ceb7aadf5afcdb6794", size = 4887513, upload-time = "2026-02-15T13:33:40.702Z" },
|
{ 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/c7/2c/581e757ee57ec6046b32e0ee25660fc734bc2622c319f57119c49c0cab58/prek-0.3.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c0c3ffac16e37a9daba43a7e8316778f5809b70254be138761a8b5b9ef0df28e", size = 4632336, upload-time = "2026-02-15T13:33:25.867Z" },
|
{ 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/d5/d8/aa276ce5d11b77882da4102ca0cb7161095831105043ae7979bbfdcc3dc4/prek-0.3.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a3dc7720b580c07c0386e17af2486a5b4bc2f6cc57034a288a614dcbc4abe555", size = 4679370, upload-time = "2026-02-15T13:33:22.247Z" },
|
{ 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/70/19/9d4fa7bde428e58d9f48a74290c08736d42aeb5690dcdccc7a713e34a449/prek-0.3.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60e0fa15da5020a03df2ee40268145ec5b88267ec2141a205317ad4df8c992d6", size = 4540316, upload-time = "2026-02-15T13:33:24.088Z" },
|
{ 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/25/b5/973cce29257e0b47b16cc9b4c162772ea01dbb7c080791ea0c068e106e05/prek-0.3.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:553515da9586d9624dc42db32b744fdb91cf62b053753037a0cadb3c2d8d82a2", size = 4724566, upload-time = "2026-02-15T13:33:29.832Z" },
|
{ 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/d6/8b/ad8b2658895a8ed2b0bc630bf38686fe38b7ff2c619c58953a80e4de3048/prek-0.3.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9512cf370e0d1496503463a4a65621480efb41b487841a9e9ff1661edf14b238", size = 4995072, upload-time = "2026-02-15T13:33:27.417Z" },
|
{ 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]]
|
||||||
@@ -4619,24 +4618,24 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.2"
|
version = "0.15.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4827,7 +4826,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentence-transformers"
|
name = "sentence-transformers"
|
||||||
version = "5.2.3"
|
version = "5.2.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -4842,9 +4841,9 @@ dependencies = [
|
|||||||
{ name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a6/bc/0bc9c0ec1cf83ab2ec6e6f38667d167349b950fff6dd2086b79bd360eeca/sentence_transformers-5.2.2.tar.gz", hash = "sha256:7033ee0a24bc04c664fd490abf2ef194d387b3a58a97adcc528783ff505159fa", size = 381607, upload-time = "2026-01-27T11:11:02.658Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/9f/dba4b3e18ebbe1eaa29d9f1764fbc7da0cd91937b83f2b7928d15c5d2d36/sentence_transformers-5.2.3-py3-none-any.whl", hash = "sha256:6437c62d4112b615ddebda362dfc16a4308d604c5b68125ed586e3e95d5b2e30", size = 494225, upload-time = "2026-02-17T14:05:18.596Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/21/7e925890636791386e81b52878134f114d63072e79fffe14cdcc5e7a5e6a/sentence_transformers-5.2.2-py3-none-any.whl", hash = "sha256:280ac54bffb84c110726b4d8848ba7b7c60813b9034547f8aea6e9a345cd1c23", size = 494106, upload-time = "2026-01-27T11:11:00.983Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5133,15 +5132,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tinytag"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/07/fb260bac73119f369a10e884016516d07cd760b5068e703773f83dd5e7bf/tinytag-2.2.0.tar.gz", hash = "sha256:f15b082510f6e0fc717e597edc8759d6f2d3ff6194ac0f3bcd675a9a09d9b798", size = 38120, upload-time = "2025-12-15T21:10:19.093Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/e2/9818fcebb348237389d2ac2fea97cf2b2638378a0866105a45ae9be49728/tinytag-2.2.0-py3-none-any.whl", hash = "sha256:d2cf3ef8ee0f6c854663f77d9d5f8159ee1c834c70f5ea4f214ddc4af8148f79", size = 32861, upload-time = "2025-12-15T21:10:17.63Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokenizers"
|
name = "tokenizers"
|
||||||
version = "0.22.2"
|
version = "0.22.2"
|
||||||
@@ -5490,11 +5480,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-markdown"
|
name = "types-markdown"
|
||||||
version = "3.10.2.20260211"
|
version = "3.10.0.20251106"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/2e/35b30a09f6ee8a69142408d3ceb248c4454aa638c0a414d8704a3ef79563/types_markdown-3.10.2.20260211.tar.gz", hash = "sha256:66164310f88c11a58c6c706094c6f8c537c418e3525d33b76276a5fbd66b01ce", size = 19768, upload-time = "2026-02-11T04:19:29.497Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/de/e4/060f0dadd9b551cae77d6407f2bc84b168f918d90650454aff219c1b3ed2/types_markdown-3.10.0.20251106.tar.gz", hash = "sha256:12836f7fcbd7221db8baeb0d3a2f820b95050d0824bfa9665c67b4d144a1afa1", size = 19486, upload-time = "2025-11-06T03:06:44.317Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/c9/659fa2df04b232b0bfcd05d2418e683080e91ec68f636f3c0a5a267350e7/types_markdown-3.10.2.20260211-py3-none-any.whl", hash = "sha256:2d94d08587e3738203b3c4479c449845112b171abe8b5cadc9b0c12fcf3e99da", size = 25854, upload-time = "2026-02-11T04:19:28.647Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/58/f666ca9391f2a8bd33bb0b0797cde6ac3e764866708d5f8aec6fab215320/types_markdown-3.10.0.20251106-py3-none-any.whl", hash = "sha256:2c39512a573899b59efae07e247ba088a75b70e3415e81277692718f430afd7e", size = 25862, upload-time = "2025-11-06T03:06:43.082Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6104,7 +6094,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zensical"
|
name = "zensical"
|
||||||
version = "0.0.23"
|
version = "0.0.21"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -6115,18 +6105,18 @@ dependencies = [
|
|||||||
{ name = "pyyaml", 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')" },
|
{ 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/a3/ab/a65452b4e769552fd5a78c4996d6cf322630d896ddfd55c5433d96485e8b/zensical-0.0.23.tar.gz", hash = "sha256:5c4fc3aaf075df99d8cf41b9f2566e4d588180d9a89493014d3607dfe50ac4bc", size = 3822451, upload-time = "2026-02-11T21:24:38.373Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/86/035aa02bd36d26a03a1885bc22a73d4fe61ba0e21d0033cc42baf13d24f6/zensical-0.0.23-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35d6d3eb803fe73a67187a1a25443408bd02a8dd50e151f4a4bafd40de3f0928", size = 12242966, upload-time = "2026-02-11T21:24:05.894Z" },
|
{ 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/be/68/335dfbb7efc972964f0610736a0ad243dd8a5dcc2ec76b9ddb84c847a4a4/zensical-0.0.23-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5973267460a190f348f24d445ff0c01e8ed334fd075947687b305e68257f6b18", size = 12125173, upload-time = "2026-02-11T21:24:08.507Z" },
|
{ 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/25/9c/d567da04fbeb077df5cf06a94f947af829ebef0ff5ca7d0ba4910a6cbdf6/zensical-0.0.23-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:953adf1f0b346a6c65fc6e05e6cc1c38a6440fec29c50c76fb29700cc1927006", size = 12489636, upload-time = "2026-02-11T21:24:10.91Z" },
|
{ 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/fe/6e/481a3ecf8a7b63a35c67f5be1ea548185d55bb1dacead54f76a9550197b2/zensical-0.0.23-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49c1cbd6131dafa056be828e081759184f9b8dd24b99bf38d1e77c8c31b0c720", size = 12421313, upload-time = "2026-02-11T21:24:13.9Z" },
|
{ 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/ba/aa/a95481547f708432636f5f8155917c90d877c244c62124a084f7448b60b2/zensical-0.0.23-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b7fe22c5d33b2b91899c5df7631ad4ce9cccfabac2560cc92ba73eafe2d297", size = 12761031, upload-time = "2026-02-11T21:24:17.016Z" },
|
{ 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/c1/9f/ce1c5af9afd11fe3521a90441aba48c484f98730c6d833d69ee4387ae2e9/zensical-0.0.23-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a3679d6bf6374f503afb74d9f6061da5de83c25922f618042b63a30b16f0389", size = 12527415, upload-time = "2026-02-11T21:24:19.558Z" },
|
{ 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/a8/b8/13a5d4d99f3b77e7bf4e791ef991a611ca2f108ed7eddf20858544ab0a91/zensical-0.0.23-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:54d981e21a19c3dcec6e7fa77c4421db47389dfdff20d29fea70df8e1be4062e", size = 12665352, upload-time = "2026-02-11T21:24:22.703Z" },
|
{ 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/ad/84/3d0a187ed941826ca26b19a661c41685d8017b2a019afa0d353eb2ebbdba/zensical-0.0.23-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:afde7865cc3c79c99f6df4a911d638fb2c3b472a1b81367d47163f8e3c36f910", size = 12689042, upload-time = "2026-02-11T21:24:26.118Z" },
|
{ 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/f0/65/12466408f428f2cf7140b32d484753db0891debae3c956f4c076b51eeb17/zensical-0.0.23-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c484674d7b0a3e6d39db83914db932249bccdef2efaf8a5669671c66c16f584d", size = 12834779, upload-time = "2026-02-11T21:24:28.788Z" },
|
{ 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/a9/ab/0771ac6ffb30e4f04c20374e3beca9e71c3f81112219cdbd86cdc0e3d337/zensical-0.0.23-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:927d12fe2851f355fb3206809e04641d6651bdd2ff4afe9c205721aa3a32aa82", size = 12797057, upload-time = "2026-02-11T21:24:31.383Z" },
|
{ 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]]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ repo_name = "paperless-ngx/paperless-ngx"
|
|||||||
nav = [
|
nav = [
|
||||||
"index.md",
|
"index.md",
|
||||||
"setup.md",
|
"setup.md",
|
||||||
|
"migration.md",
|
||||||
"usage.md",
|
"usage.md",
|
||||||
"configuration.md",
|
"configuration.md",
|
||||||
"administration.md",
|
"administration.md",
|
||||||
|
|||||||
Reference in New Issue
Block a user