pypaperless-cli2 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/PKG-INFO +94 -8
  2. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/README.md +91 -7
  3. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/pyproject.toml +2 -1
  4. pypaperless_cli2-0.3.1/src/pypaperless_cli/__init__.py +12 -0
  5. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/api.py +4 -1
  6. pypaperless_cli2-0.3.1/src/pypaperless_cli/commands/document/edit.py +306 -0
  7. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/show.py +82 -1
  8. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/__init__.py +5 -0
  9. pypaperless_cli2-0.3.1/src/pypaperless_cli/utils/converters/permissions.py +90 -0
  10. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/groups.py +7 -0
  11. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/types.py +6 -0
  12. pypaperless_cli2-0.2.0/src/pypaperless_cli/__init__.py +0 -0
  13. pypaperless_cli2-0.2.0/src/pypaperless_cli/commands/document/edit.py +0 -159
  14. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/LICENSE +0 -0
  15. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/app.py +0 -0
  16. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/__init__.py +0 -0
  17. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/auth.py +0 -0
  18. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/__init__.py +0 -0
  19. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/__init__.py +0 -0
  20. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/account.py +0 -0
  21. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/config.py +0 -0
  22. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/const.py +0 -0
  23. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/py.typed +0 -0
  24. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/__init__.py +0 -0
  25. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/custom_field.py +0 -0
  26. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/date.py +0 -0
  27. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/helpers/__init__.py +0 -0
  28. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/helpers/strtobool.py +0 -0
  29. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/tag.py +0 -0
  30. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/highlighter.py +0 -0
  31. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/__init__.py +0 -0
  32. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/custom_field.py +0 -0
  33. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/document.py +0 -0
  34. {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/tag.py +0 -0
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypaperless-cli2
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Command-line interface for Paperless-ngx (fork of pypaperless-cli, updated for pypaperless 5.x / Paperless-ngx 3.x)
5
+ License: MIT
5
6
  License-File: LICENSE
6
7
  Author: Marcel Brückner
7
8
  Author-email: marcelbrueckner@users.noreply.github.com
8
9
  Requires-Python: >=3.12,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
9
11
  Classifier: Programming Language :: Python :: 3
10
12
  Classifier: Programming Language :: Python :: 3.12
11
13
  Classifier: Programming Language :: Python :: 3.13
@@ -24,16 +26,14 @@ Description-Content-Type: text/markdown
24
26
 
25
27
  Paperless-ngx Command-Line Interface
26
28
 
27
- This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli), republished as `pypaperless-cli2` with the `pypaperless` dependency updated to `^5.1.0` for compatibility with Paperless-ngx ≥3.0 (which rejects the older API versions requested by `pypaperless` 4.x with `406 Not Acceptable`). The `pngx` command and all usage below are unchanged from upstream.
29
+ This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli).
28
30
 
29
- I've recently started a project to collect scripts around Paperless-ngx ([paperless.sh](https://paperless.sh)). It turned out - surprise - to be very tedious to write a separate [script](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/post-consumption/content-matching/pngx-update-document.py) [each](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/api/custom-field-sum/custom-field-sum.py) [time](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/api/custom-field-sum/custom-field-sum-of-differences.py) I need to work with Paperless-ngx's API.
30
- I was looking for a way to easily update certain fields of my documents but without the overhead of "manually" calling API endpoints each time.
31
- I wrapped things in simple python scripts which eventually became this tool which essentially ~~is~~ will become Paperless-ngx at your command-line.
31
+ After updating my Paperless-ngx instances to version 3.x the current pypaperless-cli package has thrown some errors when editing documents.
32
+ I republished the current pypaperless-cli as `pypaperless-cli2` with the `pypaperless` dependency updated to `^5.1.0` for compatibility with Paperless-ngx ≥3.0 (which rejects the older API versions requested by `pypaperless` 4.x with `406 Not Acceptable`).
33
+ The `pngx` command and all usage below are unchanged from upstream.
32
34
 
33
35
  ## Usage
34
36
 
35
- This very first public release currently only allows you to retrieve information or update certain fields of your existing documents (with more to come in the future).
36
-
37
37
  ```bash
38
38
  # Run pngx -h for help
39
39
  $ pngx [ command ] [ subcommand ] [ arguments and parameters ]
@@ -57,7 +57,21 @@ Title 2024-01-10_RE12345678
57
57
  ID 400
58
58
  ASN None
59
59
  Created 2024-01-10
60
- ...
60
+ Correspondent Stadtwerke
61
+ Document type Rechnung
62
+ Storage path None
63
+ Tags Rechnung
64
+ Owner j.pirner
65
+ Details https://paperless.example.com/documents/400/details/
66
+ Permissions
67
+ View
68
+ Users alice, bob
69
+ Groups Buchhaltung
70
+ Change
71
+ Users alice
72
+ Groups None
73
+ Custom fields
74
+ Rechnungsnummer RE12345678
61
75
  ```
62
76
 
63
77
  Update a document's title and correspondent.
@@ -67,6 +81,16 @@ Update a document's title and correspondent.
67
81
  $ pngx document edit <ID> --title "My new document title" --correspondent <CORRESPONDENT_ID>
68
82
  ```
69
83
 
84
+ Update a document's creation date.
85
+
86
+ The Paperless-ngx API expects the date in ISO 8601 format. With Version 0.2.0 of this package comes a dateparser which auto-detects the used date format and converts it to ISO 8601 format.
87
+
88
+ ```bash
89
+ # Set document creation date to "14.09.2026"
90
+ $ pngx document edit <ID> --created-date 14.09.2026
91
+ # The date parser automatically converts the date to 2026-09-14 and uses it in the communication with the API
92
+ ```
93
+
70
94
  Assign or unassign a document's tags.
71
95
 
72
96
  Tags can be specified by ID or the *exact* name. If your tag name contains spaces, wrap it in quotes.
@@ -92,6 +116,68 @@ $ pngx document edit <ID> --add-custom-fields <ID|EXACT_NAME>[=VALUE] [<ID|EXACT
92
116
  $ pngx document edit <ID> --remove-custom-fields <ID|EXACT_NAME> [<ID|EXACT_NAME>]
93
117
  ```
