Skip to content

Commit 36522d1

Browse files
authored
extend check_capabilities (#92)
Allows this: ```python3 check_capabilities("user_status.enabled") ``` Needed for **Talk**, as it provides a very big amount of `capabilities`, need a clean way to check for them. --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent eb84d8b commit 36522d1

File tree

10 files changed

+112
-34
lines changed

10 files changed

+112
-34
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.0.32 - 2023-08-2x]
6+
7+
### Changed
8+
9+
- `require_capabilities`/`check_capabilities` can accept value with `dot`: like `files_sharing.api_enabled` and check for sub-values.
10+
511
## [0.0.31 - 2023-08-17]
612

713
### Added

docs/FirstSteps.rst

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Full support is only available from version ``27.1`` of Nextcloud.
1111
.. note:: In many cases, even if you want to develop an application,
1212
it's a good idea to first debug and develop part of it as a client.
1313

14+
Basics
15+
^^^^^^
16+
1417
Creating Nextcloud client class
1518
"""""""""""""""""""""""""""""""
1619

@@ -36,6 +39,40 @@ To test if this works, let's print the capabilities of the Nextcloud instance:
3639
pretty_capabilities = dumps(nc.capabilities, indent=4, sort_keys=True)
3740
print(pretty_capabilities)
3841
42+
Checking Nextcloud capabilities
43+
"""""""""""""""""""""""""""""""
44+
45+
In most cases, APIs perform capability checks before invoking them and raise a :class:`~nc_py_api._exceptions.NextcloudMissingCapabilities`
46+
exception if the Nextcloud instance lacks the requisite capability.
47+
However, there are situations where this approach might not be the most convenient,
48+
and you may wish to earlier whether a certain capability is available and active.
49+
50+
To address this need, the ``check_capabilities`` method is provided.
51+
This method offers a straightforward way to proactively check for the existence and status of a particular capability.
52+
53+
Using this method is quite simple:
54+
55+
.. code-block:: python
56+
57+
import nc_py_api
58+
59+
60+
nc = Nextcloud(nextcloud_url="http://nextcloud.local", nc_auth_user="admin", nc_auth_pass="admin")
61+
if nc.check_capabilities("files_sharing"): # check one capability
62+
print("Sharing API is not present.")
63+
64+
# check child values in the same call
65+
if nc.check_capabilities("files_sharing.api_enabled"):
66+
print("Sharing API is present, but is not enabled.")
67+
68+
# check multiply capabilities at one
69+
missing_cap = nc.check_capabilities(["files_sharing.api_enabled", "user_status.enabled"])
70+
if missing_cap:
71+
print(f"Missing capabilities: {missing_cap}")
72+
73+
Files
74+
^^^^^
75+
3976
Getting list of files of User
4077
"""""""""""""""""""""""""""""
4178

@@ -92,7 +129,7 @@ Example of using ``file.find()`` to search for file objects.
92129
.. literalinclude:: ../examples/as_client/files/find.py
93130

94131
Conclusion
95-
""""""""""
132+
^^^^^^^^^^
96133

97134
Once you have a good understanding of working with files, you can move on to more APIs.
98135

docs/reference/Exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ Avalaible as `nc_py_api.{exception_name}`
1010

1111
.. autoclass:: NextcloudExceptionNotFound
1212
:members:
13+
14+
.. autoclass:: NextcloudMissingCapabilities
15+
:members:

nc_py_api/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""All possible stuff for Nextcloud & NextcloudApp that can be used."""
22

33
from . import ex_app, options
4-
from ._exceptions import NextcloudException, NextcloudExceptionNotFound
4+
from ._exceptions import (
5+
NextcloudException,
6+
NextcloudExceptionNotFound,
7+
NextcloudMissingCapabilities,
8+
)
59
from ._version import __version__
610
from .files import FilePermissions, FsNode
711
from .files.sharing import ShareType

nc_py_api/_exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ def __init__(self, reason="Not found", info: str = ""):
2727
super().__init__(404, reason=reason, info=info)
2828

2929

30+
class NextcloudMissingCapabilities(NextcloudException):
31+
"""The exception that is thrown when required capability for API is missing."""
32+
33+
def __init__(self, reason="Missing capability", info: str = ""):
34+
super().__init__(412, reason=reason, info=info)
35+
36+
3037
def check_error(code: int, info: str = ""):
3138
"""Checks HTTP code from Nextcloud, and raises exception in case of error.
3239

