nc-py-api 0.10.0__tar.gz → 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/CHANGELOG.md +10 -0
  2. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/PKG-INFO +2 -2
  3. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/__init__.py +1 -1
  4. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_version.py +1 -1
  5. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/providers/translations.py +9 -0
  6. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/files/__init__.py +61 -0
  7. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/files/_files.py +32 -10
  8. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/files/files.py +64 -7
  9. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/pyproject.toml +1 -1
  10. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/.gitignore +0 -0
  11. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/AUTHORS +0 -0
  12. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/LICENSE.txt +0 -0
  13. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/README.md +0 -0
  14. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_deffered_error.py +0 -0
  15. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_exceptions.py +0 -0
  16. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_misc.py +0 -0
  17. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_preferences.py +0 -0
  18. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_preferences_ex.py +0 -0
  19. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_session.py +0 -0
  20. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_talk_api.py +0 -0
  21. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/_theming.py +0 -0
  22. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/activity.py +0 -0
  23. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/apps.py +0 -0
  24. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/calendar.py +0 -0
  25. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/__init__.py +0 -0
  26. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/defs.py +0 -0
  27. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/integration_fastapi.py +0 -0
  28. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/misc.py +0 -0
  29. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/persist_transformers_cache.py +0 -0
  30. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/providers/__init__.py +0 -0
  31. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/providers/providers.py +0 -0
  32. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/providers/speech_to_text.py +0 -0
  33. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/providers/text_processing.py +0 -0
  34. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/__init__.py +0 -0
  35. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/files_actions.py +0 -0
  36. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/resources.py +0 -0
  37. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/settings.py +0 -0
  38. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/top_menu.py +0 -0
  39. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/ui/ui.py +0 -0
  40. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/ex_app/uvicorn_fastapi.py +0 -0
  41. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/files/sharing.py +0 -0
  42. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/nextcloud.py +0 -0
  43. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/notes.py +0 -0
  44. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/notifications.py +0 -0
  45. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/options.py +0 -0
  46. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/talk.py +0 -0
  47. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/talk_bot.py +0 -0
  48. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/user_status.py +0 -0
  49. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/users.py +0 -0
  50. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/users_groups.py +0 -0
  51. {nc_py_api-0.10.0 → nc_py_api-0.11.0}/nc_py_api/weather_status.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.11.0 - 2024-02-17]
6
+
7
+ ### Added
8
+
9
+ - Files: `lock` and `unlock` methods, lock file information to `FsNode`. #227
10
+
11
+ ### Fixed
12
+
13
+ - NextcloudApp: `MachineTranslation` provider registration - added optional `actionDetectLang` param. #229
14
+
5
15
  ## [0.10.0 - 2024-02-14]
6
16
 
7
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nc-py-api
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Nextcloud Python Framework
5
5
  Project-URL: Changelog, https://github.com/cloud-py-api/nc_py_api/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://cloud-py-api.github.io/nc_py_api/
@@ -10,7 +10,7 @@ License-Expression: BSD-3-Clause
10
10
  License-File: AUTHORS
11
11
  License-File: LICENSE.txt
12
12
  Keywords: api,client,framework,library,nextcloud
13
- Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: BSD License
16
16
  Classifier: Operating System :: MacOS :: MacOS X
@@ -7,6 +7,6 @@ from ._exceptions import (
7
7
  NextcloudMissingCapabilities,
8
8
  )
9
9
  from ._version import __version__
10
- from .files import FilePermissions, FsNode
10
+ from .files import FilePermissions, FsNode, LockType
11
11
  from .files.sharing import ShareType
12
12
  from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp
@@ -1,3 +1,3 @@
1
1
  """Version of nc_py_api."""
2
2
 
3
- __version__ = "0.10.0"
3
+ __version__ = "0.11.0"
@@ -42,6 +42,11 @@ class TranslationsProvider:
42
42
  """Relative ExApp url which will be called by Nextcloud."""
43
43
  return self._raw_data["action_handler"]
44
44
 
45
+ @property
46
+ def action_handler_detect_lang(self) -> str:
47
+ """Relative ExApp url which will be called by Nextcloud to detect language."""
48
+ return self._raw_data.get("action_detect_lang", "")
49
+
45
50
  def __repr__(self):
46
51
  return f"<{self.__class__.__name__} name={self.name}, handler={self.action_handler}>"
47
52
 
@@ -59,6 +64,7 @@ class _TranslationsProviderAPI:
59
64
  callback_url: str,
60
65
  from_languages: dict[str, str],
61
66
  to_languages: dict[str, str],