94
118
 
119
+ Set or remove a document's owner.
120
+
121
+ Similar to tags, the owner can be specified by ID or the *exact* (case-insensitive) username.
122
+
123
+ ```bash
124
+ # Set the owner given the ID or _exact_ username
125
+ $ pngx document edit <ID> --owner <ID|EXACT_USERNAME>
126
+ # Remove the owner, leaving the document unowned
127
+ $ pngx document edit <ID> --remove-owner
128
+ ```
129
+
130
+ ```bash
131
+ # Make "j.pirner" the owner of document 1489
132
+ $ pngx document edit 1489 --owner j.pirner
133
+ # Same, but referring to the user by ID
134
+ $ pngx document edit 1489 --owner 3
135
+ ```
136
+
137
+ `--owner` and `--remove-owner` are mutually exclusive.
138
+
139
+ Grant or revoke view and change permissions.
140
+
141
+ Permissions are granted to users and groups separately. Users can be specified by ID or the *exact* (case-insensitive) username, groups by ID or the *exact* group name. If your username or group name contains spaces, wrap it in quotes.
142
+
143
+ You can add and remove multiple users or groups at once, separated by space, and IDs and names can be mixed freely.
144
+
145
+ ```bash
146
+ # Grant or revoke view permission given the ID or _exact_ name
147
+ $ pngx document edit <ID> --add-view-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
148
+ $ pngx document edit <ID> --remove-view-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
149
+ $ pngx document edit <ID> --add-view-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
150
+ $ pngx document edit <ID> --remove-view-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
151
+
152
+ # Grant or revoke change permission given the ID or _exact_ name
153
+ $ pngx document edit <ID> --add-change-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
154
+ $ pngx document edit <ID> --remove-change-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
155
+ $ pngx document edit <ID> --add-change-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
156
+ $ pngx document edit <ID> --remove-change-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
157
+ ```
158
+
159
+ ```bash
160
+ # Let "alice" and user 42 view document 1489
161
+ $ pngx document edit 1489 --add-view-users alice 42
162
+ # Let the group "Buchhaltung" view and change it
163
+ $ pngx document edit 1489 --add-view-groups Buchhaltung --add-change-groups Buchhaltung
164
+ # Revoke "bob"'s change permission
165
+ $ pngx document edit 1489 --remove-change-users bob
166
+ # Group name containing spaces
167
+ $ pngx document edit 1489 --add-view-groups "Externe Buchhaltung"
168
+ ```
169
+
170
+ Permission changes are applied incrementally: existing entries you don't mention are kept. All permission parameters can be combined in a single call, together with any other parameter.
171
+
172
+ ```bash
173
+ # Hand the document over to the accounting department in one go
174
+ $ pngx document edit 1489 --title "Stadtwerke Jahresabrechnung 2024" --owner j.pirner \
175
+ --add-view-groups Buchhaltung --add-view-users alice 42 \
176
+ --add-change-users alice --remove-change-users bob
177
+ ```
178
+
179
+ `pngx document show` displays the owner and the full permission table (see above). Resolving users and groups to names requires an account that may read `/api/users/` and `/api/groups/`. If it may not, IDs are shown instead and `--owner`/`--add-*`/`--remove-*` only accept numeric IDs.
180
+
95
181
  ## Configuration
96
182
 
97
183
  The Paperless-ngx CLI client can be configured in a variety of ways.
@@ -2,16 +2,14 @@
2
2
 
3
3
  Paperless-ngx Command-Line Interface
4
4
 
5
- This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli), republished as `pypaperless-cli2` with the `pypaperless` dependency updated to `^5.1.0` for compatibility with Paperless-ngx ≥3.0 (which rejects the older API versions requested by `pypaperless` 4.x with `406 Not Acceptable`). The `pngx` command and all usage below are unchanged from upstream.
5
+ This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli).
6
6
 
7
- I've recently started a project to collect scripts around Paperless-ngx ([paperless.sh](https://paperless.sh)). It turned out - surprise - to be very tedious to write a separate [script](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/post-consumption/content-matching/pngx-update-document.py) [each](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/api/custom-field-sum/custom-field-sum.py) [time](https://github.com/marcelbrueckner/paperless.sh/blob/6cc85b20482e0281d9e26ab82fcaccf34e27a276/scripts/api/custom-field-sum/custom-field-sum-of-differences.py) I need to work with Paperless-ngx's API.
8
- I was looking for a way to easily update certain fields of my documents but without the overhead of "manually" calling API endpoints each time.
9
- I wrapped things in simple python scripts which eventually became this tool which essentially ~~is~~ will become Paperless-ngx at your command-line.
7
+ After updating my Paperless-ngx instances to version 3.x the current pypaperless-cli package has thrown some errors when editing documents.
8
+ I republished the current pypaperless-cli as `pypaperless-cli2` with the `pypaperless` dependency updated to `^5.1.0` for compatibility with Paperless-ngx ≥3.0 (which rejects the older API versions requested by `pypaperless` 4.x with `406 Not Acceptable`).
9
+ The `pngx` command and all usage below are unchanged from upstream.
10
10
 
11
11
  ## Usage
12
12
 