nc_py_api/_misc.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from string import ascii_lowercase, ascii_uppercase, digits
88
from typing import Union
99

10-
from ._exceptions import NextcloudException
10+
from ._exceptions import NextcloudMissingCapabilities
1111

1212

1313
def kwargs_to_dict(keys: list[str], **kwargs) -> dict:
@@ -19,14 +19,32 @@ def require_capabilities(capabilities: Union[str, list[str]], srv_capabilities:
1919
"""Checks for capabilities and raises an exception if any of them are missing."""
2020
result = check_capabilities(capabilities, srv_capabilities)
2121
if result:
22-
raise NextcloudException(404, f"{result} is not available")
22+
raise NextcloudMissingCapabilities(info=str(result))
23+
24+
25+
def __check_sub_capability(split_capabilities: list[str], srv_capabilities: dict) -> bool:
26+
"""Returns ``True`` if such capability is present and **enabled**."""
27+
n_split_capabilities = len(split_capabilities)
28+
capabilities_nesting = srv_capabilities
29+
for i, v in enumerate(split_capabilities):
30+
if i != 0 and i == n_split_capabilities - 1:
31+
return bool(capabilities_nesting.get(v, False))
32+
if v not in capabilities_nesting:
33+
return False
34+
capabilities_nesting = capabilities_nesting[v]
35+
return True
2336

2437

2538
def check_capabilities(capabilities: Union[str, list[str]], srv_capabilities: dict) -> list[str]:
2639
"""Checks for capabilities and returns a list of missing ones."""
2740
if isinstance(capabilities, str):
2841
capabilities = [capabilities]
29-
return [i for i in capabilities if i not in srv_capabilities]
42+
missing_capabilities = []
43+
for capability in capabilities:
44+
split_capabilities = capability.split(".")
45+
if not __check_sub_capability(split_capabilities, srv_capabilities):
46+
missing_capabilities.append(capability)
47+
return missing_capabilities
3048

3149

3250
def random_string(size: int) -> str:

nc_py_api/files/sharing.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def __init__(self, session: _session.NcSessionBasic):
131131
@property
132132
def available(self) -> bool:
133133
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
134-
return not _misc.check_capabilities("files_sharing", self._session.capabilities)
134+
return not _misc.check_capabilities("files_sharing.api_enabled", self._session.capabilities)
135135

136136
def get_list(
137137
self, shared_with_me=False, reshares=False, subfiles=False, path: typing.Union[str, FsNode] = ""
@@ -143,7 +143,7 @@ def get_list(
143143
:param subfiles: Only get all sub shares in a folder.
144144
:param path: Get shares for a specific path.
145145
"""
146-
_misc.require_capabilities("files_sharing", self._session.capabilities)
146+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
147147
path = path.user_path if isinstance(path, FsNode) else path
148148
params = {
149149
"shared_with_me": "true" if shared_with_me else "false",
@@ -157,13 +157,13 @@ def get_list(
157157

158158
def get_by_id(self, share_id: int) -> Share:
159159
"""Get Share by share ID."""
160-
_misc.require_capabilities("files_sharing", self._session.capabilities)
160+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
161161
result = self._session.ocs(method="GET", path=f"{self._ep_base}/shares/{share_id}")
162162
return Share(result[0] if isinstance(result, list) else result)
163163

164164
def get_inherited(self, path: str) -> list[Share]:
165165
"""Get all shares relative to a file, e.g., parent folders shares."""
166-
_misc.require_capabilities("files_sharing", self._session.capabilities)
166+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
167167
result = self._session.ocs(method="GET", path=f"{self._ep_base}/shares/inherited", params={"path": path})
168168
return [Share(i) for i in result]
169169

@@ -195,7 +195,7 @@ def create(
195195
* ``note`` - string with note, if any. default = ``""``
196196
* ``label`` - string with label, if any. default = ``""``
197197
"""
198-
_misc.require_capabilities("files_sharing", self._session.capabilities)
198+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
199199
path = path.user_path if isinstance(path, FsNode) else path
200200
params = {
201201
"path": path,
@@ -226,7 +226,7 @@ def update(self, share_id: typing.Union[int, Share], **kwargs) -> Share:
226226
:param kwargs: Available for update: ``permissions``, ``password``, ``send_password_by_talk``,
227227
``public_upload``, ``expire_date``, ``note``, ``label``.
228228
"""
229-
_misc.require_capabilities("files_sharing", self._session.capabilities)
229+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
230230
share_id = share_id.share_id if isinstance(share_id, Share) else share_id
231231
params: dict = {}
232232
if "permissions" in kwargs:
@@ -250,7 +250,7 @@ def delete(self, share_id: typing.Union[int, Share]) -> None:
250250
251251
:param share_id: The Share object or an ID of the share.
252252
"""
253-
_misc.require_capabilities("files_sharing", self._session.capabilities)
253+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
254254
share_id = share_id.share_id if isinstance(share_id, Share) else share_id
255255
self._session.ocs(method="DELETE", path=f"{self._ep_base}/shares/{share_id}")
256256

@@ -260,23 +260,23 @@ def get_pending(self) -> list[Share]:
260260

261261
def accept_share(self, share_id: typing.Union[int, Share]) -> None:
262262
"""Accept pending share."""
263-
_misc.require_capabilities("files_sharing", self._session.capabilities)
263+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
264264
share_id = share_id.share_id if isinstance(share_id, Share) else share_id
265265
self._session.ocs(method="POST", path=f"{self._ep_base}/pending/{share_id}")
266266

267267
def decline_share(self, share_id: typing.Union[int, Share]) -> None:
268268
"""Decline pending share."""
269-
_misc.require_capabilities("files_sharing", self._session.capabilities)
269+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
270270
share_id = share_id.share_id if isinstance(share_id, Share) else share_id
271271
self._session.ocs(method="DELETE", path=f"{self._ep_base}/pending/{share_id}")
272272

273273
def get_deleted(self) -> list[Share]:
274274
"""Get a list of deleted shares."""
275-
_misc.require_capabilities("files_sharing", self._session.capabilities)
275+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
276276
return [Share(i) for i in self._session.ocs(method="GET", path=f"{self._ep_base}/deletedshares")]
277277

278278
def undelete(self, share_id: typing.Union[int, Share]) -> None:
279279
"""Undelete a deleted share."""
280-
_misc.require_capabilities("files_sharing", self._session.capabilities)
280+
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
281281
share_id = share_id.share_id if isinstance(share_id, Share) else share_id
282282
self._session.ocs(method="POST", path=f"{self._ep_base}/deletedshares/{share_id}")

nc_py_api/users/status.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,30 @@ def __init__(self, session: NcSessionBasic):
9898
@property
9999
def available(self) -> bool:
100100
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
101-
return not check_capabilities("user_status", self._session.capabilities)
101+
return not check_capabilities("user_status.enabled", self._session.capabilities)
102102

103103
def get_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> list[UserStatus]:
104104
"""Returns statuses for all users.
105105
106106
:param limit: limits the number of results.
107107
:param offset: offset of results.
108108
"""
109-
require_capabilities("user_status", self._session.capabilities)
109+
require_capabilities("user_status.enabled", self._session.capabilities)
110110
data = kwargs_to_dict(["limit", "offset"], limit=limit, offset=offset)
111111
result = self._session.ocs(method="GET", path=f"{self._ep_base}/statuses", params=data)
112112
return [UserStatus(i) for i in result]
113113

114114
def get_current(self) -> CurrentUserStatus:
115115
"""Returns the current user status."""
116-
require_capabilities("user_status", self._session.capabilities)
116+
require_capabilities("user_status.enabled", self._session.capabilities)
117117
return CurrentUserStatus(self._session.ocs(method="GET", path=f"{self._ep_base}/user_status"))
118118

119119
def get(self, user_id: str) -> Optional[UserStatus]:
120120
"""Returns the user status for the specified user.
121121
122122
:param user_id: User ID for getting status.
123123
"""
124-
require_capabilities("user_status", self._session.capabilities)
124+
require_capabilities("user_status.enabled", self._session.capabilities)
125125
try:
126126
return UserStatus(self._session.ocs(method="GET", path=f"{self._ep_base}/statuses/{user_id}"))
127127
except NextcloudExceptionNotFound:
@@ -131,7 +131,7 @@ def get_predefined(self) -> list[PredefinedStatus]:
131131
"""Returns a list of predefined statuses available for installation on this Nextcloud instance."""
132132
if self._session.nc_version["major"] < 27:
133133
return []
134-
require_capabilities("user_status", self._session.capabilities)
134+
require_capabilities("user_status.enabled", self._session.capabilities)
135135
result = self._session.ocs(method="GET", path=f"{self._ep_base}/predefined_statuses")
136136
return [PredefinedStatus(i) for i in result]
137137

@@ -143,14 +143,15 @@ def set_predefined(self, status_id: str, clear_at: int = 0) -> None:
143143
"""
144144
if self._session.nc_version["major"] < 27:
145145
return
146-
require_capabilities("user_status", self._session.capabilities)
146+
require_capabilities("user_status.enabled", self._session.capabilities)
147147
params: dict[str, Union[int, str]] = {"messageId": status_id}
148148
if clear_at:
149149
params["clearAt"] = clear_at
150150
self._session.ocs(method="PUT", path=f"{self._ep_base}/user_status/message/predefined", params=params)
151151

152152
def set_status_type(self, value: Literal["online", "away", "dnd", "invisible", "offline"]) -> None:
153153
"""Sets the status type for the current user."""
154+
require_capabilities("user_status.enabled", self._session.capabilities)
154155
self._session.ocs(method="PUT", path=f"{self._ep_base}/user_status/status", params={"statusType": value})
155156

156157
def set_status(self, message: Optional[str] = None, clear_at: int = 0, status_icon: str = "") -> None:
@@ -160,12 +161,12 @@ def set_status(self, message: Optional[str] = None, clear_at: int = 0, status_ic
160161
:param clear_at: Unix Timestamp, representing the time to clear the status.
161162
:param status_icon: The icon picked by the user (must be one emoji)
162163
"""
163-
require_capabilities("user_status", self._session.capabilities)
164+
require_capabilities("user_status.enabled", self._session.capabilities)
164165
if message is None:
165166
self._session.ocs(method="DELETE", path=f"{self._ep_base}/user_status/message")
166167
return
167168
if status_icon:
168-
require_capabilities("supports_emoji", self._session.capabilities["user_status"])
169+
require_capabilities("user_status.supports_emoji", self._session.capabilities)
169170
params: dict[str, Union[int, str]] = {"message": message}
170171
if clear_at:
171172
params["clearAt"] = clear_at
@@ -178,7 +179,7 @@ def get_backup_status(self, user_id: str = "") -> Optional[UserStatus]:
178179
179180
:param user_id: User ID for getting status.
180181
"""
181-
require_capabilities("user_status", self._session.capabilities)
182+
require_capabilities("user_status.enabled", self._session.capabilities)
182183
user_id = user_id if user_id else self._session.user
183184
if not user_id:
184185
raise ValueError("user_id can not be empty.")
@@ -189,7 +190,7 @@ def restore_backup_status(self, status_id: str) -> Optional[CurrentUserStatus]:
189190
190191
:param status_id: backup status ID.
191192
"""
192-
require_capabilities("user_status", self._session.capabilities)
193-
require_capabilities("restore", self._session.capabilities["user_status"])
193+
require_capabilities("user_status.enabled", self._session.capabilities)
194+
require_capabilities("user_status.restore", self._session.capabilities)
194195
result = self._session.ocs(method="DELETE", path=f"{self._ep_base}/user_status/revert/{status_id}")
195196
return result if result else None

nc_py_api/users/weather.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ def __init__(self, session: NcSessionBasic):
5252
@property
5353
def available(self) -> bool:
5454
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
55-
return not check_capabilities("weather_status", self._session.capabilities)
55+
return not check_capabilities("weather_status.enabled", self._session.capabilities)
5656

5757
def get_location(self) -> WeatherLocation:
5858
"""Returns the current location set on the Nextcloud server for the user."""
59-
require_capabilities("weather_status", self._session.capabilities)
59+
require_capabilities("weather_status.enabled", self._session.capabilities)
6060
return WeatherLocation(self._session.ocs(method="GET", path=f"{self._ep_base}/location"))
6161

6262
def set_location(
@@ -68,7 +68,7 @@ def set_location(
6868
:param longitude: east–west position of a point on the surface of the Earth.
6969
:param address: city, index(*optional*) and country, e.g. "Paris, 75007, France"
7070
"""
71-
require_capabilities("weather_status", self._session.capabilities)
71+
require_capabilities("weather_status.enabled", self._session.capabilities)
7272
params: dict[str, Union[str, float]] = {}
7373
if latitude is not None and longitude is not None:
7474
params.update({"lat": latitude, "lon": longitude})
@@ -81,24 +81,24 @@ def set_location(
8181

8282
def get_forecast(self) -> list[dict]:
8383
"""Get forecast for the current location."""
84-
require_capabilities("weather_status", self._session.capabilities)
84+
require_capabilities("weather_status.enabled", self._session.capabilities)
8585
return self._session.ocs(method="GET", path=f"{self._ep_base}/forecast")
8686

8787
def get_favorites(self) -> list[str]:
8888
"""Returns favorites addresses list."""
89-
require_capabilities("weather_status", self._session.capabilities)
89+
require_capabilities("weather_status.enabled", self._session.capabilities)
9090
return self._session.ocs(method="GET", path=f"{self._ep_base}/favorites")
9191

9292
def set_favorites(self, favorites: list[str]) -> bool:
9393
"""Sets favorites addresses list."""
94-
require_capabilities("weather_status", self._session.capabilities)
94+
require_capabilities("weather_status.enabled", self._session.capabilities)
9595
result = self._session.ocs(method="PUT", path=f"{self._ep_base}/favorites", json={"favorites": favorites})
9696
return result.get("success", False)
9797

9898
def set_mode(self, mode: WeatherLocationMode) -> bool:
9999
"""Change the weather status mode."""
100100
if int(mode) == WeatherLocationMode.UNKNOWN.value:
101101
raise ValueError("This mode can not be set")
102-
require_capabilities("weather_status", self._session.capabilities)
102+
require_capabilities("weather_status.enabled", self._session.capabilities)
103103
result = self._session.ocs(method="PUT", path=f"{self._ep_base}/mode", params={"mode": int(mode)})
104104
return result.get("success", False)

tests/misc_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def test_require_capabilities():
3636
require_capabilities(["non_exist_capability", "app_ecosystem_v2"], NC_APP.capabilities)
3737
with pytest.raises(NextcloudException):
3838
require_capabilities(["non_exist_capability", "non_exist_capability2", "app_ecosystem_v2"], NC_APP.capabilities)
39+
with pytest.raises(NextcloudException):
40+
require_capabilities("app_ecosystem_v2.non_exist_capability", NC_APP.capabilities)
3941

4042

4143
def test_config_get_value():

0 commit comments

Comments
 (0)