67
+ detect_lang_callback_url: str = "",
62
68
  ) -> None:
63
69
  """Registers or edit the Translations provider."""
64
70
  require_capabilities("app_api", self._session.capabilities)
@@ -68,6 +74,7 @@ class _TranslationsProviderAPI:
68
74
  "fromLanguages": from_languages,
69
75
  "toLanguages": to_languages,
70
76
  "actionHandler": callback_url,
77
+ "actionDetectLang": detect_lang_callback_url,
71
78
  }
72
79
  self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
73
80
 
@@ -114,6 +121,7 @@ class _AsyncTranslationsProviderAPI:
114
121
  callback_url: str,
115
122
  from_languages: dict[str, str],
116
123
  to_languages: dict[str, str],
124
+ detect_lang_callback_url: str = "",
117
125
  ) -> None:
118
126
  """Registers or edit the Translations provider."""
119
127
  require_capabilities("app_api", await self._session.capabilities)
@@ -123,6 +131,7 @@ class _AsyncTranslationsProviderAPI:
123
131
  "fromLanguages": from_languages,
124
132
  "toLanguages": to_languages,
125
133
  "actionHandler": callback_url,
134
+ "actionDetectLang": detect_lang_callback_url,
126
135
  }
127
136
  await self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
128
137
 
@@ -9,6 +9,63 @@ import warnings
9
9
  from .. import _misc
10
10
 
11
11
 
12
+ class LockType(enum.IntEnum):
13
+ """Nextcloud File Locks types."""
14
+
15
+ MANUAL_LOCK = 0
16
+ COLLABORATIVE_LOCK = 1
17
+ WEBDAV_TOKEN = 2
18
+
19
+
20
+ @dataclasses.dataclass
21
+ class FsNodeLockInfo:
22
+ """File Lock information if Nextcloud `files_lock` is enabled."""
23
+
24
+ def __init__(self, **kwargs):
25
+ self._is_locked = bool(int(kwargs.get("is_locked", False)))
26
+ self._lock_owner_type = LockType(int(kwargs.get("lock_owner_type", 0)))
27
+ self._lock_owner = kwargs.get("lock_owner", "")
28
+ self._owner_display_name = kwargs.get("owner_display_name", "")
29
+ self._owner_editor = kwargs.get("lock_owner_editor", "")
30
+ self._lock_time = int(kwargs.get("lock_time", 0))
31
+ self._lock_ttl = int(kwargs.get("_lock_ttl", 0))
32
+
33
+ @property
34
+ def is_locked(self) -> bool:
35
+ """Returns ``True`` if the file is locked, ``False`` otherwise."""
36
+ return self._is_locked
37
+
38
+ @property
39
+ def type(self) -> LockType:
40
+ """Type of the lock."""
41
+ return LockType(self._lock_owner_type)
42
+
43
+ @property
44
+ def owner(self) -> str:
45
+ """User id of the lock owner."""
46
+ return self._lock_owner
47
+
48
+ @property
49
+ def owner_display_name(self) -> str:
50
+ """Display name of the lock owner."""
51
+ return self._owner_display_name
52
+
53
+ @property
54
+ def owner_editor(self) -> str:
55
+ """App id of an app owned lock to allow clients to suggest joining the collaborative editing session."""
56
+ return self._owner_editor
57
+
58
+ @property
59
+ def lock_creation_time(self) -> datetime.datetime:
60
+ """Lock creation time."""
61
+ return datetime.datetime.utcfromtimestamp(self._lock_time).replace(tzinfo=datetime.timezone.utc)
62
+
63
+ @property
64
+ def lock_ttl(self) -> int:
65
+ """TTL of the lock in seconds staring from the creation time. A value of 0 means the timeout is infinite."""
66
+ return self._lock_ttl
67
+
68
+
12
69
  @dataclasses.dataclass
13
70
  class FsNodeInfo:
14
71
  """Extra FS object attributes from Nextcloud."""
@@ -116,11 +173,15 @@ class FsNode:
116
173
  info: FsNodeInfo
117
174
  """Additional extra information for the object"""
118
175
 
176
+ lock_info: FsNodeLockInfo
177
+ """Class describing `lock` information if any."""
178
+
119
179
  def __init__(self, full_path: str, **kwargs):
120
180
  self.full_path = full_path
121
181
  self.file_id = kwargs.get("file_id", "")
122
182
  self.etag = kwargs.get("etag", "")
123
183
  self.info = FsNodeInfo(**kwargs)