13
- This very first public release currently only allows you to retrieve information or update certain fields of your existing documents (with more to come in the future).
14
-
15
13
  ```bash
16
14
  # Run pngx -h for help
17
15
  $ pngx [ command ] [ subcommand ] [ arguments and parameters ]
@@ -35,7 +33,21 @@ Title 2024-01-10_RE12345678
35
33
  ID 400
36
34
  ASN None
37
35
  Created 2024-01-10
38
- ...
36
+ Correspondent Stadtwerke
37
+ Document type Rechnung
38
+ Storage path None
39
+ Tags Rechnung
40
+ Owner j.pirner
41
+ Details https://paperless.example.com/documents/400/details/
42
+ Permissions
43
+ View
44
+ Users alice, bob
45
+ Groups Buchhaltung
46
+ Change
47
+ Users alice
48
+ Groups None
49
+ Custom fields
50
+ Rechnungsnummer RE12345678
39
51
  ```
40
52
 
41
53
  Update a document's title and correspondent.
@@ -45,6 +57,16 @@ Update a document's title and correspondent.
45
57
  $ pngx document edit <ID> --title "My new document title" --correspondent <CORRESPONDENT_ID>
46
58
  ```
47
59
 
60
+ Update a document's creation date.
61
+
62
+ The Paperless-ngx API expects the date in ISO 8601 format. With Version 0.2.0 of this package comes a dateparser which auto-detects the used date format and converts it to ISO 8601 format.
63
+
64
+ ```bash
65
+ # Set document creation date to "14.09.2026"
66
+ $ pngx document edit <ID> --created-date 14.09.2026
67
+ # The date parser automatically converts the date to 2026-09-14 and uses it in the communication with the API
68
+ ```
69
+
48
70
  Assign or unassign a document's tags.
49
71
 
50
72
  Tags can be specified by ID or the *exact* name. If your tag name contains spaces, wrap it in quotes.
@@ -70,6 +92,68 @@ $ pngx document edit <ID> --add-custom-fields <ID|EXACT_NAME>[=VALUE] [<ID|EXACT
70
92
  $ pngx document edit <ID> --remove-custom-fields <ID|EXACT_NAME> [<ID|EXACT_NAME>]
71
93
  ```
72
94
 
95
+ Set or remove a document's owner.
96
+
97
+ Similar to tags, the owner can be specified by ID or the *exact* (case-insensitive) username.
98
+
99
+ ```bash
100
+ # Set the owner given the ID or _exact_ username
101
+ $ pngx document edit <ID> --owner <ID|EXACT_USERNAME>
102
+ # Remove the owner, leaving the document unowned
103
+ $ pngx document edit <ID> --remove-owner
104
+ ```
105
+
106
+ ```bash
107
+ # Make "j.pirner" the owner of document 1489
108
+ $ pngx document edit 1489 --owner j.pirner
109
+ # Same, but referring to the user by ID
110
+ $ pngx document edit 1489 --owner 3
111
+ ```
112
+
113
+ `--owner` and `--remove-owner` are mutually exclusive.
114
+
115
+ Grant or revoke view and change permissions.
116
+
117
+ Permissions are granted to users and groups separately. Users can be specified by ID or the *exact* (case-insensitive) username, groups by ID or the *exact* group name. If your username or group name contains spaces, wrap it in quotes.
118
+
119
+ You can add and remove multiple users or groups at once, separated by space, and IDs and names can be mixed freely.
120
+
121
+ ```bash
122
+ # Grant or revoke view permission given the ID or _exact_ name
123
+ $ pngx document edit <ID> --add-view-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
124
+ $ pngx document edit <ID> --remove-view-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
125
+ $ pngx document edit <ID> --add-view-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
126
+ $ pngx document edit <ID> --remove-view-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
127
+
128
+ # Grant or revoke change permission given the ID or _exact_ name
129
+ $ pngx document edit <ID> --add-change-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
130
+ $ pngx document edit <ID> --remove-change-users <ID|EXACT_USERNAME> [<ID|EXACT_USERNAME>]
131
+ $ pngx document edit <ID> --add-change-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
132
+ $ pngx document edit <ID> --remove-change-groups <ID|EXACT_NAME> [<ID|EXACT_NAME>]
133
+ ```
134
+
135
+ ```bash
136
+ # Let "alice" and user 42 view document 1489
137
+ $ pngx document edit 1489 --add-view-users alice 42
138
+ # Let the group "Buchhaltung" view and change it
139
+ $ pngx document edit 1489 --add-view-groups Buchhaltung --add-change-groups Buchhaltung
140
+ # Revoke "bob"'s change permission
141
+ $ pngx document edit 1489 --remove-change-users bob
142
+ # Group name containing spaces
143
+ $ pngx document edit 1489 --add-view-groups "Externe Buchhaltung"
144
+ ```
145
+
146
+ Permission changes are applied incrementally: existing entries you don't mention are kept. All permission parameters can be combined in a single call, together with any other parameter.
147
+
148
+ ```bash
149
+ # Hand the document over to the accounting department in one go
150
+ $ pngx document edit 1489 --title "Stadtwerke Jahresabrechnung 2024" --owner j.pirner \
151
+ --add-view-groups Buchhaltung --add-view-users alice 42 \
152
+ --add-change-users alice --remove-change-users bob
153
+ ```
154
+
155
+ `pngx document show` displays the owner and the full permission table (see above). Resolving users and groups to names requires an account that may read `/api/users/` and `/api/groups/`. If it may not, IDs are shown instead and `--owner`/`--add-*`/`--remove-*` only accept numeric IDs.
156
+
73
157
  ## Configuration
74
158
 
75
159
  The Paperless-ngx CLI client can be configured in a variety of ways.
@@ -1,9 +1,10 @@
1
1
  [tool.poetry]
2
2
  name = "pypaperless-cli2"
3
- version = "0.2.0"
3
+ version = "0.3.1"
4
4
  description = "Command-line interface for Paperless-ngx (fork of pypaperless-cli, updated for pypaperless 5.x / Paperless-ngx 3.x)"
5
5
  authors = ["Marcel Brückner <marcelbrueckner@users.noreply.github.com>"]
6
6
  readme = "README.md"
