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.
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/PKG-INFO +94 -8
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/README.md +91 -7
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/pyproject.toml +2 -1
- pypaperless_cli2-0.3.1/src/pypaperless_cli/__init__.py +12 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/api.py +4 -1
- pypaperless_cli2-0.3.1/src/pypaperless_cli/commands/document/edit.py +306 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/show.py +82 -1
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/__init__.py +5 -0
- pypaperless_cli2-0.3.1/src/pypaperless_cli/utils/converters/permissions.py +90 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/groups.py +7 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/types.py +6 -0
- pypaperless_cli2-0.2.0/src/pypaperless_cli/__init__.py +0 -0
- pypaperless_cli2-0.2.0/src/pypaperless_cli/commands/document/edit.py +0 -159
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/LICENSE +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/app.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/auth.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/account.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/config/config.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/const.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/py.typed +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/custom_field.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/date.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/helpers/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/helpers/strtobool.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/tag.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/highlighter.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/__init__.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/custom_field.py +0 -0
- {pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/document.py +0 -0
- {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.
|
|
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)
|
|
29
|
+
This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli).
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
I
|
|
31
|
-
|
|
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)
|
|
5
|
+
This is a fork of [Marcel Brückner's `pypaperless-cli`](https://github.com/marcelbrueckner/paperless-ngx-cli).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
I
|
|
9
|
-
|
|
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.
|
|
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":
|
|
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))
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/show.py
RENAMED
|
@@ -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:
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/__init__.py
RENAMED
|
@@ -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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/commands/document/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/date.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/converters/tag.py
RENAMED
|
File without changes
|
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/document.py
RENAMED
|
File without changes
|
{pypaperless_cli2-0.2.0 → pypaperless_cli2-0.3.1}/src/pypaperless_cli/utils/validators/tag.py
RENAMED
|
File without changes
|