184
+ self.lock_info = FsNodeLockInfo(**kwargs)
124
185
 
125
186
  @property
126
187
  def is_dir(self) -> bool:
@@ -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 build_find_request(req: list, path: str | FsNode, user: str) -> ElementTree.Element:
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 PROPFIND_PROPERTIES:
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(properties: list[str] | None, tags: list[int | SystemTag] | None) -> ElementTree.Element:
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 PROPFIND_PROPERTIES:
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:
@@ -243,7 +255,7 @@ def etag_fileid_from_response(response: Response) -> dict:
243
255
  return {"etag": response.headers.get("OC-Etag", ""), "file_id": response.headers["OC-FileId"]}
244
256
 
245
257
 
246
- def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
258
+ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode: # noqa pylint: disable = too-many-branches
247
259
  fs_node_args = {}
248
260
  for prop_stat in prop_stats:
249
261
  if str(prop_stat.get("d:status", "")).find("200 OK") == -1:
@@ -274,7 +286,17 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
274
286
  fs_node_args["trashbin_original_location"] = prop["nc:trashbin-original-location"]
275
287
  if "nc:trashbin-deletion-time" in prop_keys:
276
288
  fs_node_args["trashbin_deletion_time"] = prop["nc:trashbin-deletion-time"]
277
- # xz = prop.get("oc:dDC", "")
289
+ for k, v in {
290
+ "nc:lock": "is_locked",
291
+ "nc:lock-owner-type": "lock_owner_type",
292
+ "nc:lock-owner": "lock_owner",
293
+ "nc:lock-owner-displayname": "lock_owner_displayname",
294
+ "nc:lock-owner-editor": "lock_owner_editor",
295
+ "nc:lock-time": "lock_time",
296
+ "nc:lock-timeout": "lock_ttl",
297
+ }.items():
298
+ if k in prop_keys and prop[k] is not None:
299
+ fs_node_args[v] = prop[k]
278
300
  return FsNode(full_path, **fs_node_args)
279
301
 
280
302
 
@@ -10,7 +10,7 @@ from httpx import Headers
10
10
  from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
11
11
  from .._misc import random_string, require_capabilities
12
12
  from .._session import AsyncNcSessionBasic, NcSessionBasic