7
+ license = "MIT"
7
8
  homepage = "https://gitlab.jpi-it.com/j.pirner/pypaperless-cli2"
8
9
  repository = "https://gitlab.jpi-it.com/j.pirner/pypaperless-cli2"
9
10
  packages = [{include = "pypaperless_cli", from = "src"}]
@@ -0,0 +1,12 @@
1
+ """Paperless-ngx command-line interface."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ # The importable module is `pypaperless_cli`, but the distribution is named
7
+ # `pypaperless-cli2`. Cyclopts derives `--version` from the root module name,
8
+ # which doesn't resolve here, so it needs this `__version__` to fall back on
9
+ # (otherwise it reports "0.0.0").
10
+ __version__ = version("pypaperless-cli2")
11
+ except PackageNotFoundError: # not installed, e.g. running from a source checkout
12
+ __version__ = "0.0.0+unknown"
@@ -3,13 +3,16 @@
3
3
  from aiohttp import ClientSession
4
4
  from pypaperless import Paperless
5
5
 
6
+ from pypaperless_cli import __version__
6
7
  from pypaperless_cli.config import config as appconfig
7
8
 
9
+ USER_AGENT = f"pypaperless-cli2/{__version__} (https://gitlab.jpi-it.com/j.pirner/pypaperless-cli2)"
10
+
8
11
  class PaperlessAsyncAPI(Paperless):
9
12
  """Represent the Paperless API"""
10
13
 
11
14
  def __init__(self):
12
- session = ClientSession(headers={"User-Agent": f"pypaperless-cli/0.1-dev (https://github.com/marcelbrueckner/paperless-ngx-cli)"})
15
+ session = ClientSession(headers={"User-Agent": USER_AGENT})
13
16
  super().__init__(appconfig.current.host, appconfig.current.token, session=session)
14
17
 
15
18
  # Don't care about warnings
@@ -0,0 +1,306 @@
1
+ """Method for editing documents."""
2
+
3
+ from typing import Annotated, List, Optional
4
+
5
+ from cyclopts import Group, Parameter
6
+
7
+ from pypaperless.models.common import CustomFieldValue
8
+
9
+ from pypaperless_cli.api import PaperlessAsyncAPI
10
+ from pypaperless_cli.utils import converters, groups, validators
11
+ from pypaperless_cli.utils.types import CustomFieldKeyValue, Date, Document, Owner
12
+
13
+ group_tags = Group(name = "Tags parameters", sort_key=groups.standard_fields.sort_key+1)
14
+ group_custom_fields = Group(name = "Custom fields parameters", sort_key=group_tags.sort_key+1)
15
+ group_permissions = Group(name = "Permissions parameters", sort_key=group_custom_fields.sort_key+1)
16
+
17
+ async def edit(
18
+ id: Document,
19
+ /, *,
20
+ asn: Optional[int] = None,
21
+ correspondent: Optional[int] = None,
22
+ document_type: Optional[int] = None,
23
+ storage_path: Optional[int] = None,
24
+ title: Optional[str] = None,
25
+ created_date: Optional[Date] = None,
26
+
27
+ # Handle tags
28
+ add_tags: Annotated[
29
+ Optional[List[str|int]],
30
+ Parameter(
31
+ name = ["--tags", "--add-tags"],
32
+ negative = [],
33
+ group = group_tags,
34
+ # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
35
+ # thus explicitly adding it to parameter
36
+ converter = converters.tag_name_to_id,
37
+ validator = validators.tag_exists
38
+ )] = None,
39
+ remove_tags: Annotated[
40
+ Optional[List[str|int]],
41
+ Parameter(
42
+ negative = [],
43
+ group = group_tags,
44
+ # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
45
+ # thus explicitly adding it to parameter
46
+ converter = converters.tag_name_to_id,
47
+ validator = validators.tag_exists
48
+ )] = None,
49
+
50
+ add_custom_fields: Annotated[
51
+ Optional[List[CustomFieldKeyValue]],
52
+ Parameter(
53
+ name = ["--custom-fields", "--add-custom-fields"],
54
+ negative = [],
55
+ group = group_custom_fields,
56
+ # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
57
+ # thus explicitly adding it to parameter
58
+ converter = converters.custom_field_name_to_id,
59
+ validator = validators.custom_field_exists
60
+ )] = None,
61
+ remove_custom_fields: Annotated[
62
+ Optional[List[CustomFieldKeyValue]],
63
+ Parameter(
64
+ negative = [],
65
+ group = group_custom_fields,
66
+ # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
67
+ # thus explicitly adding it to parameter
68
+ converter = converters.custom_field_name_to_id,
69
+ validator = validators.custom_field_exists
70
+ )] = None,
71
+
72
+ # Handle permissions
73
+ owner: Annotated[
74
+ Optional[Owner],
75
+ Parameter(
76
+ group = [group_permissions, groups.owner_xor_remove_owner]
77
+ )] = None,
78
+ remove_owner: Annotated[
79
+ Optional[bool],
80
+ Parameter(
81
+ negative = [],
82
+ show_default = False,
83
+ group = [group_permissions, groups.owner_xor_remove_owner]
84
+ )] = False,
85
+ add_view_users: Annotated[
86
+ Optional[List[str|int]],
87
+ Parameter(
88
+ name = ["--view-users", "--add-view-users"],
89
+ negative = [],
90
+ group = group_permissions,
91
+ # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
92
+ # thus explicitly adding it to parameter
93
+ converter = converters.user_name_to_id
94
+ )] = None,
95
+ remove_view_users: Annotated[
96
+ Optional[List[str|int]],
97
+ Parameter(
98
+ negative = [],
99
+ group = group_permissions,
100
+ converter = converters.user_name_to_id
101
+ )] = None,
102
+ add_view_groups: Annotated[
103
+ Optional[List[str|int]],
104
+ Parameter(
105
+ name = ["--view-groups", "--add-view-groups"],
106
+ negative = [],
107
+ group = group_permissions,
108
+ converter = converters.group_name_to_id
109
+ )] = None,
110
+ remove_view_groups: Annotated[
111
+ Optional[List[str|int]],
112
+ Parameter(
113
+ negative = [],
114
+ group = group_permissions,
115
+ converter = converters.group_name_to_id
116
+ )] = None,
117
+ add_change_users: Annotated[
118
+ Optional[List[str|int]],
119
+ Parameter(
120
+ name = ["--change-users", "--add-change-users"],
121
+ negative = [],
122
+ group = group_permissions,
123
+ converter = converters.user_name_to_id
124
+ )] = None,
125
+ remove_change_users: Annotated[
126
+ Optional[List[str|int]],
127
+ Parameter(
128
+ negative = [],
129
+ group = group_permissions,
130
+ converter = converters.user_name_to_id
131
+ )] = None,
132
+ add_change_groups: Annotated[
133
+ Optional[List[str|int]],
134
+ Parameter(
135
+ name = ["--change-groups", "--add-change-groups"],
136
+ negative = [],
137
+ group = group_permissions,
138
+ converter = converters.group_name_to_id
139
+ )] = None,
140
+ remove_change_groups: Annotated[
141
+ Optional[List[str|int]],
142
+ Parameter(
143
+ negative = [],
144
+ group = group_permissions,
145
+ converter = converters.group_name_to_id
146
+ )] = None
147
+ ) -> None:
148
+
149
+ """Update a document's information.
150
+
151
+ Parameters
152
+ ----------
153
+ id: int
154
+ The ID of the document to be updated.
155
+ asn: int
156
+ Archive serial number. The unique identifier of the document in your physical document binders.
157
+ correspondent: int
158
+ ID of the correspondent.
159
+ document_type: int
160
+ ID of the document type.
161
+ storage_path: int
162
+ ID of the storage path.
163
+ title: str
164
+ Document title
165
+ created_date: str
166
+ The date the document was initially issued. Accepts several formats, e.g. YYYY-MM-DD
167
+ (ISO 8601) or DD.MM.YYYY (German), and normalizes them automatically.
168
+
169
+ add_tags: List[str|int]
170
+ Assign tags. Requires the ID or the exact name of the tags.
171
+ remove_tags: List[str|int]
172
+ Unassign tags. Requires the ID or the exact name of the tags.
173
+
174
+ add_custom_fields: List[CustomFieldKeyValue]
175
+ Assign custom fields (--custom-fields <NAME|ID>), optionally set a value (--custom-fields <NAME|ID>=VALUE).
176
+ To clear a custom field, set VALUE to an empty string.
177
+ remove_custom_fields: List[CustomFieldKeyValue]
178
+ Unassign given custom fields.
179
+
180
+ owner: int
181
+ Set the document owner. Requires the ID or the exact username.
182
+ remove_owner: bool
183
+ Remove the document owner, making the document unowned.
184
+ add_view_users: List[str|int]
185
+ Grant view permission to users. Requires the ID or the exact username.
186
+ remove_view_users: List[str|int]
187
+ Revoke view permission from users. Requires the ID or the exact username.
188
+ add_view_groups: List[str|int]
189
+ Grant view permission to groups. Requires the ID or the exact group name.
190
+ remove_view_groups: List[str|int]
191
+ Revoke view permission from groups. Requires the ID or the exact group name.
192
+ add_change_users: List[str|int]
193
+ Grant change permission to users. Requires the ID or the exact username.
194
+ remove_change_users: List[str|int]
195
+ Revoke change permission from users. Requires the ID or the exact username.
196
+ add_change_groups: List[str|int]
197
+ Grant change permission to groups. Requires the ID or the exact group name.
198
+ remove_change_groups: List[str|int]
199
+ Revoke change permission from groups. Requires the ID or the exact group name.
200
+ """
201
+
202
+ async with PaperlessAsyncAPI() as paperless:
203
+ # Define how document should be updated
204
+ only_changed: bool = True
205
+
206
+ permission_changes = [
207
+ add_view_users, remove_view_users,
208
+ add_view_groups, remove_view_groups,
209
+ add_change_users, remove_change_users,
210
+ add_change_groups, remove_change_groups
211
+ ]
212
+
213
+ if any(permission_changes):
214
+ # `document.permissions` is only populated when the document is requested
215
+ # with `full_perms=true`. Without it, pypaperless silently skips renaming
216
+ # `permissions` to `set_permissions` on update (see its UpdatableMixin.
217
+ # _check_permissions_field), so every permission change would be lost.
218
+ # Deliberately not enabled for plain edits, to keep them off that code path.
219
+ paperless.documents.request_permissions = True
220
+
221
+ document = await paperless.documents(id)
222
+
223
+ if asn:
224
+ document.archive_serial_number = asn
225
+
226
+ if correspondent:
227
+ document.correspondent = correspondent
228
+
229
+ if document_type:
230
+ document.document_type = document_type
231
+
232
+ if storage_path:
233
+ document.storage_path = storage_path
234
+
235
+ if title:
236
+ document.title = title
237
+
238
+ if created_date:
239
+ document.created = created_date
240
+
241
+ if remove_tags:
242
+ # Only keep tags not in `remove_tags``
243
+ document.tags = [t for t in document.tags if t not in remove_tags]
244
+
245
+ if add_tags:
246
+ # Union existing and new tags, removing duplicate entries
247
+ document.tags = list(dict.fromkeys(document.tags + add_tags))
248
+
249
+ if remove_custom_fields:
250
+ # Remove given custom field if it's assigned to document
251
+ for f in remove_custom_fields:
252
+ document.custom_fields.remove(f["id"])
253
+
254
+ # Custom fields are only updated by paperless-api when updating all fields (PUT)
255
+ only_changed = False
256
+
257
+ if add_custom_fields:
258
+ # Update existing custom fields with possibly new values, or add new ones
259
+ for f in add_custom_fields:
260
+ existing_custom_field = document.custom_fields.default(f["id"])
261
+ if existing_custom_field is not None:
262
+ if f["value"] is not None:
263
+ existing_custom_field.value = f["value"]
264
+ else:
265
+ document.custom_fields.add(CustomFieldValue(field=f["id"], value=f["value"]))
266
+
267
+ # Custom fields are only updated by paperless-api when updating all fields (PUT)
268
+ only_changed = False
269
+
270
+ if remove_owner:
271
+ document.owner = None
272
+ elif owner is not None:
273
+ document.owner = owner
274
+
275
+ if any(permission_changes):
276
+ if document.permissions is None:
277
+ raise ValueError(
278
+ "Paperless-ngx did not return the permissions of this document. "
279
+ "Editing permissions requires an account allowed to view them."
280
+ )
281
+
282
+ def _apply(
283
+ current: List[int],
284
+ add: Optional[List],
285
+ remove: Optional[List]
286
+ ) -> List[int]:
287
+ """Remove, then union, keeping the order and dropping duplicates."""
288
+
289
+ if remove:
290
+ current = [i for i in current if i not in remove]
291
+ if add:
292
+ current = list(dict.fromkeys(current + add))
293
+
294
+ return current
295
+
296
+ permissions = document.permissions
297
+
298
+ permissions.view.users = _apply(permissions.view.users, add_view_users, remove_view_users)
299
+ permissions.view.groups = _apply(permissions.view.groups, add_view_groups, remove_view_groups)
300
+ permissions.change.users = _apply(permissions.change.users, add_change_users, remove_change_users)
301
+ permissions.change.groups = _apply(permissions.change.groups, add_change_groups, remove_change_groups)
302
+
303
+ try:
304
+ await document.update(only_changed=only_changed)
305
+ except Exception as e:
306
+ raise ValueError(str(e))
@@ -13,6 +13,39 @@ from pypaperless_cli.config import config as appconfig
13
13
  from pypaperless_cli.utils.highlighter import highlight_none
14
14
  from pypaperless_cli.utils.types import Document
15
15
 
16
+
17
+ async def _principal_names(paperless) -> tuple[dict, dict]:
18
+ """Return ID -> name maps for users and groups.
19
+
20
+ Either map is empty if the current account may not list that endpoint, in
21
+ which case the caller falls back to displaying bare IDs.
22
+ """
23
+
24
+ users = {}
25
+ groups = {}
26
+
27
+ try:
28
+ users = {user.id: user.username for user in await paperless.users.as_list()}
29
+ except Exception:
30
+ pass
31
+
32
+ try:
33
+ groups = {group.id: group.name for group in await paperless.groups.as_list()}
34
+ except Exception:
35
+ pass
36
+
37
+ return users, groups
38
+
39
+
40
+ def _format_principals(ids: list, names: dict) -> str|None:
41
+ """Render user/group IDs as names, falling back to the bare ID."""
42
+
43
+ if not ids:
44
+ return None
45
+
46
+ return ", ".join(str(names.get(id, id)) for id in ids)
47
+
48
+
16
49
  async def show(
17
50
  id: Document, /, *,
18
51
  json: Annotated[Optional[bool], Parameter(
@@ -32,8 +65,11 @@ async def show(
32
65
  """
33
66
 
34
67
  async with PaperlessAsyncAPI() as paperless:
68
+ # Request the `permissions` table alongside the document (`full_perms=true`),
69
+ # otherwise `document.permissions` stays `None`
70
+ paperless.documents.request_permissions = True
35
71
  document = await paperless.documents(id)
36
-
72
+
37
73
  # Everything except created date is optional
38
74
  # therefore initialize possibly empty fields
39
75
  doc_title = None
@@ -42,6 +78,8 @@ async def show(
42
78
  storage_path = None
43
79
  tags = []
44
80
  custom_fields = []
81
+ owner = None
82
+ permissions: Optional[dict] = None
45
83
 
46
84
  if len(document.title) > 0:
47
85
  doc_title = document.title
@@ -81,6 +119,37 @@ async def show(
81
119
  "data_type": field.data_type
82
120
  })
83
121
 
122
+ # `document.permissions` is only set when requested with `full_perms=true`
123
+ doc_permissions = document.permissions
124
+
125
+ # Only pay for the users/groups lookups if there is something to resolve.
126
+ # Without them, `_format_principals` falls back to displaying bare IDs.
127
+ needs_names = document.owner is not None or (
128
+ doc_permissions is not None and any([
129
+ doc_permissions.view.users,
130
+ doc_permissions.view.groups,
131
+ doc_permissions.change.users,
132
+ doc_permissions.change.groups
133
+ ])
134
+ )
135
+
136
+ users, groups = await _principal_names(paperless) if needs_names else ({}, {})
137
+
138
+ if document.owner is not None:
139
+ owner = users.get(document.owner, document.owner)
140
+
141
+ if doc_permissions is not None:
142
+ permissions = {
143
+ "view": {
144
+ "users": _format_principals(doc_permissions.view.users, users),
145
+ "groups": _format_principals(doc_permissions.view.groups, groups)
146
+ },
147
+ "change": {
148
+ "users": _format_principals(doc_permissions.change.users, users),
149
+ "groups": _format_principals(doc_permissions.change.groups, groups)
150
+ }
151
+ }
152
+
84
153
  if json:
85
154
  Console().print_json(data=document._data)
86
155
 
@@ -108,8 +177,20 @@ async def show(
108
177
  else:
109
178
  table.add_row("Tags", highlight_none(str(None)))
110
179
 
180
+ table.add_row("Owner", highlight_none(str(owner)))
111
181
  table.add_row("Details", f"{appconfig.current.host}{GUI_PATH['documents_details'].format(pk=document.id)}")
112
182
 
183
+ table.add_row("[white]Permissions")
184
+ if permissions is not None:
185
+ table.add_row("View")
186
+ table.add_row(" Users", highlight_none(str(permissions["view"]["users"])))
187
+ table.add_row(" Groups", highlight_none(str(permissions["view"]["groups"])))
188
+ table.add_row("Change")
189
+ table.add_row(" Users", highlight_none(str(permissions["change"]["users"])))
190
+ table.add_row(" Groups", highlight_none(str(permissions["change"]["groups"])))
191
+ else:
192
+ table.add_row(highlight_none(str(None)))
193
+
113
194
  table.add_row("[white]Custom fields")
114
195
  if custom_fields:
115
196
  for custom_field in custom_fields:
@@ -5,6 +5,11 @@ from typing import Any
5
5
 
6
6
  from pypaperless_cli.utils.converters.custom_field import custom_field_name_to_id
7
7
  from pypaperless_cli.utils.converters.date import parse_date
8
+ from pypaperless_cli.utils.converters.permissions import (
9
+ group_name_to_id,
10
+ owner_name_to_id,
11
+ user_name_to_id
12
+ )
8
13
  from pypaperless_cli.utils.converters.tag import tag_name_to_id
9
14
 
10
15
  import pypaperless_cli.utils.converters.helpers
@@ -0,0 +1,90 @@
1
+ """User and group related converters"""
2
+
3
+ import asyncio
4
+ from typing import Any, Dict, List, Set, Tuple
5
+
6
+ from pypaperless_cli.api import PaperlessAsyncAPI
7
+
8
+
9
+ async def _fetch_principals(kind: str) -> Tuple[Dict[str, int], Set[int]]:
10
+ """Return a (lowercased name -> ID) mapping and the set of known IDs.
11
+
12
+ Unlike tags and custom fields, `/api/users/` and `/api/groups/` are requested
13
+ unfiltered: Paperless-ngx doesn't document a name filter for either endpoint,
14
+ and both lists are small enough that one request resolves (and validates)
15
+ every value of a single invocation. That's also why resolution and existence
16
+ checking both happen here instead of in a separate validator.
17
+ """
18
+
19
+ async with PaperlessAsyncAPI() as paperless:
20
+ if kind == "user":
21
+ users = await paperless.users.as_list()
22
+ return (
23
+ {(user.username or "").lower(): user.id for user in users},
24
+ {user.id for user in users}
25
+ )
26
+
27
+ groups = await paperless.groups.as_list()
28
+ return (
29
+ {(group.name or "").lower(): group.id for group in groups},
30
+ {group.id for group in groups}
31
+ )
32
+
33
+
34
+ async def _resolve(kind: str, values: List[str|int]) -> List[int]:
35
+ """Resolve a mixed list of IDs and names into a list of IDs."""
36
+
37
+ try:
38
+ names, ids = await _fetch_principals(kind)
39
+
40
+ except Exception as e:
41
+ # Listing users/groups requires elevated privileges on some instances.
42
+ # Numeric IDs can still be passed through unvalidated in that case.
43
+ if any(not str(value).isdigit() for value in values):
44
+ raise ValueError(
45
+ f"Could not read the {kind} list from Paperless-ngx ({e}). "
46
+ f"Refer to {kind}s by numeric ID instead of name."
47
+ )
48
+
49
+ return [int(value) for value in values]
50
+
51
+ resolved: List[int] = []
52
+ unknown: List[str] = []
53
+
54
+ for value in values:
55
+ if str(value).isdigit():
56
+ if int(value) in ids:
57
+ resolved.append(int(value))
58
+ else:
59
+ unknown.append(f"ID {value}")
60
+ else:
61
+ id = names.get(str(value).lower())
62
+ if id is None:
63
+ unknown.append(f"\"{value}\"")
64
+ else:
65
+ resolved.append(id)
66
+
67
+ if len(unknown) == 1:
68
+ raise ValueError(f"{kind.capitalize()} {unknown[0]} does not exist.")
69
+ if len(unknown) > 1:
70
+ raise ValueError(f"{kind.capitalize()}s {', '.join(unknown)} do not exist.")
71
+
72
+ return resolved
73
+
74
+
75
+ def user_name_to_id(type_, *args) -> Any:
76
+ """Determines IDs for usernames, passing numeric IDs through."""
77
+
78
+ return asyncio.run(_resolve("user", list(args)))
79
+
80
+
81
+ def group_name_to_id(type_, *args) -> Any:
82
+ """Determines IDs for group names, passing numeric IDs through."""
83
+
84
+ return asyncio.run(_resolve("group", list(args)))
85
+
86
+
87
+ def owner_name_to_id(type_, *args) -> Any:
88
+ """Determines the ID for a single username, passing a numeric ID through."""
89
+
90
+ return asyncio.run(_resolve("user", [args[0]]))[0]
@@ -32,6 +32,13 @@ password_xor_token = Group(
32
32
  validator=validators.LimitedChoice()
33
33
  )
34
34
 
35
+ # Mutually exclusive owner parameters
36
+ owner_xor_remove_owner = Group(
37
+ "Owner",
38
+ show = False,
39
+ validator = validators.LimitedChoice()
40
+ )
41
+
35
42
  meta_parameters = Group(
36
43
  "Session Parameters",
37
44
  show = False,
@@ -26,6 +26,12 @@ Date = Annotated[date, Parameter(
26
26
  converter = converters.parse_date
27
27
  )]
28
28
 
29
+ # Existence is verified by the converter itself, which already holds the full
30
+ # user list after resolving the name — see converters/permissions.py.
31
+ Owner = Annotated[int, Parameter(
32
+ converter = converters.owner_name_to_id
33
+ )]
34
+
29
35
  CustomFieldKeyValue = Annotated[str|int, Parameter(
30
36
  converter = converters.custom_field_name_to_id,
31
37
  validator = validators.custom_field_exists
File without changes
@@ -1,159 +0,0 @@
1
- """Method for editing documents."""
2
-
3
- from typing import Annotated, List, Optional
4
-
5
- from cyclopts import Group, Parameter
6
-
7
- from pypaperless.models.common import CustomFieldValue
8
-
9
- from pypaperless_cli.api import PaperlessAsyncAPI
10
- from pypaperless_cli.utils import converters, groups, validators
11
- from pypaperless_cli.utils.types import CustomFieldKeyValue, Date, Document
12
-
13
- group_tags = Group(name = "Tags parameters", sort_key=groups.standard_fields.sort_key+1)
14
- group_custom_fields = Group(name = "Custom fields parameters", sort_key=group_tags.sort_key+1)
15
-
16
- async def edit(
17
- id: Document,
18
- /, *,
19
- asn: Optional[int] = None,
20
- correspondent: Optional[int] = None,
21
- document_type: Optional[int] = None,
22
- storage_path: Optional[int] = None,
23
- title: Optional[str] = None,
24
- created_date: Optional[Date] = None,
25
-
26
- # Handle tags
27
- add_tags: Annotated[
28
- Optional[List[str|int]],
29
- Parameter(
30
- name = ["--tags", "--add-tags"],
31
- negative = [],
32
- group = group_tags,
33
- # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
34
- # thus explicitly adding it to parameter
35
- converter = converters.tag_name_to_id,
36
- validator = validators.tag_exists
37
- )] = None,
38
- remove_tags: Annotated[
39
- Optional[List[str|int]],
40
- Parameter(
41
- negative = [],
42
- group = group_tags,
43
- # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
44
- # thus explicitly adding it to parameter
45
- converter = converters.tag_name_to_id,
46
- validator = validators.tag_exists
47
- )] = None,
48
-
49
- add_custom_fields: Annotated[
50
- Optional[List[CustomFieldKeyValue]],
51
- Parameter(
52
- name = ["--custom-fields", "--add-custom-fields"],
53
- negative = [],
54
- group = group_custom_fields,
55
- # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
56
- # thus explicitly adding it to parameter
57
- converter = converters.custom_field_name_to_id,
58
- validator = validators.custom_field_exists
59
- )] = None,
60
- remove_custom_fields: Annotated[
61
- Optional[List[CustomFieldKeyValue]],
62
- Parameter(
63
- negative = [],
64
- group = group_custom_fields,
65
- # Assigning converter/validator to custom type doesn't work with the current version of Cyclopts,
66
- # thus explicitly adding it to parameter
67
- converter = converters.custom_field_name_to_id,
68
- validator = validators.custom_field_exists
69
- )] = None
70
- ) -> None:
71
-
72
- """Update a document's information.
73
-
74
- Parameters
75
- ----------
76
- id: int
77
- The ID of the document to be updated.
78
- asn: int
79
- Archive serial number. The unique identifier of the document in your physical document binders.
80
- correspondent: int
81
- ID of the correspondent.
82
- document_type: int
83
- ID of the document type.
84
- storage_path: int
85
- ID of the storage path.
86
- title: str
87
- Document title
88
- created_date: str
89
- The date the document was initially issued. Accepts several formats, e.g. YYYY-MM-DD
90
- (ISO 8601) or DD.MM.YYYY (German), and normalizes them automatically.
91
-
92
- add_tags: List[str|int]
93
- Assign tags. Requires the ID or the exact name of the tags.
94
- remove_tags: List[str|int]
95
- Unassign tags. Requires the ID or the exact name of the tags.
96
-
97
- add_custom_fields: List[CustomFieldKeyValue]
98
- Assign custom fields (--custom-fields <NAME|ID>), optionally set a value (--custom-fields <NAME|ID>=VALUE).
99
- To clear a custom field, set VALUE to an empty string.
100
- remove_custom_fields: List[CustomFieldKeyValue]
101
- Unassign given custom fields.
102
- """
103
-
104
- async with PaperlessAsyncAPI() as paperless:
105
- # Define how document should be updated
106
- only_changed: bool = True
107
- document = await paperless.documents(id)
108
-
109
- if asn:
110
- document.archive_serial_number = asn
111
-
112
- if correspondent:
113
- document.correspondent = correspondent
114
-
115
- if document_type:
116
- document.document_type = document_type
117
-
118
- if storage_path:
119
- document.storage_path = storage_path
120
-
121
- if title:
122
- document.title = title
123
-
124
- if created_date:
125
- document.created = created_date
126
-
127
- if remove_tags:
128
- # Only keep tags not in `remove_tags``
129
- document.tags = [t for t in document.tags if t not in remove_tags]
130
-
131
- if add_tags:
132
- # Union existing and new tags, removing duplicate entries
133
- document.tags = list(dict.fromkeys(document.tags + add_tags))
134
-
135
- if remove_custom_fields:
136
- # Remove given custom field if it's assigned to document
137
- for f in remove_custom_fields:
138
- document.custom_fields.remove(f["id"])
139
-
140
- # Custom fields are only updated by paperless-api when updating all fields (PUT)
141
- only_changed = False
142
-
143
- if add_custom_fields:
144
- # Update existing custom fields with possibly new values, or add new ones
145
- for f in add_custom_fields:
146
- existing_custom_field = document.custom_fields.default(f["id"])
147
- if existing_custom_field is not None:
148
- if f["value"] is not None:
149
- existing_custom_field.value = f["value"]
150
- else:
151
- document.custom_fields.add(CustomFieldValue(field=f["id"], value=f["value"]))
152
-
153
- # Custom fields are only updated by paperless-api when updating all fields (PUT)
154
- only_changed = False
155
-
156
- try:
157
- await document.update(only_changed=only_changed)
158
- except Exception as e:
159
- raise ValueError(str(e))