nc-py-api 0.10.0__py3-none-any.whl → 0.18.1__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.
- nc_py_api/__init__.py +1 -1
- nc_py_api/_session.py +27 -14
- nc_py_api/_version.py +1 -1
- nc_py_api/apps.py +0 -13
- nc_py_api/ex_app/__init__.py +13 -3
- nc_py_api/ex_app/defs.py +17 -29
- nc_py_api/ex_app/events_listener.py +137 -0
- nc_py_api/ex_app/integration_fastapi.py +25 -14
- nc_py_api/ex_app/logging.py +46 -0
- nc_py_api/ex_app/misc.py +6 -1
- nc_py_api/ex_app/occ_commands.py +153 -0
- nc_py_api/ex_app/providers/providers.py +7 -21
- nc_py_api/ex_app/providers/task_processing.py +261 -0
- nc_py_api/ex_app/ui/files_actions.py +45 -61
- nc_py_api/files/__init__.py +137 -6
- nc_py_api/files/_files.py +44 -10
- nc_py_api/files/files.py +59 -464
- nc_py_api/files/files_async.py +528 -0
- nc_py_api/loginflow_v2.py +161 -0
- nc_py_api/nextcloud.py +77 -21
- nc_py_api/talk_bot.py +5 -0
- nc_py_api/users.py +3 -3
- nc_py_api/webhooks.py +224 -0
- {nc_py_api-0.10.0.dist-info → nc_py_api-0.18.1.dist-info}/METADATA +36 -24
- nc_py_api-0.18.1.dist-info/RECORD +53 -0
- {nc_py_api-0.10.0.dist-info → nc_py_api-0.18.1.dist-info}/WHEEL +1 -1
- {nc_py_api-0.10.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/AUTHORS +1 -0
- nc_py_api/ex_app/providers/speech_to_text.py +0 -128
- nc_py_api/ex_app/providers/text_processing.py +0 -135
- nc_py_api/ex_app/providers/translations.py +0 -156
- nc_py_api-0.10.0.dist-info/RECORD +0 -49
- {nc_py_api-0.10.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/LICENSE.txt +0 -0
nc_py_api/files/__init__.py
CHANGED
|
@@ -4,10 +4,76 @@ import dataclasses
|
|
|
4
4
|
import datetime
|
|
5
5
|
import email.utils
|
|
6
6
|
import enum
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
7
9
|
import warnings
|
|
8
10
|
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
9
13
|
from .. import _misc
|
|
10
14
|
|
|
15
|
+
user_regex = re.compile(r"(?:files|trashbin|versions)/([^/]+)/")
|
|
16
|
+
"""Regex for evaluating user from full path string; instantiated once on import."""
|
|
17
|
+
user_path_regex = re.compile(r".*?(files|trashbin|versions)/([^/]+)/")
|
|
18
|
+
"""Regex for evaluating user path from full path string; instantiated once on import."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LockType(enum.IntEnum):
|
|
22
|
+
"""Nextcloud File Locks types."""
|
|
23
|
+
|
|
24
|
+
MANUAL_LOCK = 0
|
|
25
|
+
COLLABORATIVE_LOCK = 1
|
|
26
|
+
WEBDAV_TOKEN = 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass
|
|
30
|
+
class FsNodeLockInfo:
|
|
31
|
+
"""File Lock information if Nextcloud `files_lock` is enabled."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, **kwargs):
|
|
34
|
+
self._is_locked = bool(int(kwargs.get("is_locked", False)))
|
|
35
|
+
self._lock_owner_type = LockType(int(kwargs.get("lock_owner_type", 0)))
|
|
36
|
+
self._lock_owner = kwargs.get("lock_owner", "")
|
|
37
|
+
self._owner_display_name = kwargs.get("owner_display_name", "")
|
|
38
|
+
self._owner_editor = kwargs.get("lock_owner_editor", "")
|
|
39
|
+
self._lock_time = int(kwargs.get("lock_time", 0))
|
|
40
|
+
self._lock_ttl = int(kwargs.get("_lock_ttl", 0))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_locked(self) -> bool:
|
|
44
|
+
"""Returns ``True`` if the file is locked, ``False`` otherwise."""
|
|
45
|
+
return self._is_locked
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def type(self) -> LockType:
|
|
49
|
+
"""Type of the lock."""
|
|
50
|
+
return LockType(self._lock_owner_type)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def owner(self) -> str:
|
|
54
|
+
"""User id of the lock owner."""
|
|
55
|
+
return self._lock_owner
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def owner_display_name(self) -> str:
|
|
59
|
+
"""Display name of the lock owner."""
|
|
60
|
+
return self._owner_display_name
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def owner_editor(self) -> str:
|
|
64
|
+
"""App id of an app owned lock to allow clients to suggest joining the collaborative editing session."""
|
|
65
|
+
return self._owner_editor
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def lock_creation_time(self) -> datetime.datetime:
|
|
69
|
+
"""Lock creation time."""
|
|
70
|
+
return datetime.datetime.utcfromtimestamp(self._lock_time).replace(tzinfo=datetime.timezone.utc)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def lock_ttl(self) -> int:
|
|
74
|
+
"""TTL of the lock in seconds staring from the creation time. A value of 0 means the timeout is infinite."""
|
|
75
|
+
return self._lock_ttl
|
|
76
|
+
|
|
11
77
|
|
|
12
78
|
@dataclasses.dataclass
|
|
13
79
|
class FsNodeInfo:
|
|
@@ -116,11 +182,15 @@ class FsNode:
|
|
|
116
182
|
info: FsNodeInfo
|
|
117
183
|
"""Additional extra information for the object"""
|
|
118
184
|
|
|
185
|
+
lock_info: FsNodeLockInfo
|
|
186
|
+
"""Class describing `lock` information if any."""
|
|
187
|
+
|
|
119
188
|
def __init__(self, full_path: str, **kwargs):
|
|
120
189
|
self.full_path = full_path
|
|
121
190
|
self.file_id = kwargs.get("file_id", "")
|
|
122
191
|
self.etag = kwargs.get("etag", "")
|
|
123
192
|
self.info = FsNodeInfo(**kwargs)
|
|
193
|
+
self.lock_info = FsNodeLockInfo(**kwargs)
|
|
124
194
|
|
|
125
195
|
@property
|
|
126
196
|
def is_dir(self) -> bool:
|
|
@@ -139,9 +209,7 @@ class FsNode:
|
|
|
139
209
|
)
|
|
140
210
|
|
|
141
211
|
def __eq__(self, other):
|
|
142
|
-
|
|
143
|
-
return True
|
|
144
|
-
return False
|
|
212
|
+
return bool(self.file_id and self.file_id == other.file_id)
|
|
145
213
|
|
|
146
214
|
@property
|
|
147
215
|
def has_extra(self) -> bool:
|
|
@@ -156,12 +224,12 @@ class FsNode:
|
|
|
156
224
|
@property
|
|
157
225
|
def user(self) -> str:
|
|
158
226
|
"""Returns user ID extracted from the `full_path`."""
|
|
159
|
-
return self.full_path
|
|
227
|
+
return user_regex.findall(self.full_path)[0]
|
|
160
228
|
|
|
161
229
|
@property
|
|
162
230
|
def user_path(self) -> str:
|
|
163
231
|
"""Returns path relative to the user's root directory."""
|
|
164
|
-
return
|
|
232
|
+
return user_path_regex.sub("", self.full_path, count=1)
|
|
165
233
|
|
|
166
234
|
@property
|
|
167
235
|
def is_shared(self) -> bool:
|
|
@@ -218,12 +286,13 @@ class FilePermissions(enum.IntFlag):
|
|
|
218
286
|
"""Access to re-share object(s)"""
|
|
219
287
|
|
|
220
288
|
|
|
221
|
-
def permissions_to_str(permissions: int, is_dir: bool = False) -> str:
|
|
289
|
+
def permissions_to_str(permissions: int | str, is_dir: bool = False) -> str:
|
|
222
290
|
"""Converts integer permissions to string permissions.
|
|
223
291
|
|
|
224
292
|
:param permissions: concatenation of ``FilePermissions`` integer flags.
|
|
225
293
|
:param is_dir: Flag indicating is permissions related to the directory object or not.
|
|
226
294
|
"""
|
|
295
|
+
permissions = int(permissions) if not isinstance(permissions, int) else permissions
|
|
227
296
|
r = ""
|
|
228
297
|
if permissions & FilePermissions.PERMISSION_SHARE:
|
|
229
298
|
r += "R"
|
|
@@ -400,3 +469,65 @@ class Share:
|
|
|
400
469
|
f"{self.share_type.name}: `{self.path}` with id={self.share_id}"
|
|
401
470
|
f" from {self.share_owner} to {self.share_with}"
|
|
402
471
|
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class ActionFileInfo(BaseModel):
|
|
475
|
+
"""Information Nextcloud sends to the External Application about File Nodes affected in action."""
|
|
476
|
+
|
|
477
|
+
fileId: int
|
|
478
|
+
"""FileID without Nextcloud instance ID"""
|
|
479
|
+
name: str
|
|
480
|
+
"""Name of the file/directory"""
|
|
481
|
+
directory: str
|
|
482
|
+
"""Directory relative to the user's home directory"""
|
|
483
|
+
etag: str
|
|
484
|
+
mime: str
|
|
485
|
+
fileType: str
|
|
486
|
+
"""**file** or **dir**"""
|
|
487
|
+
size: int
|
|
488
|
+
"""size of file/directory"""
|
|
489
|
+
favorite: str
|
|
490
|
+
"""**true** or **false**"""
|
|
491
|
+
permissions: int
|
|
492
|
+
"""Combination of :py:class:`~nc_py_api.files.FilePermissions` values"""
|
|
493
|
+
mtime: int
|
|
494
|
+
"""Last modified time"""
|
|
495
|
+
userId: str
|
|
496
|
+
"""The ID of the user performing the action."""
|
|
497
|
+
shareOwner: str | None = None
|
|
498
|
+
"""If the object is shared, this is a display name of the share owner."""
|
|
499
|
+
shareOwnerId: str | None = None
|
|
500
|
+
"""If the object is shared, this is the owner ID of the share."""
|
|
501
|
+
instanceId: str | None = None
|
|
502
|
+
"""Nextcloud instance ID."""
|
|
503
|
+
|
|
504
|
+
def to_fs_node(self) -> FsNode:
|
|
505
|
+
"""Returns usual :py:class:`~nc_py_api.files.FsNode` created from this class."""
|
|
506
|
+
user_path = os.path.join(self.directory, self.name).rstrip("/")
|
|
507
|
+
is_dir = bool(self.fileType.lower() == "dir")
|
|
508
|
+
if is_dir:
|
|
509
|
+
user_path += "/"
|
|
510
|
+
full_path = os.path.join(f"files/{self.userId}", user_path.lstrip("/"))
|
|
511
|
+
file_id = str(self.fileId).rjust(8, "0")
|
|
512
|
+
|
|
513
|
+
permissions = "S" if self.shareOwnerId else ""
|
|
514
|
+
permissions += permissions_to_str(self.permissions, is_dir)
|
|
515
|
+
return FsNode(
|
|
516
|
+
full_path,
|
|
517
|
+
etag=self.etag,
|
|
518
|
+
size=self.size,
|
|
519
|
+
content_length=0 if is_dir else self.size,
|
|
520
|
+
permissions=permissions,
|
|
521
|
+
favorite=bool(self.favorite.lower() == "true"),
|
|
522
|
+
file_id=file_id + self.instanceId if self.instanceId else file_id,
|
|
523
|
+
fileid=self.fileId,
|
|
524
|
+
last_modified=datetime.datetime.utcfromtimestamp(self.mtime).replace(tzinfo=datetime.timezone.utc),
|
|
525
|
+
mimetype=self.mime,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ActionFileInfoEx(BaseModel):
|
|
530
|
+
"""New ``register_ex`` uses new data format which allowing receiving multiple NC Nodes in one request."""
|
|
531
|
+
|
|
532
|
+
files: list[ActionFileInfo]
|
|
533
|
+
"""Always list of ``ActionFileInfo`` with one element minimum."""
|
nc_py_api/files/_files.py
CHANGED
|
@@ -10,7 +10,7 @@ import xmltodict
|
|
|
10
10
|
from httpx import Response
|
|
11
11
|
|
|
12
12
|
from .._exceptions import NextcloudException, check_error
|
|
13
|
-
from .._misc import clear_from_params_empty
|
|
13
|
+
from .._misc import check_capabilities, clear_from_params_empty
|
|
14
14
|
from . import FsNode, SystemTag
|
|
15
15
|
|
|
16
16
|
PROPFIND_PROPERTIES = [
|
|
@@ -29,13 +29,16 @@ PROPFIND_PROPERTIES = [
|
|
|
29
29
|
"oc:share-types",
|
|
30
30
|
"oc:favorite",
|
|
31
31
|
"nc:is-encrypted",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
PROPFIND_LOCKING_PROPERTIES = [
|
|
32
35
|
"nc:lock",
|
|
33
36
|
"nc:lock-owner-displayname",
|
|
34
37
|
"nc:lock-owner",
|
|
35
38
|
"nc:lock-owner-type",
|
|
36
|
-
"nc:lock-owner-editor",
|
|
37
|
-
"nc:lock-time",
|
|
38
|
-
"nc:lock-timeout",
|
|
39
|
+
"nc:lock-owner-editor", # App id of an app owned lock
|
|
40
|
+
"nc:lock-time", # Timestamp of the log creation time
|
|
41
|
+
"nc:lock-timeout", # TTL of the lock in seconds staring from the creation time
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
SEARCH_PROPERTIES_MAP = {
|
|
@@ -57,7 +60,14 @@ class PropFindType(enum.IntEnum):
|
|
|
57
60
|
VERSIONS_FILE_ID = 3
|
|
58
61
|
|
|
59
62
|
|
|
60
|
-
def
|
|
63
|
+
def get_propfind_properties(capabilities: dict) -> list:
|
|
64
|
+
r = PROPFIND_PROPERTIES
|
|
65
|
+
if not check_capabilities("files.locking", capabilities):
|
|
66
|
+
r += PROPFIND_LOCKING_PROPERTIES
|
|
67
|
+
return r
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_find_request(req: list, path: str | FsNode, user: str, capabilities: dict) -> ElementTree.Element:
|
|
61
71
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
62
72
|
root = ElementTree.Element(
|
|
63
73
|
"d:searchrequest",
|
|
@@ -65,7 +75,7 @@ def build_find_request(req: list, path: str | FsNode, user: str) -> ElementTree.
|
|
|
65
75
|
)
|
|
66
76
|
xml_search = ElementTree.SubElement(root, "d:basicsearch")
|
|
67
77
|
xml_select_prop = ElementTree.SubElement(ElementTree.SubElement(xml_search, "d:select"), "d:prop")
|
|
68
|
-
for i in
|
|
78
|
+
for i in get_propfind_properties(capabilities):
|
|
69
79
|
ElementTree.SubElement(xml_select_prop, i)
|
|
70
80
|
xml_from_scope = ElementTree.SubElement(ElementTree.SubElement(xml_search, "d:from"), "d:scope")
|
|
71
81
|
href = f"/files/{user}/{path.removeprefix('/')}"
|
|
@@ -76,7 +86,9 @@ def build_find_request(req: list, path: str | FsNode, user: str) -> ElementTree.
|
|
|
76
86
|
return root
|
|
77
87
|
|
|
78
88
|
|
|
79
|
-
def build_list_by_criteria_req(
|
|
89
|
+
def build_list_by_criteria_req(
|
|
90
|
+
properties: list[str] | None, tags: list[int | SystemTag] | None, capabilities: dict
|
|
91
|
+
) -> ElementTree.Element:
|
|
80
92
|
if not properties and not tags:
|
|
81
93
|
raise ValueError("Either specify 'properties' or 'tags' to filter results.")
|
|
82
94
|
root = ElementTree.Element(
|
|
@@ -84,7 +96,7 @@ def build_list_by_criteria_req(properties: list[str] | None, tags: list[int | Sy
|
|
|
84
96
|
attrib={"xmlns:d": "DAV:", "xmlns:oc": "http://owncloud.org/ns", "xmlns:nc": "http://nextcloud.org/ns"},
|
|
85
97
|
)
|
|
86
98
|
prop = ElementTree.SubElement(root, "d:prop")
|
|
87
|
-
for i in
|
|
99
|
+
for i in get_propfind_properties(capabilities):
|
|
88
100
|
ElementTree.SubElement(prop, i)
|
|
89
101
|
xml_filter_rules = ElementTree.SubElement(root, "oc:filter-rules")
|
|
90
102
|
if properties and "favorite" in properties:
|
|
@@ -156,6 +168,18 @@ def build_list_tags_response(response: Response) -> list[SystemTag]:
|
|
|
156
168
|
return result
|
|
157
169
|
|
|
158
170
|
|
|
171
|
+
def build_tags_ids_for_object(url_to_fetch: str, response: Response) -> list[int]:
|
|
172
|
+
result = []
|
|
173
|
+
records = _webdav_response_to_records(response, "list_tags_ids")
|
|
174
|
+
for record in records:
|
|
175
|
+
prop_stat = record["d:propstat"]
|
|
176
|
+
if str(prop_stat.get("d:status", "")).find("200 OK") != -1:
|
|
177
|
+
href_suffix = str(record["d:href"]).removeprefix(url_to_fetch).strip("/")
|
|
178
|
+
if href_suffix:
|
|
179
|
+
result.append(int(href_suffix))
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
159
183
|
def build_update_tag_req(
|
|
160
184
|
name: str | None, user_visible: bool | None, user_assignable: bool | None
|
|
161
185
|
) -> ElementTree.Element:
|
|
@@ -243,7 +267,7 @@ def etag_fileid_from_response(response: Response) -> dict:
|
|
|
243
267
|
return {"etag": response.headers.get("OC-Etag", ""), "file_id": response.headers["OC-FileId"]}
|
|
244
268
|
|
|
245
269
|
|
|
246
|
-
def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
|
|
270
|
+
def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode: # noqa pylint: disable = too-many-branches
|
|
247
271
|
fs_node_args = {}
|
|
248
272
|
for prop_stat in prop_stats:
|
|
249
273
|
if str(prop_stat.get("d:status", "")).find("200 OK") == -1:
|
|
@@ -274,7 +298,17 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
|
|
|
274
298
|
fs_node_args["trashbin_original_location"] = prop["nc:trashbin-original-location"]
|
|
275
299
|
if "nc:trashbin-deletion-time" in prop_keys:
|
|
276
300
|
fs_node_args["trashbin_deletion_time"] = prop["nc:trashbin-deletion-time"]
|
|
277
|
-
|
|
301
|
+
for k, v in {
|
|
302
|
+
"nc:lock": "is_locked",
|
|
303
|
+
"nc:lock-owner-type": "lock_owner_type",
|
|
304
|
+
"nc:lock-owner": "lock_owner",
|
|
305
|
+
"nc:lock-owner-displayname": "lock_owner_displayname",
|
|
306
|
+
"nc:lock-owner-editor": "lock_owner_editor",
|
|
307
|
+
"nc:lock-time": "lock_time",
|
|
308
|
+
"nc:lock-timeout": "lock_ttl",
|
|
309
|
+
}.items():
|
|
310
|
+
if k in prop_keys and prop[k] is not None:
|
|
311
|
+
fs_node_args[v] = prop[k]
|
|
278
312
|
return FsNode(full_path, **fs_node_args)
|
|
279
313
|
|
|
280
314
|
|