13
- from . import FsNode, SystemTag
13
+ from . import FsNode, LockType, SystemTag
14
14
  from ._files import (
15
15
  PROPFIND_PROPERTIES,
16
16
  PropFindType,
@@ -25,6 +25,7 @@ from ._files import (
25
25
  dav_get_obj_path,
26
26
  element_tree_as_str,
27
27
  etag_fileid_from_response,
28
+ get_propfind_properties,
28
29
  lf_parse_webdav_response,
29
30
  )
30
31
  from .sharing import _AsyncFilesSharingAPI, _FilesSharingAPI
@@ -50,7 +51,7 @@ class FilesAPI:
50
51
  """
51
52
  if exclude_self and not depth:
52
53
  raise ValueError("Wrong input parameters, query will return nothing.")
53
- properties = PROPFIND_PROPERTIES
54
+ properties = get_propfind_properties(self._session.capabilities)
54
55
  path = path.user_path if isinstance(path, FsNode) else path
55
56
  return self._listdir(self._session.user, path, properties=properties, depth=depth, exclude_self=exclude_self)
56
57
 
@@ -76,7 +77,7 @@ class FilesAPI:
76
77
  :param path: path where to search from. Default = **""**.
77
78
  """
78
79
  # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
79
- root = build_find_request(req, path, self._session.user)
80
+ root = build_find_request(req, path, self._session.user, self._session.capabilities)
80
81
  webdav_response = self._session.adapter_dav.request(
81
82
  "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
82
83
  )
@@ -248,7 +249,7 @@ class FilesAPI:
248
249
  Supported values: **favorite**
249
250
  :param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
250
251
  """
251
- root = build_list_by_criteria_req(properties, tags)
252
+ root = build_list_by_criteria_req(properties, tags, self._session.capabilities)
252
253
  webdav_response = self._session.adapter_dav.request(
253
254
  "REPORT", dav_get_obj_path(self._session.user), content=element_tree_as_str(root)
254
255
  )
@@ -396,6 +397,34 @@ class FilesAPI:
396
397
  """Removes Tag from a file/directory."""
397
398
  self._file_change_tag_state(file_id, tag_id, False)
398
399
 
400
+ def lock(self, path: FsNode | str, lock_type: LockType = LockType.MANUAL_LOCK) -> None:
401
+ """Locks the file.
402
+
403
+ .. note:: Exception codes: 423 - existing lock present.
404
+ """
405
+ require_capabilities("files.locking", self._session.capabilities)
406
+ full_path = dav_get_obj_path(self._session.user, path.user_path if isinstance(path, FsNode) else path)
407
+ response = self._session.adapter_dav.request(
408
+ "LOCK",
409
+ quote(full_path),
410
+ headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
411
+ )
412
+ check_error(response, f"lock: user={self._session.user}, path={full_path}")
413
+
414
+ def unlock(self, path: FsNode | str) -> None:
415
+ """Unlocks the file.
416
+
417
+ .. note:: Exception codes: 412 - the file is not locked, 423 - the lock is owned by another user.
418
+ """
419
+ require_capabilities("files.locking", self._session.capabilities)
420
+ full_path = dav_get_obj_path(self._session.user, path.user_path if isinstance(path, FsNode) else path)
421
+ response = self._session.adapter_dav.request(
422
+ "UNLOCK",
423
+ quote(full_path),
424
+ headers={"X-User-Lock": "1"},
425
+ )
426
+ check_error(response, f"unlock: user={self._session.user}, path={full_path}")
427
+
399
428
  def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
400
429
  fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
401
430
  tag = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
@@ -493,7 +522,7 @@ class AsyncFilesAPI:
493
522
  """
494
523
  if exclude_self and not depth:
495
524
  raise ValueError("Wrong input parameters, query will return nothing.")
496
- properties = PROPFIND_PROPERTIES
525
+ properties = get_propfind_properties(await self._session.capabilities)
497
526
  path = path.user_path if isinstance(path, FsNode) else path
498
527
  return await self._listdir(
499
528
  await self._session.user, path, properties=properties, depth=depth, exclude_self=exclude_self
@@ -521,7 +550,7 @@ class AsyncFilesAPI:
521
550
  :param path: path where to search from. Default = **""**.
522
551
  """
523
552
  # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
524
- root = build_find_request(req, path, await self._session.user)
553
+ root = build_find_request(req, path, await self._session.user, await self._session.capabilities)
525
554
  webdav_response = await self._session.adapter_dav.request(
526
555
  "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
527
556
  )
@@ -695,7 +724,7 @@ class AsyncFilesAPI:
695
724
  Supported values: **favorite**
696
725
  :param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
697
726
  """
698
- root = build_list_by_criteria_req(properties, tags)
727
+ root = build_list_by_criteria_req(properties, tags, await self._session.capabilities)
699
728
  webdav_response = await self._session.adapter_dav.request(
700
729
  "REPORT", dav_get_obj_path(await self._session.user), content=element_tree_as_str(root)
701
730
  )
@@ -848,6 +877,34 @@ class AsyncFilesAPI:
848
877
  """Removes Tag from a file/directory."""
849
878
  await self._file_change_tag_state(file_id, tag_id, False)
850
879
 
880
+ async def lock(self, path: FsNode | str, lock_type: LockType = LockType.MANUAL_LOCK) -> None:
881
+ """Locks the file.
882
+
883
+ .. note:: Exception codes: 423 - existing lock present.
884
+ """
885
+ require_capabilities("files.locking", await self._session.capabilities)
886
+ full_path = dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path)
887
+ response = await self._session.adapter_dav.request(
888
+ "LOCK",
889
+ quote(full_path),
890
+ headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
891
+ )
892
+ check_error(response, f"lock: user={self._session.user}, path={full_path}")
893
+
894
+ async def unlock(self, path: FsNode | str) -> None:
895
+ """Unlocks the file.
896
+
897
+ .. note:: Exception codes: 412 - the file is not locked, 423 - the lock is owned by another user.
898
+ """
899
+ require_capabilities("files.locking", await self._session.capabilities)
900
+ full_path = dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path)
901
+ response = await self._session.adapter_dav.request(
902
+ "UNLOCK",
903
+ quote(full_path),
904
+ headers={"X-User-Lock": "1"},
905
+ )
906
+ check_error(response, f"unlock: user={self._session.user}, path={full_path}")
907
+
851
908
  async def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
852
909
  fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
853
910
  tag = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
@@ -21,7 +21,7 @@ authors = [
21
21
  ]
22
22
  requires-python = ">=3.10"
23
23
  classifiers = [
24
- "Development Status :: 3 - Alpha",
24
+ "Development Status :: 4 - Beta",
25
25
  "Intended Audience :: Developers",
26
26
  "License :: OSI Approved :: BSD License",
27
27
  "Operating System :: MacOS :: MacOS X",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes