pypaperless-cli2 0.1.0__py3-none-any.whl
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_cli/__init__.py +0 -0
- pypaperless_cli/api.py +16 -0
- pypaperless_cli/app.py +186 -0
- pypaperless_cli/commands/__init__.py +6 -0
- pypaperless_cli/commands/auth.py +166 -0
- pypaperless_cli/commands/document/__init__.py +14 -0
- pypaperless_cli/commands/document/edit.py +159 -0
- pypaperless_cli/commands/document/show.py +120 -0
- pypaperless_cli/config/__init__.py +2 -0
- pypaperless_cli/config/account.py +50 -0
- pypaperless_cli/config/config.py +274 -0
- pypaperless_cli/const.py +7 -0
- pypaperless_cli/py.typed +0 -0
- pypaperless_cli/utils/__init__.py +1 -0
- pypaperless_cli/utils/converters/__init__.py +22 -0
- pypaperless_cli/utils/converters/custom_field.py +66 -0
- pypaperless_cli/utils/converters/helpers/__init__.py +3 -0
- pypaperless_cli/utils/converters/helpers/strtobool.py +19 -0
- pypaperless_cli/utils/converters/tag.py +32 -0
- pypaperless_cli/utils/groups.py +39 -0
- pypaperless_cli/utils/highlighter.py +11 -0
- pypaperless_cli/utils/types.py +27 -0
- pypaperless_cli/utils/validators/__init__.py +42 -0
- pypaperless_cli/utils/validators/custom_field.py +32 -0
- pypaperless_cli/utils/validators/document.py +27 -0
- pypaperless_cli/utils/validators/tag.py +31 -0
- pypaperless_cli2-0.1.0.dist-info/METADATA +174 -0
- pypaperless_cli2-0.1.0.dist-info/RECORD +31 -0
- pypaperless_cli2-0.1.0.dist-info/WHEEL +4 -0
- pypaperless_cli2-0.1.0.dist-info/entry_points.txt +3 -0
- pypaperless_cli2-0.1.0.dist-info/licenses/LICENSE +22 -0
|
File without changes
|
pypaperless_cli/api.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Paperless API client"""
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
from pypaperless import Paperless
|
|
5
|
+
|
|
6
|
+
from pypaperless_cli.config import config as appconfig
|
|
7
|
+
|
|
8
|
+
class PaperlessAsyncAPI(Paperless):
|
|
9
|
+
"""Represent the Paperless API"""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
session = ClientSession(headers={"User-Agent": f"pypaperless-cli/0.1-dev (https://github.com/marcelbrueckner/paperless-ngx-cli)"})
|
|
13
|
+
super().__init__(appconfig.current.host, appconfig.current.token, session=session)
|
|
14
|
+
|
|
15
|
+
# Don't care about warnings
|
|
16
|
+
self.logger.setLevel("ERROR")
|
pypaperless_cli/app.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
from cyclopts import App, Parameter
|
|
7
|
+
from cyclopts.types import Path
|
|
8
|
+
from cyclopts.exceptions import format_cyclopts_error
|
|
9
|
+
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from pypaperless_cli.config import config as appconfig
|
|
14
|
+
from pypaperless_cli.utils import groups, validators
|
|
15
|
+
from pypaperless_cli.commands import (
|
|
16
|
+
auth,
|
|
17
|
+
document,
|
|
18
|
+
)
|
|
19
|
+
from pypaperless_cli.utils.types import (
|
|
20
|
+
account_alias,
|
|
21
|
+
URL
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# BASIC APP STRUCTURE
|
|
26
|
+
# Loosely based on the Paperless-ngx API and web interface structure
|
|
27
|
+
# https://cyclopts.readthedocs.io/en/latest/commands.html
|
|
28
|
+
# https://docs.paperless-ngx.com/api/
|
|
29
|
+
|
|
30
|
+
app = App(
|
|
31
|
+
name="pngx",
|
|
32
|
+
help="Command-line interface for Paperless-ngx 🌱",
|
|
33
|
+
group_commands=groups.commands,
|
|
34
|
+
version_flags=["--version", "-v"]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Change the group of "--help" and "--version" to the implicit "Help" group.
|
|
38
|
+
app["--help"].group = "Help"
|
|
39
|
+
app["--version"].group = "Help"
|
|
40
|
+
|
|
41
|
+
app.command(auth)
|
|
42
|
+
app.command(document)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# CLI HELP
|
|
47
|
+
#
|
|
48
|
+
|
|
49
|
+
app.meta["--help"].group = "Help"
|
|
50
|
+
app.meta["--version"].group = "Help"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# CLI entry-point
|
|
55
|
+
#
|
|
56
|
+
|
|
57
|
+
# Set up configuration before running the actual application
|
|
58
|
+
@app.meta.default()
|
|
59
|
+
def main(
|
|
60
|
+
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
|
|
61
|
+
host: Annotated[Optional[URL], Parameter(
|
|
62
|
+
env_var=['PNGX_HOST'],
|
|
63
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc],
|
|
64
|
+
)] = None,
|
|
65
|
+
user: Annotated[Optional[str], Parameter(
|
|
66
|
+
env_var = ['PNGX_USER'],
|
|
67
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc],
|
|
68
|
+
validator = validators.not_empty,
|
|
69
|
+
)] = None,
|
|
70
|
+
password: Annotated[Optional[str], Parameter(
|
|
71
|
+
env_var = ['PNGX_PASSWORD'],
|
|
72
|
+
negative = "--ask-password",
|
|
73
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc, groups.password_xor_token]
|
|
74
|
+
)] = None,
|
|
75
|
+
ask_password: Annotated[Optional[bool], Parameter(
|
|
76
|
+
show = False,
|
|
77
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc, groups.password_xor_token]
|
|
78
|
+
)] = None,
|
|
79
|
+
token: Annotated[Optional[str], Parameter(
|
|
80
|
+
env_var = ['PNGX_TOKEN'],
|
|
81
|
+
negative = "--ask-token",
|
|
82
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc, groups.password_xor_token],
|
|
83
|
+
)] = None,
|
|
84
|
+
ask_token: Annotated[Optional[bool], Parameter(
|
|
85
|
+
show = False,
|
|
86
|
+
group = [groups.meta_parameters, groups.meta_parameters_adhoc, groups.password_xor_token]
|
|
87
|
+
)] = None,
|
|
88
|
+
config_file: Annotated[Optional[Path], Parameter(
|
|
89
|
+
name = "--config",
|
|
90
|
+
env_var = ['PNGX_CONFIG'],
|
|
91
|
+
group = [groups.meta_parameters, groups.meta_parameters_specific]
|
|
92
|
+
)] = None,
|
|
93
|
+
use_account: Annotated[Optional[account_alias], Parameter(
|
|
94
|
+
name = "--use",
|
|
95
|
+
group = [groups.meta_parameters, groups.meta_parameters_specific],
|
|
96
|
+
validator = validators.starts_with_ascii_letters
|
|
97
|
+
)] = None,
|
|
98
|
+
show_config: Annotated[Optional[bool], Parameter(
|
|
99
|
+
group = [groups.meta_parameters, "Help"],
|
|
100
|
+
negative = [],
|
|
101
|
+
show_default = False
|
|
102
|
+
)] = False,
|
|
103
|
+
) -> None:
|
|
104
|
+
|
|
105
|
+
"""Initiate CLI
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
host: str
|
|
110
|
+
The URL of your Paperless-ngx host, possibly including a custom port and/or script path.
|
|
111
|
+
user: str
|
|
112
|
+
Username
|
|
113
|
+
password: str
|
|
114
|
+
Password. Will be used to request an API token only.
|
|
115
|
+
token: str
|
|
116
|
+
API token.
|
|
117
|
+
config_file: Path
|
|
118
|
+
Path to configuration file.
|
|
119
|
+
use_account: str
|
|
120
|
+
Name (alias) of an account that should be used.
|
|
121
|
+
|
|
122
|
+
If an account with the given alias exists, its credentials will be re-used.
|
|
123
|
+
If not specified, the default account will be used (if any).
|
|
124
|
+
show_config: bool
|
|
125
|
+
Show path of the configuration file in use.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if ask_password:
|
|
130
|
+
password = Prompt.ask("What's your password?", password=True)
|
|
131
|
+
|
|
132
|
+
elif ask_token:
|
|
133
|
+
token = Prompt.ask("What's your API token?", password=True)
|
|
134
|
+
|
|
135
|
+
# Parse configuration
|
|
136
|
+
try:
|
|
137
|
+
appconfig.load(config_file, use_account)
|
|
138
|
+
except ValueError as e:
|
|
139
|
+
Console().print(format_cyclopts_error(e))
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
if show_config:
|
|
143
|
+
print(appconfig.filepath.absolute())
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
# Add ad-hoc configuration
|
|
147
|
+
if host and not tokens[:2] == ('auth', 'login'):
|
|
148
|
+
try:
|
|
149
|
+
appconfig.add_account(
|
|
150
|
+
host = host,
|
|
151
|
+
user = user,
|
|
152
|
+
password = password,
|
|
153
|
+
token = token,
|
|
154
|
+
alias = "__adhoc__"
|
|
155
|
+
)
|
|
156
|
+
except ValueError as e:
|
|
157
|
+
Console().print(format_cyclopts_error(e))
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
elif tokens[:2] == ('auth', 'login'):
|
|
161
|
+
# Pass credentials to login function
|
|
162
|
+
if host:
|
|
163
|
+
tokens += (host,)
|
|
164
|
+
if user:
|
|
165
|
+
tokens += ("--user", user)
|
|
166
|
+
if password:
|
|
167
|
+
tokens += ("--password", password)
|
|
168
|
+
if token:
|
|
169
|
+
tokens += ("--token", token)
|
|
170
|
+
|
|
171
|
+
elif not appconfig.list():
|
|
172
|
+
Console().print(format_cyclopts_error("No accounts configured that can be used."))
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
# Now run the actual app
|
|
176
|
+
try:
|
|
177
|
+
app(tokens)
|
|
178
|
+
except ValueError as e:
|
|
179
|
+
Console().print(format_cyclopts_error(e))
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def launch() -> None:
|
|
184
|
+
"""Run commands."""
|
|
185
|
+
|
|
186
|
+
app.meta()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command to manage authentication information.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
from cyclopts import App, Parameter
|
|
9
|
+
from cyclopts.exceptions import format_cyclopts_error
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich import box
|
|
14
|
+
|
|
15
|
+
from pypaperless_cli.utils.types import (
|
|
16
|
+
account_alias,
|
|
17
|
+
URL
|
|
18
|
+
)
|
|
19
|
+
from pypaperless_cli.config import config as appconfig
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Authentication
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
auth = App(name="auth", help="Manage authentication information", version_flags=[])
|
|
26
|
+
auth["--help"].group = "Help"
|
|
27
|
+
|
|
28
|
+
@auth.command
|
|
29
|
+
def login(
|
|
30
|
+
host: Annotated[URL, Parameter(env_var=['PNGX_HOST'])],
|
|
31
|
+
/, *,
|
|
32
|
+
user: Annotated[Optional[str], Parameter(env_var=['PNGX_USER'])] = None,
|
|
33
|
+
password: Annotated[Optional[str], Parameter(env_var=['PNGX_PASSWORD'],negative="--ask-password")] = None,
|
|
34
|
+
token: Annotated[Optional[str], Parameter(env_var=['PNGX_TOKEN'],negative="--ask-token")] = None,
|
|
35
|
+
alias: Optional[account_alias] = "default"
|
|
36
|
+
) -> None:
|
|
37
|
+
|
|
38
|
+
"""Log in to your instance of Paperless-ngx.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
pngx login https://paperless.example.com --user USERNAME --token TOKEN
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
host: str
|
|
47
|
+
The URL of your Paperless-ngx host, possibly including a custom port and/or script path.
|
|
48
|
+
user: str
|
|
49
|
+
Username
|
|
50
|
+
alias: str
|
|
51
|
+
Assign the credentials provided to an account named according to the given alias.
|
|
52
|
+
Allows to easily switch between multiple Paperless-ngx hosts (e.g. personal, business) on subsequent commands.
|
|
53
|
+
|
|
54
|
+
If not specified, an implicit default account will be created.
|
|
55
|
+
|
|
56
|
+
:warning: Providing an existing account alias will overwrite existing credentials.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Add credentials
|
|
60
|
+
try:
|
|
61
|
+
appconfig.add_account(
|
|
62
|
+
host = host,
|
|
63
|
+
user = user,
|
|
64
|
+
password = password,
|
|
65
|
+
token = token,
|
|
66
|
+
alias = alias
|
|
67
|
+
)
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
Console().print(format_cyclopts_error(e))
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@auth.command
|
|
74
|
+
def logout(alias: account_alias = None, /) -> None:
|
|
75
|
+
"""Remove credentials from disk for the given account.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
alias: account_alias
|
|
80
|
+
Name of the account to be logged out from. Defaults to the account currently in use.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if alias:
|
|
84
|
+
appconfig.remove_account(alias)
|
|
85
|
+
else:
|
|
86
|
+
appconfig.remove_account(appconfig.current.alias)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@auth.command
|
|
90
|
+
def use(alias: account_alias, /) -> None:
|
|
91
|
+
"""Set the current account
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
alias: account_alias
|
|
96
|
+
Name of an existing account to be set as the default account.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
appconfig.use_account(alias)
|
|
100
|
+
appconfig.write()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@auth.command
|
|
104
|
+
def show(alias: account_alias = None, /) -> None:
|
|
105
|
+
"""Show details of an existing account.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
alias: account_alias
|
|
110
|
+
Name of an existing account. Defaults to the current account if no alias is given.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
accounts = appconfig.list()
|
|
114
|
+
|
|
115
|
+
if accounts:
|
|
116
|
+
table = Table(box=box.SIMPLE_HEAD)
|
|
117
|
+
table.add_column("Alias")
|
|
118
|
+
table.add_column("Host")
|
|
119
|
+
table.add_column("User")
|
|
120
|
+
|
|
121
|
+
if not alias:
|
|
122
|
+
table.add_row(appconfig.current.alias, appconfig.current.host, appconfig.current.user)
|
|
123
|
+
else:
|
|
124
|
+
account = appconfig.get_account(alias)
|
|
125
|
+
table.add_row(account.alias, account.host, account.user)
|
|
126
|
+
|
|
127
|
+
Console().print(table)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@auth.command
|
|
131
|
+
def list() -> None:
|
|
132
|
+
"""List available accounts"""
|
|
133
|
+
|
|
134
|
+
accounts = appconfig.list()
|
|
135
|
+
|
|
136
|
+
if accounts:
|
|
137
|
+
table = Table(box=box.SIMPLE_HEAD)
|
|
138
|
+
table.add_column("Alias")
|
|
139
|
+
table.add_column("Host")
|
|
140
|
+
table.add_column("User")
|
|
141
|
+
|
|
142
|
+
for account in accounts:
|
|
143
|
+
if account.alias == appconfig.current.alias:
|
|
144
|
+
table.add_row(f"* {account.alias}", account.host, account.user, style="green")
|
|
145
|
+
else:
|
|
146
|
+
table.add_row(f" {account.alias}", account.host, account.user)
|
|
147
|
+
|
|
148
|
+
Console().print(table)
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
Console().print("No accounts configured.")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@auth.command
|
|
155
|
+
def rename(alias: account_alias, new_alias: account_alias, /) -> None:
|
|
156
|
+
"""Rename an existing account.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
alias: account_alias
|
|
161
|
+
Name of an existing account that should be renamed.
|
|
162
|
+
new_alias: account_alias
|
|
163
|
+
New name of the account.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
appconfig.rename_account(alias, new_alias)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Command to manage documents."""
|
|
2
|
+
|
|
3
|
+
from cyclopts import App
|
|
4
|
+
|
|
5
|
+
from pypaperless_cli.utils import groups
|
|
6
|
+
|
|
7
|
+
from pypaperless_cli.commands.document.show import show
|
|
8
|
+
from pypaperless_cli.commands.document.edit import edit
|
|
9
|
+
|
|
10
|
+
document = App(name="document", help="Work with your documents.", group_commands=groups.commands, version_flags=[])
|
|
11
|
+
document["--help"].group = "Help"
|
|
12
|
+
|
|
13
|
+
document.command(show)
|
|
14
|
+
document.command(edit, group_arguments=groups.arguments, group_parameters=groups.standard_fields)
|
|
@@ -0,0 +1,159 @@
|
|
|
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, 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[str] = 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 ISO 8601 date (YYYY-MM-DD) the document was initially issued.
|
|
90
|
+
|
|
91
|
+
add_tags: List[str|int]
|
|
92
|
+
Assign tags. Requires the ID or the exact name of the tags.
|
|
93
|
+
remove_tags: List[str|int]
|
|
94
|
+
Unassign tags. Requires the ID or the exact name of the tags.
|
|
95
|
+
|
|
96
|
+
add_custom_fields: List[CustomFieldKeyValue]
|
|
97
|
+
Assign custom fields (--custom-fields <NAME|ID>), optionally set a value (--custom-fields <NAME|ID>=VALUE).
|
|
98
|
+
To clear a custom field, set VALUE to an empty string.
|
|
99
|
+
remove_custom_fields: List[CustomFieldKeyValue]
|
|
100
|
+
Unassign given custom fields.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
async with PaperlessAsyncAPI() as paperless:
|
|
104
|
+
# Define how document should be updated
|
|
105
|
+
only_changed: bool = True
|
|
106
|
+
document = await paperless.documents(id)
|
|
107
|
+
|
|
108
|
+
if asn:
|
|
109
|
+
document.archive_serial_number = asn
|
|
110
|
+
|
|
111
|
+
if correspondent:
|
|
112
|
+
document.correspondent = correspondent
|
|
113
|
+
|
|
114
|
+
if document_type:
|
|
115
|
+
document.document_type = document_type
|
|
116
|
+
|
|
117
|
+
if storage_path:
|
|
118
|
+
document.storage_path = storage_path
|
|
119
|
+
|
|
120
|
+
if title:
|
|
121
|
+
document.title = title
|
|
122
|
+
|
|
123
|
+
if created_date:
|
|
124
|
+
from datetime import datetime
|
|
125
|
+
document.created = datetime.strptime(created_date, "%Y-%m-%d").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))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Method for retrieving information about a document."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
|
|
5
|
+
from cyclopts import Parameter
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from pypaperless_cli.api import PaperlessAsyncAPI
|
|
11
|
+
from pypaperless_cli.const import GUI_PATH
|
|
12
|
+
from pypaperless_cli.config import config as appconfig
|
|
13
|
+
from pypaperless_cli.utils.highlighter import highlight_none
|
|
14
|
+
from pypaperless_cli.utils.types import Document
|
|
15
|
+
|
|
16
|
+
async def show(
|
|
17
|
+
id: Document, /, *,
|
|
18
|
+
json: Annotated[Optional[bool], Parameter(
|
|
19
|
+
negative = [],
|
|
20
|
+
show_default = False
|
|
21
|
+
)] = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
|
|
24
|
+
"""Show information about a document.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
id: int
|
|
29
|
+
The ID of the document to show information about.
|
|
30
|
+
json: bool
|
|
31
|
+
If given, the information is printed as JSON.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
async with PaperlessAsyncAPI() as paperless:
|
|
35
|
+
document = await paperless.documents(id)
|
|
36
|
+
|
|
37
|
+
# Everything except created date is optional
|
|
38
|
+
# therefore initialize possibly empty fields
|
|
39
|
+
doc_title = None
|
|
40
|
+
doc_type = None
|
|
41
|
+
correspondent = None
|
|
42
|
+
storage_path = None
|
|
43
|
+
tags = []
|
|
44
|
+
custom_fields = []
|
|
45
|
+
|
|
46
|
+
if len(document.title) > 0:
|
|
47
|
+
doc_title = document.title
|
|
48
|
+
|
|
49
|
+
if document.document_type is not None:
|
|
50
|
+
_doc_type = await paperless.document_types(document.document_type)
|
|
51
|
+
doc_type = _doc_type.name
|
|
52
|
+
|
|
53
|
+
if document.correspondent is not None:
|
|
54
|
+
_correspondent = await paperless.correspondents(document.correspondent)
|
|
55
|
+
correspondent = _correspondent.name
|
|
56
|
+
|
|
57
|
+
if document.storage_path is not None:
|
|
58
|
+
_storage_path = await paperless.storage_paths(document.storage_path)
|
|
59
|
+
storage_path = f"{_storage_path.name}\n({_storage_path.path})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if document.tags:
|
|
63
|
+
filters = {
|
|
64
|
+
"id__in": ",".join(map(str, document.tags))
|
|
65
|
+
}
|
|
66
|
+
async with paperless.tags.reduce(**filters) as filtered:
|
|
67
|
+
async for tag in filtered:
|
|
68
|
+
tags.append(tag)
|
|
69
|
+
|
|
70
|
+
custom_field_values = list(document.custom_fields)
|
|
71
|
+
if custom_field_values:
|
|
72
|
+
filters = {
|
|
73
|
+
"id__in": ",".join(str(f.field) for f in custom_field_values)
|
|
74
|
+
}
|
|
75
|
+
async with paperless.custom_fields.reduce(**filters) as filtered:
|
|
76
|
+
async for field in filtered:
|
|
77
|
+
custom_fields.append({
|
|
78
|
+
"id": field.id,
|
|
79
|
+
"name": field.name,
|
|
80
|
+
"value": next(f.value for f in custom_field_values if f.field == field.id),
|
|
81
|
+
"data_type": field.data_type
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if json:
|
|
85
|
+
Console().print_json(data=document._data)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
table = Table.grid(padding=(0,3))
|
|
89
|
+
|
|
90
|
+
table.add_column(style="blue")
|
|
91
|
+
table.add_column(style="green", no_wrap=True)
|
|
92
|
+
|
|
93
|
+
# Explicitly check title as NoneHighlighter doesn't work well with additional styles
|
|
94
|
+
if doc_title is not None:
|
|
95
|
+
table.add_row("[b]Title", f"[b]{doc_title}")
|
|
96
|
+
else:
|
|
97
|
+
table.add_row("[b]Title", f"[b purple]{str(doc_title)}")
|
|
98
|
+
|
|
99
|
+
table.add_row("ID", str(document.id))
|
|
100
|
+
table.add_row("ASN", highlight_none(str(document.archive_serial_number)))
|
|
101
|
+
table.add_row("Created", str(document.created_date))
|
|
102
|
+
table.add_row("Correspondent", highlight_none(str(correspondent)))
|
|
103
|
+
table.add_row("Document type", highlight_none(str(doc_type)))
|
|
104
|
+
table.add_row("Storage path", highlight_none(str(storage_path)))
|
|
105
|
+
|
|
106
|
+
if tags:
|
|
107
|
+
table.add_row("Tags", "\n".join([tag.name for tag in tags]))
|
|
108
|
+
else:
|
|
109
|
+
table.add_row("Tags", highlight_none(str(None)))
|
|
110
|
+
|
|
111
|
+
table.add_row("Details", f"{appconfig.current.host}{GUI_PATH['documents_details'].format(pk=document.id)}")
|
|
112
|
+
|
|
113
|
+
table.add_row("[white]Custom fields")
|
|
114
|
+
if custom_fields:
|
|
115
|
+
for custom_field in custom_fields:
|
|
116
|
+
table.add_row(custom_field["name"], highlight_none(str(custom_field["value"])))
|
|
117
|
+
else:
|
|
118
|
+
table.add_row(highlight_none(str(None)))
|
|
119
|
+
|
|
120
|
+
Console().print(table)
|