nc-py-api 0.20.1__tar.gz → 0.21.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.
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/CHANGELOG.md +16 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/PKG-INFO +2 -3
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_exceptions.py +6 -4
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_preferences_ex.py +32 -48
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_session.py +116 -72
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_version.py +1 -1
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/calendar_api.py +1 -1
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/integration_fastapi.py +4 -3
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/settings.py +2 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/files/_files.py +1 -1
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/files/files.py +17 -15
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/files/files_async.py +19 -17
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/loginflow_v2.py +2 -2
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/nextcloud.py +5 -5
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/notes.py +2 -2
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/options.py +3 -12
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/talk_bot.py +20 -13
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/users.py +4 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/pyproject.toml +1 -2
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/.gitignore +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/AUTHORS +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/LICENSE.txt +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/README.md +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/__init__.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_deffered_error.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_misc.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_preferences.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_talk_api.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/_theming.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/activity.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/apps.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/__init__.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/defs.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/logger.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/misc.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/occ_commands.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/persist_transformers_cache.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/providers/__init__.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/providers/providers.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/providers/task_processing.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/__init__.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/files_actions.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/resources.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/top_menu.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/ui/ui.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/ex_app/uvicorn_fastapi.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/files/__init__.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/files/sharing.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/notifications.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/talk.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/user_status.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/users_groups.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/weather_status.py +0 -0
- {nc_py_api-0.20.1 → nc_py_api-0.21.0}/nc_py_api/webhooks.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.21.0 - 2025-08-26]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added wipe method to UsersAPI. #368 Thanks to @MrAalen
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Switch from `httpx` library to the `niquests` library. #375 Thanks to @Ousret
|
|
14
|
+
|
|
15
|
+
## [0.20.2 - 2025-05-28]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- ExApps(NC32+): field `sensitive` to the `SettingsField` class (Declarative Settings) and to the `PreferencesExAPI` class. #357
|
|
20
|
+
|
|
5
21
|
## [0.20.1 - 2025-05-06]
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nc-py-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.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/
|
|
@@ -31,10 +31,9 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
31
31
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
32
32
|
Requires-Python: >=3.10
|
|
33
33
|
Requires-Dist: fastapi>=0.109.2
|
|
34
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: niquests<4,>=3
|
|
35
35
|
Requires-Dist: pydantic>=2.1.1
|
|
36
36
|
Requires-Dist: python-dotenv>=1
|
|
37
|
-
Requires-Dist: truststore==0.10
|
|
38
37
|
Requires-Dist: xmltodict>=0.13
|
|
39
38
|
Provides-Extra: app
|
|
40
39
|
Requires-Dist: uvicorn[standard]>=0.23.2; extra == 'app'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Exceptions for the Nextcloud API."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from niquests import HTTPError, Response
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class NextcloudException(Exception):
|
|
@@ -60,6 +60,8 @@ def check_error(response: Response, info: str = ""):
|
|
|
60
60
|
else:
|
|
61
61
|
phrase = "Unknown error"
|
|
62
62
|
raise NextcloudException(status_code, reason=phrase, info=info)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
except HTTPError as e:
|
|
67
|
+
raise NextcloudException(status_code, reason=response.reason, info=info) from e
|
|
@@ -61,6 +61,21 @@ class _BasicAppCfgPref:
|
|
|
61
61
|
if not not_fail:
|
|
62
62
|
raise e from None
|
|
63
63
|
|
|
64
|
+
def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
|
|
65
|
+
"""Sets a value and if specified the sensitive flag for a key.
|
|
66
|
+
|
|
67
|
+
.. note:: A sensitive flag ensures key value are encrypted and truncated in Nextcloud logs.
|
|
68
|
+
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
|
|
69
|
+
sensitive is *unspecified* it will not change the existing `sensitive` flag.
|
|
70
|
+
"""
|
|
71
|
+
if not key:
|
|
72
|
+
raise ValueError("`key` parameter can not be empty")
|
|
73
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
74
|
+
params: dict = {"configKey": key, "configValue": value}
|
|
75
|
+
if sensitive is not None:
|
|
76
|
+
params["sensitive"] = sensitive
|
|
77
|
+
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
78
|
+
|
|
64
79
|
|
|
65
80
|
class _AsyncBasicAppCfgPref:
|
|
66
81
|
_url_suffix: str
|
|
@@ -104,72 +119,41 @@ class _AsyncBasicAppCfgPref:
|
|
|
104
119
|
if not not_fail:
|
|
105
120
|
raise e from None
|
|
106
121
|
|
|
122
|
+
async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
|
|
123
|
+
"""Sets a value and if specified the sensitive flag for a key.
|
|
124
|
+
|
|
125
|
+
.. note:: A sensitive flag ensures key value are encrypted and truncated in Nextcloud logs.
|
|
126
|
+
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
|
|
127
|
+
sensitive is *unspecified* it will not change the existing `sensitive` flag.
|
|
128
|
+
"""
|
|
129
|
+
if not key:
|
|
130
|
+
raise ValueError("`key` parameter can not be empty")
|
|
131
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
132
|
+
params: dict = {"configKey": key, "configValue": value}
|
|
133
|
+
if sensitive is not None:
|
|
134
|
+
params["sensitive"] = sensitive
|
|
135
|
+
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
136
|
+
|
|
107
137
|
|
|
108
138
|
class PreferencesExAPI(_BasicAppCfgPref):
|
|
109
|
-
"""User specific preferences API,
|
|
139
|
+
"""User specific preferences API, available as **nc.preferences_ex.<method>**."""
|
|
110
140
|
|
|
111
141
|
_url_suffix = "ex-app/preference"
|
|
112
142
|
|
|
113
|
-
def set_value(self, key: str, value: str) -> None:
|
|
114
|
-
"""Sets a value for a key."""
|
|
115
|
-
if not key:
|
|
116
|
-
raise ValueError("`key` parameter can not be empty")
|
|
117
|
-
require_capabilities("app_api", self._session.capabilities)
|
|
118
|
-
params = {"configKey": key, "configValue": value}
|
|
119
|
-
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
120
|
-
|
|
121
143
|
|
|
122
144
|
class AsyncPreferencesExAPI(_AsyncBasicAppCfgPref):
|
|
123
145
|
"""User specific preferences API."""
|
|
124
146
|
|
|
125
147
|
_url_suffix = "ex-app/preference"
|
|
126
148
|
|
|
127
|
-
async def set_value(self, key: str, value: str) -> None:
|
|
128
|
-
"""Sets a value for a key."""
|
|
129
|
-
if not key:
|
|
130
|
-
raise ValueError("`key` parameter can not be empty")
|
|
131
|
-
require_capabilities("app_api", await self._session.capabilities)
|
|
132
|
-
params = {"configKey": key, "configValue": value}
|
|
133
|
-
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
134
|
-
|
|
135
149
|
|
|
136
150
|
class AppConfigExAPI(_BasicAppCfgPref):
|
|
137
|
-
"""Non-user(App) specific preferences API,
|
|
151
|
+
"""Non-user(App) specific preferences API, available as **nc.appconfig_ex.<method>**."""
|
|
138
152
|
|
|
139
153
|
_url_suffix = "ex-app/config"
|
|
140
154
|
|
|
141
|
-
def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
|
|
142
|
-
"""Sets a value and if specified the sensitive flag for a key.
|
|
143
|
-
|
|
144
|
-
.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
|
|
145
|
-
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
|
|
146
|
-
sensitive is *unspecified* it will not change the existing `sensitive` flag.
|
|
147
|
-
"""
|
|
148
|
-
if not key:
|
|
149
|
-
raise ValueError("`key` parameter can not be empty")
|
|
150
|
-
require_capabilities("app_api", self._session.capabilities)
|
|
151
|
-
params: dict = {"configKey": key, "configValue": value}
|
|
152
|
-
if sensitive is not None:
|
|
153
|
-
params["sensitive"] = sensitive
|
|
154
|
-
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
155
|
-
|
|
156
155
|
|
|
157
156
|
class AsyncAppConfigExAPI(_AsyncBasicAppCfgPref):
|
|
158
157
|
"""Non-user(App) specific preferences API."""
|
|
159
158
|
|
|
160
159
|
_url_suffix = "ex-app/config"
|
|
161
|
-
|
|
162
|
-
async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
|
|
163
|
-
"""Sets a value and if specified the sensitive flag for a key.
|
|
164
|
-
|
|
165
|
-
.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
|
|
166
|
-
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
|
|
167
|
-
sensitive is *unspecified* it will not change the existing `sensitive` flag.
|
|
168
|
-
"""
|
|
169
|
-
if not key:
|
|
170
|
-
raise ValueError("`key` parameter can not be empty")
|
|
171
|
-
require_capabilities("app_api", await self._session.capabilities)
|
|
172
|
-
params: dict = {"configKey": key, "configValue": value}
|
|
173
|
-
if sensitive is not None:
|
|
174
|
-
params["sensitive"] = sensitive
|
|
175
|
-
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
|
|
@@ -10,9 +10,11 @@ from dataclasses import dataclass
|
|
|
10
10
|
from enum import IntEnum
|
|
11
11
|
from json import loads
|
|
12
12
|
from os import environ
|
|
13
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
13
14
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
15
|
+
from niquests import AsyncSession, ReadTimeout, Request, Response, Session
|
|
16
|
+
from niquests import __version__ as niquests_version
|
|
17
|
+
from niquests.structures import CaseInsensitiveDict
|
|
16
18
|
from starlette.requests import HTTPConnection
|
|
17
19
|
|
|
18
20
|
from . import options
|
|
@@ -49,6 +51,13 @@ class ServerVersion(typing.TypedDict):
|
|
|
49
51
|
"""Indicates if the subscription has extended support"""
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
@dataclass
|
|
55
|
+
class Limits:
|
|
56
|
+
max_keepalive_connections: int | None = 20
|
|
57
|
+
max_connections: int | None = 100
|
|
58
|
+
keepalive_expiry: int | float | None = 5
|
|
59
|
+
|
|
60
|
+
|
|
52
61
|
@dataclass
|
|
53
62
|
class RuntimeOptions:
|
|
54
63
|
xdebug_session: str
|
|
@@ -134,11 +143,11 @@ class AppConfig(BasicConfig):
|
|
|
134
143
|
|
|
135
144
|
|
|
136
145
|
class NcSessionBase(ABC):
|
|
137
|
-
adapter:
|
|
138
|
-
adapter_dav:
|
|
146
|
+
adapter: AsyncSession | Session
|
|
147
|
+
adapter_dav: AsyncSession | Session
|
|
139
148
|
cfg: BasicConfig
|
|
140
149
|
custom_headers: dict
|
|
141
|
-
response_headers:
|
|
150
|
+
response_headers: CaseInsensitiveDict
|
|
142
151
|
_user: str
|
|
143
152
|
_capabilities: dict
|
|
144
153
|
|
|
@@ -150,7 +159,7 @@ class NcSessionBase(ABC):
|
|
|
150
159
|
self.limits = Limits(max_keepalive_connections=20, max_connections=20, keepalive_expiry=60.0)
|
|
151
160
|
self.init_adapter()
|
|
152
161
|
self.init_adapter_dav()
|
|
153
|
-
self.response_headers =
|
|
162
|
+
self.response_headers = CaseInsensitiveDict()
|
|
154
163
|
self._ocs_regexp = re.compile(r"/ocs/v[12]\.php/|/apps/groupfolders/")
|
|
155
164
|
|
|
156
165
|
def init_adapter(self, restart=False) -> None:
|
|
@@ -172,7 +181,7 @@ class NcSessionBase(ABC):
|
|
|
172
181
|
self.adapter_dav.cookies.set("XDEBUG_SESSION", options.XDEBUG_SESSION)
|
|
173
182
|
|
|
174
183
|
@abstractmethod
|
|
175
|
-
def _create_adapter(self, dav: bool = False) ->
|
|
184
|
+
def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
|
|
176
185
|
pass # pragma: no cover
|
|
177
186
|
|
|
178
187
|
@property
|
|
@@ -187,8 +196,8 @@ class NcSessionBase(ABC):
|
|
|
187
196
|
|
|
188
197
|
|
|
189
198
|
class NcSessionBasic(NcSessionBase, ABC):
|
|
190
|
-
adapter:
|
|
191
|
-
adapter_dav:
|
|
199
|
+
adapter: Session
|
|
200
|
+
adapter_dav: Session
|
|
192
201
|
|
|
193
202
|
def ocs(
|
|
194
203
|
self,
|
|
@@ -206,9 +215,7 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
206
215
|
info = f"request: {method} {path}"
|
|
207
216
|
nested_req = kwargs.pop("nested_req", False)
|
|
208
217
|
try:
|
|
209
|
-
response = self.adapter.request(
|
|
210
|
-
method, path, content=content, json=json, params=params, files=files, **kwargs
|
|
211
|
-
)
|
|
218
|
+
response = self.adapter.request(method, path, data=content, json=json, params=params, files=files, **kwargs)
|
|
212
219
|
except ReadTimeout:
|
|
213
220
|
raise NextcloudException(408, info=info) from None
|
|
214
221
|
|
|
@@ -281,18 +288,18 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
281
288
|
return {
|
|
282
289
|
"base_url": self.cfg.dav_endpoint,
|
|
283
290
|
"timeout": self.cfg.options.timeout_dav,
|
|
284
|
-
"event_hooks": {"
|
|
291
|
+
"event_hooks": {"pre_request": [], "response": [self._response_event]},
|
|
285
292
|
}
|
|
286
293
|
return {
|
|
287
294
|
"base_url": self.cfg.endpoint,
|
|
288
295
|
"timeout": self.cfg.options.timeout,
|
|
289
|
-
"event_hooks": {"
|
|
296
|
+
"event_hooks": {"pre_request": [self._request_event_ocs], "response": [self._response_event]},
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
def _request_event_ocs(self, request: Request) -> None:
|
|
293
300
|
str_url = str(request.url)
|
|
294
301
|
if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
|
|
295
|
-
request.url = request.url
|
|
302
|
+
request.url = patch_param(request.url, "format", "json")
|
|
296
303
|
request.headers["Accept"] = "application/json"
|
|
297
304
|
|
|
298
305
|
def _response_event(self, response: Response) -> None:
|
|
@@ -305,15 +312,15 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
305
312
|
|
|
306
313
|
def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
307
314
|
adapter = self.adapter_dav if dav else self.adapter
|
|
308
|
-
with adapter.
|
|
315
|
+
with adapter.get(url_path, params=params, headers=kwargs.get("headers"), stream=True) as response:
|
|
309
316
|
check_error(response)
|
|
310
|
-
for data_chunk in response.
|
|
317
|
+
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", -1)):
|
|
311
318
|
fp.write(data_chunk)
|
|
312
319
|
|
|
313
320
|
|
|
314
321
|
class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
315
|
-
adapter:
|
|
316
|
-
adapter_dav:
|
|
322
|
+
adapter: AsyncSession
|
|
323
|
+
adapter_dav: AsyncSession
|
|
317
324
|
|
|
318
325
|
async def ocs(
|
|
319
326
|
self,
|
|
@@ -332,7 +339,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
332
339
|
nested_req = kwargs.pop("nested_req", False)
|
|
333
340
|
try:
|
|
334
341
|
response = await self.adapter.request(
|
|
335
|
-
method, path,
|
|
342
|
+
method, path, data=content, json=json, params=params, files=files, **kwargs
|
|
336
343
|
)
|
|
337
344
|
except ReadTimeout:
|
|
338
345
|
raise NextcloudException(408, info=info) from None
|
|
@@ -350,7 +357,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
350
357
|
and ocs_meta["statuscode"] == 403
|
|
351
358
|
and str(ocs_meta["message"]).lower().find("password confirmation is required") != -1
|
|
352
359
|
):
|
|
353
|
-
await self.adapter.
|
|
360
|
+
await self.adapter.close()
|
|
354
361
|
self.init_adapter(restart=True)
|
|
355
362
|
return await self.ocs(
|
|
356
363
|
method, path, **kwargs, content=content, json=json, params=params, nested_req=True
|
|
@@ -408,18 +415,18 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
408
415
|
return {
|
|
409
416
|
"base_url": self.cfg.dav_endpoint,
|
|
410
417
|
"timeout": self.cfg.options.timeout_dav,
|
|
411
|
-
"event_hooks": {"
|
|
418
|
+
"event_hooks": {"pre_request": [], "response": [self._response_event]},
|
|
412
419
|
}
|
|
413
420
|
return {
|
|
414
421
|
"base_url": self.cfg.endpoint,
|
|
415
422
|
"timeout": self.cfg.options.timeout,
|
|
416
|
-
"event_hooks": {"
|
|
423
|
+
"event_hooks": {"pre_request": [self._request_event_ocs], "response": [self._response_event]},
|
|
417
424
|
}
|
|
418
425
|
|
|
419
426
|
async def _request_event_ocs(self, request: Request) -> None:
|
|
420
427
|
str_url = str(request.url)
|
|
421
428
|
if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
|
|
422
|
-
request.url = request.url
|
|
429
|
+
request.url = patch_param(request.url, "format", "json")
|
|
423
430
|
request.headers["Accept"] = "application/json"
|
|
424
431
|
|
|
425
432
|
async def _response_event(self, response: Response) -> None:
|
|
@@ -432,10 +439,12 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
432
439
|
|
|
433
440
|
async def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
434
441
|
adapter = self.adapter_dav if dav else self.adapter
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
response = await adapter.get(url_path, params=params, headers=kwargs.get("headers"), stream=True)
|
|
443
|
+
|
|
444
|
+
check_error(response)
|
|
445
|
+
|
|
446
|
+
async for data_chunk in await response.iter_raw(chunk_size=kwargs.get("chunk_size", -1)):
|
|
447
|
+
fp.write(data_chunk)
|
|
439
448
|
|
|
440
449
|
|
|
441
450
|
class NcSession(NcSessionBasic):
|
|
@@ -445,15 +454,20 @@ class NcSession(NcSessionBasic):
|
|
|
445
454
|
self.cfg = Config(**kwargs)
|
|
446
455
|
super().__init__()
|
|
447
456
|
|
|
448
|
-
def _create_adapter(self, dav: bool = False) ->
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
auth=self.cfg.auth,
|
|
457
|
+
def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
|
|
458
|
+
session_kwargs = self._get_adapter_kwargs(dav)
|
|
459
|
+
hooks = session_kwargs.pop("event_hooks")
|
|
460
|
+
|
|
461
|
+
session = Session(
|
|
462
|
+
keepalive_delay=self.limits.keepalive_expiry, pool_maxsize=self.limits.max_connections, **session_kwargs
|
|
455
463
|
)
|
|
456
464
|
|
|
465
|
+
session.auth = self.cfg.auth
|
|
466
|
+
session.verify = self.cfg.options.nc_cert
|
|
467
|
+
session.hooks.update(hooks)
|
|
468
|
+
|
|
469
|
+
return session
|
|
470
|
+
|
|
457
471
|
|
|
458
472
|
class AsyncNcSession(AsyncNcSessionBasic):
|
|
459
473
|
cfg: Config
|
|
@@ -462,21 +476,28 @@ class AsyncNcSession(AsyncNcSessionBasic):
|
|
|
462
476
|
self.cfg = Config(**kwargs)
|
|
463
477
|
super().__init__()
|
|
464
478
|
|
|
465
|
-
def _create_adapter(self, dav: bool = False) ->
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
479
|
+
def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
|
|
480
|
+
session_kwargs = self._get_adapter_kwargs(dav)
|
|
481
|
+
hooks = session_kwargs.pop("event_hooks")
|
|
482
|
+
|
|
483
|
+
session = AsyncSession(
|
|
484
|
+
keepalive_delay=self.limits.keepalive_expiry,
|
|
485
|
+
pool_maxsize=self.limits.max_connections,
|
|
486
|
+
**session_kwargs,
|
|
472
487
|
)
|
|
473
488
|
|
|
489
|
+
session.verify = self.cfg.options.nc_cert
|
|
490
|
+
session.auth = self.cfg.auth
|
|
491
|
+
session.hooks.update(hooks)
|
|
492
|
+
|
|
493
|
+
return session
|
|
494
|
+
|
|
474
495
|
|
|
475
496
|
class NcSessionAppBasic(ABC):
|
|
476
497
|
cfg: AppConfig
|
|
477
498
|
_user: str
|
|
478
|
-
adapter:
|
|
479
|
-
adapter_dav:
|
|
499
|
+
adapter: AsyncSession | Session
|
|
500
|
+
adapter_dav: AsyncSession | Session
|
|
480
501
|
|
|
481
502
|
def __init__(self, **kwargs):
|
|
482
503
|
self.cfg = AppConfig(**kwargs)
|
|
@@ -505,22 +526,29 @@ class NcSessionAppBasic(ABC):
|
|
|
505
526
|
class NcSessionApp(NcSessionAppBasic, NcSessionBasic):
|
|
506
527
|
cfg: AppConfig
|
|
507
528
|
|
|
508
|
-
def _create_adapter(self, dav: bool = False) ->
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"EX-APP-ID": self.cfg.app_name,
|
|
519
|
-
"EX-APP-VERSION": self.cfg.app_version,
|
|
520
|
-
"user-agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (httpx/{httpx_version})",
|
|
521
|
-
},
|
|
529
|
+
def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
|
|
530
|
+
session_kwargs = self._get_adapter_kwargs(dav)
|
|
531
|
+
session_kwargs["event_hooks"]["pre_request"].append(self._add_auth)
|
|
532
|
+
|
|
533
|
+
hooks = session_kwargs.pop("event_hooks")
|
|
534
|
+
|
|
535
|
+
session = Session(
|
|
536
|
+
keepalive_delay=self.limits.keepalive_expiry,
|
|
537
|
+
pool_maxsize=self.limits.max_connections,
|
|
538
|
+
**session_kwargs,
|
|
522
539
|
)
|
|
523
540
|
|
|
541
|
+
session.verify = self.cfg.options.nc_cert
|
|
542
|
+
session.headers = {
|
|
543
|
+
"AA-VERSION": self.cfg.aa_version,
|
|
544
|
+
"EX-APP-ID": self.cfg.app_name,
|
|
545
|
+
"EX-APP-VERSION": self.cfg.app_version,
|
|
546
|
+
"user-agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (niquests/{niquests_version})",
|
|
547
|
+
}
|
|
548
|
+
session.hooks.update(hooks)
|
|
549
|
+
|
|
550
|
+
return session
|
|
551
|
+
|
|
524
552
|
def _add_auth(self, request: Request):
|
|
525
553
|
request.headers.update(
|
|
526
554
|
{"AUTHORIZATION-APP-API": b64encode(f"{self._user}:{self.cfg.app_secret}".encode("UTF=8"))}
|
|
@@ -530,23 +558,39 @@ class NcSessionApp(NcSessionAppBasic, NcSessionBasic):
|
|
|
530
558
|
class AsyncNcSessionApp(NcSessionAppBasic, AsyncNcSessionBasic):
|
|
531
559
|
cfg: AppConfig
|
|
532
560
|
|
|
533
|
-
def _create_adapter(self, dav: bool = False) ->
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
"EX-APP-ID": self.cfg.app_name,
|
|
544
|
-
"EX-APP-VERSION": self.cfg.app_version,
|
|
545
|
-
"User-Agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (httpx/{httpx_version})",
|
|
546
|
-
},
|
|
561
|
+
def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
|
|
562
|
+
session_kwargs = self._get_adapter_kwargs(dav)
|
|
563
|
+
session_kwargs["event_hooks"]["pre_request"].append(self._add_auth)
|
|
564
|
+
|
|
565
|
+
hooks = session_kwargs.pop("event_hooks")
|
|
566
|
+
|
|
567
|
+
session = AsyncSession(
|
|
568
|
+
keepalive_delay=self.limits.keepalive_expiry,
|
|
569
|
+
pool_maxsize=self.limits.max_connections,
|
|
570
|
+
**session_kwargs,
|
|
547
571
|
)
|
|
572
|
+
session.verify = self.cfg.options.nc_cert
|
|
573
|
+
session.headers = {
|
|
574
|
+
"AA-VERSION": self.cfg.aa_version,
|
|
575
|
+
"EX-APP-ID": self.cfg.app_name,
|
|
576
|
+
"EX-APP-VERSION": self.cfg.app_version,
|
|
577
|
+
"User-Agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (niquests/{niquests_version})",
|
|
578
|
+
}
|
|
579
|
+
session.hooks.update(hooks)
|
|
580
|
+
|
|
581
|
+
return session
|
|
548
582
|
|
|
549
583
|
async def _add_auth(self, request: Request):
|
|
550
584
|
request.headers.update(
|
|
551
585
|
{"AUTHORIZATION-APP-API": b64encode(f"{self._user}:{self.cfg.app_secret}".encode("UTF=8"))}
|
|
552
586
|
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def patch_param(url: str, key: str, value: str) -> str:
|
|
590
|
+
parts = urlsplit(url)
|
|
591
|
+
query = dict(parse_qsl(parts.query, keep_blank_values=True))
|
|
592
|
+
query[key] = value
|
|
593
|
+
|
|
594
|
+
new_query = urlencode(query, doseq=True)
|
|
595
|
+
|
|
596
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
|
|
@@ -23,7 +23,7 @@ try:
|
|
|
23
23
|
if body:
|
|
24
24
|
body = body.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
|
|
25
25
|
r = self._session.adapter_dav.request(
|
|
26
|
-
method, url if isinstance(url, str) else str(url),
|
|
26
|
+
method, url if isinstance(url, str) else str(url), data=body, headers=headers
|
|
27
27
|
)
|
|
28
28
|
return DAVResponse(r)
|
|
29
29
|
|
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import typing
|
|
10
10
|
from urllib.parse import urlparse
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import niquests
|
|
13
13
|
from fastapi import (
|
|
14
14
|
BackgroundTasks,
|
|
15
15
|
Depends,
|
|
@@ -143,7 +143,8 @@ def __fetch_model_as_file(
|
|
|
143
143
|
) -> str | None:
|
|
144
144
|
result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
|
|
145
145
|
try:
|
|
146
|
-
|
|
146
|
+
|
|
147
|
+
with niquests.get("GET", model_path, stream=True) as response:
|
|
147
148
|
if not response.is_success:
|
|
148
149
|
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
|
|
149
150
|
return None
|
|
@@ -171,7 +172,7 @@ def __fetch_model_as_file(
|
|
|
171
172
|
|
|
172
173
|
with builtins.open(result_path, "wb") as file:
|
|
173
174
|
last_progress = current_progress
|
|
174
|
-
for chunk in response.
|
|
175
|
+
for chunk in response.iter_raw(-1):
|
|
175
176
|
downloaded_size += file.write(chunk)
|
|
176
177
|
if total_size:
|
|
177
178
|
new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
|
|
@@ -48,6 +48,7 @@ class SettingsField:
|
|
|
48
48
|
description: str = ""
|
|
49
49
|
placeholder: str = ""
|
|
50
50
|
label: str = ""
|
|
51
|
+
sensitive: bool = False
|
|
51
52
|
notify = False # to be supported in future
|
|
52
53
|
|
|
53
54
|
@classmethod
|
|
@@ -74,6 +75,7 @@ class SettingsField:
|
|
|
74
75
|
"placeholder": self.placeholder,
|
|
75
76
|
"label": self.label,
|
|
76
77
|
"notify": self.notify,
|
|
78
|
+
"sensitive": self.sensitive,
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
|
|
@@ -9,7 +9,7 @@ from urllib.parse import unquote
|
|
|
9
9
|
from xml.etree import ElementTree
|
|
10
10
|
|
|
11
11
|
import xmltodict
|
|
12
|
-
from
|
|
12
|
+
from niquests import Response
|
|
13
13
|
|
|
14
14
|
from .._exceptions import NextcloudException, check_error
|
|
15
15
|
from .._misc import check_capabilities, clear_from_params_empty
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from urllib.parse import quote
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from niquests.structures import CaseInsensitiveDict
|
|
9
9
|
|
|
10
10
|
from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
|
|
11
11
|
from .._misc import random_string, require_capabilities
|
|
@@ -80,7 +80,7 @@ class FilesAPI:
|
|
|
80
80
|
# `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
|
|
81
81
|
root = build_find_request(req, path, self._session.user, self._session.capabilities)
|
|
82
82
|
webdav_response = self._session.adapter_dav.request(
|
|
83
|
-
"SEARCH", "",
|
|
83
|
+
"SEARCH", "", data=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
|
|
84
84
|
)
|
|
85
85
|
request_info = f"find: {self._session.user}, {req}, {path}"
|
|
86
86
|
return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
|
|
@@ -133,7 +133,7 @@ class FilesAPI:
|
|
|
133
133
|
"""
|
|
134
134
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
135
135
|
full_path = dav_get_obj_path(self._session.user, path)
|
|
136
|
-
response = self._session.adapter_dav.put(quote(full_path),
|
|
136
|
+
response = self._session.adapter_dav.put(quote(full_path), data=content)
|
|
137
137
|
check_error(response, f"upload: user={self._session.user}, path={path}, size={len(content)}")
|
|
138
138
|
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
139
139
|
|
|
@@ -215,7 +215,7 @@ class FilesAPI:
|
|
|
215
215
|
self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
216
216
|
)
|
|
217
217
|
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
218
|
-
headers =
|
|
218
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
|
|
219
219
|
response = self._session.adapter_dav.request(
|
|
220
220
|
"MOVE",
|
|
221
221
|
quote(dav_get_obj_path(self._session.user, path_src)),
|
|
@@ -237,7 +237,7 @@ class FilesAPI:
|
|
|
237
237
|
self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
238
238
|
)
|
|
239
239
|
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
240
|
-
headers =
|
|
240
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
|
|
241
241
|
response = self._session.adapter_dav.request(
|
|
242
242
|
"COPY",
|
|
243
243
|
quote(dav_get_obj_path(self._session.user, path_src)),
|
|
@@ -257,7 +257,7 @@ class FilesAPI:
|
|
|
257
257
|
"""
|
|
258
258
|
root = build_list_by_criteria_req(properties, tags, self._session.capabilities)
|
|
259
259
|
webdav_response = self._session.adapter_dav.request(
|
|
260
|
-
"REPORT", dav_get_obj_path(self._session.user),
|
|
260
|
+
"REPORT", dav_get_obj_path(self._session.user), data=element_tree_as_str(root)
|
|
261
261
|
)
|
|
262
262
|
request_info = f"list_files_by_criteria: {self._session.user}"
|
|
263
263
|
check_error(webdav_response, request_info)
|
|
@@ -272,7 +272,7 @@ class FilesAPI:
|
|
|
272
272
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
273
273
|
root = build_setfav_req(value)
|
|
274
274
|
webdav_response = self._session.adapter_dav.request(
|
|
275
|
-
"PROPPATCH", quote(dav_get_obj_path(self._session.user, path)),
|
|
275
|
+
"PROPPATCH", quote(dav_get_obj_path(self._session.user, path)), data=element_tree_as_str(root)
|
|
276
276
|
)
|
|
277
277
|
check_error(webdav_response, f"setfav: path={path}, value={value}")
|
|
278
278
|
|
|
@@ -293,7 +293,7 @@ class FilesAPI:
|
|
|
293
293
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
294
294
|
|
|
295
295
|
dest = self._session.cfg.dav_endpoint + f"/trashbin/{self._session.user}/restore/{restore_name}"
|
|
296
|
-
headers =
|
|
296
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
|
|
297
297
|
response = self._session.adapter_dav.request(
|
|
298
298
|
"MOVE",
|
|
299
299
|
quote(f"/trashbin/{self._session.user}/{path}"),
|
|
@@ -336,7 +336,7 @@ class FilesAPI:
|
|
|
336
336
|
"""
|
|
337
337
|
require_capabilities("files.versioning", self._session.capabilities)
|
|
338
338
|
dest = self._session.cfg.dav_endpoint + f"/versions/{self._session.user}/restore/{file_object.name}"
|
|
339
|
-
headers =
|
|
339
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
|
|
340
340
|
response = self._session.adapter_dav.request(
|
|
341
341
|
"MOVE",
|
|
342
342
|
quote(f"/versions/{self._session.user}/{file_object.user_path}"),
|
|
@@ -347,7 +347,7 @@ class FilesAPI:
|
|
|
347
347
|
def list_tags(self) -> list[SystemTag]:
|
|
348
348
|
"""Returns list of the avalaible Tags."""
|
|
349
349
|
root = build_list_tag_req()
|
|
350
|
-
response = self._session.adapter_dav.request("PROPFIND", "/systemtags",
|
|
350
|
+
response = self._session.adapter_dav.request("PROPFIND", "/systemtags", data=element_tree_as_str(root))
|
|
351
351
|
return build_list_tags_response(response)
|
|
352
352
|
|
|
353
353
|
def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
|
|
@@ -389,7 +389,7 @@ class FilesAPI:
|
|
|
389
389
|
tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
|
|
390
390
|
root = build_update_tag_req(name, user_visible, user_assignable)
|
|
391
391
|
response = self._session.adapter_dav.request(
|
|
392
|
-
"PROPPATCH", f"/systemtags/{tag_id}",
|
|
392
|
+
"PROPPATCH", f"/systemtags/{tag_id}", data=element_tree_as_str(root)
|
|
393
393
|
)
|
|
394
394
|
check_error(response)
|
|
395
395
|
|
|
@@ -466,7 +466,7 @@ class FilesAPI:
|
|
|
466
466
|
webdav_response = self._session.adapter_dav.request(
|
|
467
467
|
"PROPFIND",
|
|
468
468
|
quote(dav_path),
|
|
469
|
-
|
|
469
|
+
data=element_tree_as_str(root),
|
|
470
470
|
headers={"Depth": "infinity" if depth == -1 else str(depth)},
|
|
471
471
|
)
|
|
472
472
|
return build_listdir_response(
|
|
@@ -478,7 +478,9 @@ class FilesAPI:
|
|
|
478
478
|
_dav_path = quote(dav_get_obj_path(self._session.user, _tmp_path, root_path="/uploads"))
|
|
479
479
|
_v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
|
|
480
480
|
full_path = dav_get_obj_path(self._session.user, path)
|
|
481
|
-
headers =
|
|
481
|
+
headers = CaseInsensitiveDict(
|
|
482
|
+
{"Destination": (self._session.cfg.dav_endpoint + quote(full_path)).encode("utf-8")}
|
|
483
|
+
)
|
|
482
484
|
if _v2:
|
|
483
485
|
response = self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
|
|
484
486
|
else:
|
|
@@ -494,11 +496,11 @@ class FilesAPI:
|
|
|
494
496
|
end_bytes = start_bytes + len(piece)
|
|
495
497
|
if _v2:
|
|
496
498
|
response = self._session.adapter_dav.put(
|
|
497
|
-
_dav_path + "/" + str(chunk_number),
|
|
499
|
+
_dav_path + "/" + str(chunk_number), data=piece, headers=headers
|
|
498
500
|
)
|
|
499
501
|
else:
|
|
500
502
|
_filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
|
|
501
|
-
response = self._session.adapter_dav.put(_dav_path + "/" + _filename,
|
|
503
|
+
response = self._session.adapter_dav.put(_dav_path + "/" + _filename, data=piece)
|
|
502
504
|
check_error(
|
|
503
505
|
response,
|
|
504
506
|
f"upload_stream(v={_v2}): user={self._session.user}, path={path}, cur_size={end_bytes}",
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from urllib.parse import quote
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from niquests.structures import CaseInsensitiveDict
|
|
9
9
|
|
|
10
10
|
from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
|
|
11
11
|
from .._misc import random_string, require_capabilities
|
|
@@ -82,7 +82,7 @@ class AsyncFilesAPI:
|
|
|
82
82
|
# `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
|
|
83
83
|
root = build_find_request(req, path, await self._session.user, await self._session.capabilities)
|
|
84
84
|
webdav_response = await self._session.adapter_dav.request(
|
|
85
|
-
"SEARCH", "",
|
|
85
|
+
"SEARCH", "", data=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
|
|
86
86
|
)
|
|
87
87
|
request_info = f"find: {await self._session.user}, {req}, {path}"
|
|
88
88
|
return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
|
|
@@ -137,7 +137,7 @@ class AsyncFilesAPI:
|
|
|
137
137
|
"""
|
|
138
138
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
139
139
|
full_path = dav_get_obj_path(await self._session.user, path)
|
|
140
|
-
response = await self._session.adapter_dav.put(quote(full_path),
|
|
140
|
+
response = await self._session.adapter_dav.put(quote(full_path), data=content)
|
|
141
141
|
check_error(response, f"upload: user={await self._session.user}, path={path}, size={len(content)}")
|
|
142
142
|
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
143
143
|
|
|
@@ -219,7 +219,7 @@ class AsyncFilesAPI:
|
|
|
219
219
|
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
220
220
|
)
|
|
221
221
|
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
222
|
-
headers =
|
|
222
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
|
|
223
223
|
response = await self._session.adapter_dav.request(
|
|
224
224
|
"MOVE",
|
|
225
225
|
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
@@ -241,7 +241,7 @@ class AsyncFilesAPI:
|
|
|
241
241
|
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
242
242
|
)
|
|
243
243
|
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
244
|
-
headers =
|
|
244
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
|
|
245
245
|
response = await self._session.adapter_dav.request(
|
|
246
246
|
"COPY",
|
|
247
247
|
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
@@ -261,7 +261,7 @@ class AsyncFilesAPI:
|
|
|
261
261
|
"""
|
|
262
262
|
root = build_list_by_criteria_req(properties, tags, await self._session.capabilities)
|
|
263
263
|
webdav_response = await self._session.adapter_dav.request(
|
|
264
|
-
"REPORT", dav_get_obj_path(await self._session.user),
|
|
264
|
+
"REPORT", dav_get_obj_path(await self._session.user), data=element_tree_as_str(root)
|
|
265
265
|
)
|
|
266
266
|
request_info = f"list_files_by_criteria: {await self._session.user}"
|
|
267
267
|
check_error(webdav_response, request_info)
|
|
@@ -276,7 +276,7 @@ class AsyncFilesAPI:
|
|
|
276
276
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
277
277
|
root = build_setfav_req(value)
|
|
278
278
|
webdav_response = await self._session.adapter_dav.request(
|
|
279
|
-
"PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)),
|
|
279
|
+
"PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), data=element_tree_as_str(root)
|
|
280
280
|
)
|
|
281
281
|
check_error(webdav_response, f"setfav: path={path}, value={value}")
|
|
282
282
|
|
|
@@ -302,7 +302,7 @@ class AsyncFilesAPI:
|
|
|
302
302
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
303
303
|
|
|
304
304
|
dest = self._session.cfg.dav_endpoint + f"/trashbin/{await self._session.user}/restore/{restore_name}"
|
|
305
|
-
headers =
|
|
305
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
|
|
306
306
|
response = await self._session.adapter_dav.request(
|
|
307
307
|
"MOVE",
|
|
308
308
|
quote(f"/trashbin/{await self._session.user}/{path}"),
|
|
@@ -345,7 +345,7 @@ class AsyncFilesAPI:
|
|
|
345
345
|
"""
|
|
346
346
|
require_capabilities("files.versioning", await self._session.capabilities)
|
|
347
347
|
dest = self._session.cfg.dav_endpoint + f"/versions/{await self._session.user}/restore/{file_object.name}"
|
|
348
|
-
headers =
|
|
348
|
+
headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
|
|
349
349
|
response = await self._session.adapter_dav.request(
|
|
350
350
|
"MOVE",
|
|
351
351
|
quote(f"/versions/{await self._session.user}/{file_object.user_path}"),
|
|
@@ -356,7 +356,7 @@ class AsyncFilesAPI:
|
|
|
356
356
|
async def list_tags(self) -> list[SystemTag]:
|
|
357
357
|
"""Returns list of the avalaible Tags."""
|
|
358
358
|
root = build_list_tag_req()
|
|
359
|
-
response = await self._session.adapter_dav.request("PROPFIND", "/systemtags",
|
|
359
|
+
response = await self._session.adapter_dav.request("PROPFIND", "/systemtags", data=element_tree_as_str(root))
|
|
360
360
|
return build_list_tags_response(response)
|
|
361
361
|
|
|
362
362
|
async def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
|
|
@@ -398,7 +398,7 @@ class AsyncFilesAPI:
|
|
|
398
398
|
tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
|
|
399
399
|
root = build_update_tag_req(name, user_visible, user_assignable)
|
|
400
400
|
response = await self._session.adapter_dav.request(
|
|
401
|
-
"PROPPATCH", f"/systemtags/{tag_id}",
|
|
401
|
+
"PROPPATCH", f"/systemtags/{tag_id}", data=element_tree_as_str(root)
|
|
402
402
|
)
|
|
403
403
|
check_error(response)
|
|
404
404
|
|
|
@@ -435,7 +435,7 @@ class AsyncFilesAPI:
|
|
|
435
435
|
quote(full_path),
|
|
436
436
|
headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
|
|
437
437
|
)
|
|
438
|
-
check_error(response, f"lock: user={self._session.user}, path={full_path}")
|
|
438
|
+
check_error(response, f"lock: user={await self._session.user}, path={full_path}")
|
|
439
439
|
|
|
440
440
|
async def unlock(self, path: FsNode | str) -> None:
|
|
441
441
|
"""Unlocks the file.
|
|
@@ -449,7 +449,7 @@ class AsyncFilesAPI:
|
|
|
449
449
|
quote(full_path),
|
|
450
450
|
headers={"X-User-Lock": "1"},
|
|
451
451
|
)
|
|
452
|
-
check_error(response, f"unlock: user={self._session.user}, path={full_path}")
|
|
452
|
+
check_error(response, f"unlock: user={await self._session.user}, path={full_path}")
|
|
453
453
|
|
|
454
454
|
async def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
|
|
455
455
|
fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
|
|
@@ -475,7 +475,7 @@ class AsyncFilesAPI:
|
|
|
475
475
|
webdav_response = await self._session.adapter_dav.request(
|
|
476
476
|
"PROPFIND",
|
|
477
477
|
quote(dav_path),
|
|
478
|
-
|
|
478
|
+
data=element_tree_as_str(root),
|
|
479
479
|
headers={"Depth": "infinity" if depth == -1 else str(depth)},
|
|
480
480
|
)
|
|
481
481
|
return build_listdir_response(
|
|
@@ -487,7 +487,9 @@ class AsyncFilesAPI:
|
|
|
487
487
|
_dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
|
|
488
488
|
_v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
|
|
489
489
|
full_path = dav_get_obj_path(await self._session.user, path)
|
|
490
|
-
headers =
|
|
490
|
+
headers = CaseInsensitiveDict(
|
|
491
|
+
{"Destination": (self._session.cfg.dav_endpoint + quote(full_path)).encode("utf-8")}
|
|
492
|
+
)
|
|
491
493
|
if _v2:
|
|
492
494
|
response = await self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
|
|
493
495
|
else:
|
|
@@ -502,11 +504,11 @@ class AsyncFilesAPI:
|
|
|
502
504
|
end_bytes = start_bytes + len(piece)
|
|
503
505
|
if _v2:
|
|
504
506
|
response = await self._session.adapter_dav.put(
|
|
505
|
-
_dav_path + "/" + str(chunk_number),
|
|
507
|
+
_dav_path + "/" + str(chunk_number), data=piece, headers=headers
|
|
506
508
|
)
|
|
507
509
|
else:
|
|
508
510
|
_filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
|
|
509
|
-
response = await self._session.adapter_dav.put(_dav_path + "/" + _filename,
|
|
511
|
+
response = await self._session.adapter_dav.put(_dav_path + "/" + _filename, data=piece)
|
|
510
512
|
check_error(
|
|
511
513
|
response,
|
|
512
514
|
f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, cur_size={end_bytes}",
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import time
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import niquests
|
|
9
9
|
|
|
10
10
|
from ._exceptions import check_error
|
|
11
11
|
from ._session import AsyncNcSession, NcSession
|
|
@@ -156,6 +156,6 @@ class _AsyncLoginFlowV2API:
|
|
|
156
156
|
return r_model
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def _res_to_json(response:
|
|
159
|
+
def _res_to_json(response: niquests.Response) -> dict:
|
|
160
160
|
check_error(response)
|
|
161
161
|
return json.loads(response.text)
|
|
@@ -4,7 +4,7 @@ import contextlib
|
|
|
4
4
|
import typing
|
|
5
5
|
from abc import ABC
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from niquests.structures import CaseInsensitiveDict
|
|
8
8
|
|
|
9
9
|
from ._exceptions import NextcloudExceptionNotFound
|
|
10
10
|
from ._misc import check_capabilities, require_capabilities
|
|
@@ -112,8 +112,8 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
112
112
|
self._session.update_server_info()
|
|
113
113
|
|
|
114
114
|
@property
|
|
115
|
-
def response_headers(self) ->
|
|
116
|
-
"""Returns the `
|
|
115
|
+
def response_headers(self) -> CaseInsensitiveDict:
|
|
116
|
+
"""Returns the `Niquests headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
|
|
117
117
|
return self._session.response_headers
|
|
118
118
|
|
|
119
119
|
@property
|
|
@@ -216,8 +216,8 @@ class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
216
216
|
await self._session.update_server_info()
|
|
217
217
|
|
|
218
218
|
@property
|
|
219
|
-
def response_headers(self) ->
|
|
220
|
-
"""Returns the `
|
|
219
|
+
def response_headers(self) -> CaseInsensitiveDict:
|
|
220
|
+
"""Returns the `Niquests headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
|
|
221
221
|
return self._session.response_headers
|
|
222
222
|
|
|
223
223
|
@property
|
|
@@ -5,7 +5,7 @@ import datetime
|
|
|
5
5
|
import json
|
|
6
6
|
import typing
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import niquests
|
|
9
9
|
|
|
10
10
|
from ._exceptions import check_error
|
|
11
11
|
from ._misc import check_capabilities, clear_from_params_empty, require_capabilities
|
|
@@ -367,6 +367,6 @@ class _AsyncNotesAPI:
|
|
|
367
367
|
check_error(await self._session.adapter.put(self._ep_base + "/settings", json=params))
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def _res_to_json(response:
|
|
370
|
+
def _res_to_json(response: niquests.Response) -> dict:
|
|
371
371
|
check_error(response)
|
|
372
372
|
return json.loads(response.text) if response.status_code != 304 else {}
|
|
@@ -33,22 +33,13 @@ NPA_NC_CERT: bool | str
|
|
|
33
33
|
SSL certificates (a.k.a CA bundle) used to verify the identity of requested hosts. Either **True** (default CA bundle),
|
|
34
34
|
a path to an SSL certificate file, or **False** (which will disable verification)."""
|
|
35
35
|
str_val = environ.get("NPA_NC_CERT", "True")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
|
|
37
|
+
NPA_NC_CERT = True
|
|
38
|
+
|
|
39
39
|
if str_val.lower() in ("false", "0"):
|
|
40
40
|
NPA_NC_CERT = False
|
|
41
41
|
elif str_val.lower() not in ("true", "1"):
|
|
42
42
|
NPA_NC_CERT = str_val
|
|
43
|
-
else:
|
|
44
|
-
# Temporary workaround, see comment above.
|
|
45
|
-
# Use system certificate stores
|
|
46
|
-
|
|
47
|
-
import ssl
|
|
48
|
-
|
|
49
|
-
import truststore
|
|
50
|
-
|
|
51
|
-
NPA_NC_CERT = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
52
43
|
|
|
53
44
|
CHUNKED_UPLOAD_V2 = environ.get("CHUNKED_UPLOAD_V2", True)
|
|
54
45
|
"""Option to enable/disable **version 2** chunked upload(better Object Storages support).
|
|
@@ -7,7 +7,7 @@ import json
|
|
|
7
7
|
import os
|
|
8
8
|
import typing
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import niquests
|
|
11
11
|
|
|
12
12
|
from . import options
|
|
13
13
|
from ._misc import random_string
|
|
@@ -117,7 +117,7 @@ class TalkBot:
|
|
|
117
117
|
|
|
118
118
|
def send_message(
|
|
119
119
|
self, message: str, reply_to_message: int | TalkBotMessage, silent: bool = False, token: str = ""
|
|
120
|
-
) -> tuple[
|
|
120
|
+
) -> tuple[niquests.Response, str]:
|
|
121
121
|
"""Send a message and returns a "reference string" to identify the message again in a "get messages" request.
|
|
122
122
|
|
|
123
123
|
:param message: The message to say.
|
|
@@ -127,7 +127,7 @@ class TalkBot:
|
|
|
127
127
|
:param silent: Flag controlling if the message should create a chat notifications for the users.
|
|
128
128
|
:param token: Token of the conversation.
|
|
129
129
|
Can be empty if ``reply_to_message`` is :py:class:`~nc_py_api.talk_bot.TalkBotMessage`.
|
|
130
|
-
:returns: Tuple, where fist element is :py:class:`
|
|
130
|
+
:returns: Tuple, where fist element is :py:class:`niquests.Response` and second is a "reference string".
|
|
131
131
|
:raises ValueError: in case of an invalid usage.
|
|
132
132
|
:raises RuntimeError: in case of a broken installation.
|
|
133
133
|
"""
|
|
@@ -143,7 +143,7 @@ class TalkBot:
|
|
|
143
143
|
}
|
|
144
144
|
return self._sign_send_request("POST", f"/{token}/message", params, message), reference_id
|
|
145
145
|
|
|
146
|
-
def react_to_message(self, message: int | TalkBotMessage, reaction: str, token: str = "") ->
|
|
146
|
+
def react_to_message(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
|
|
147
147
|
"""React to a message.
|
|
148
148
|
|
|
149
149
|
:param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to react to.
|
|
@@ -162,7 +162,7 @@ class TalkBot:
|
|
|
162
162
|
}
|
|
163
163
|
return self._sign_send_request("POST", f"/{token}/reaction/{message_id}", params, reaction)
|
|
164
164
|
|
|
165
|
-
def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") ->
|
|
165
|
+
def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
|
|
166
166
|
"""Removes reaction from a message.
|
|
167
167
|
|
|
168
168
|
:param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to remove reaction from.
|
|
@@ -181,7 +181,7 @@ class TalkBot:
|
|
|
181
181
|
}
|
|
182
182
|
return self._sign_send_request("DELETE", f"/{token}/reaction/{message_id}", params, reaction)
|
|
183
183
|
|
|
184
|
-
def _sign_send_request(self, method: str, url_suffix: str, data: dict, data_to_sign: str) ->
|
|
184
|
+
def _sign_send_request(self, method: str, url_suffix: str, data: dict, data_to_sign: str) -> niquests.Response:
|
|
185
185
|
secret = get_bot_secret(self.callback_url)
|
|
186
186
|
if secret is None:
|
|
187
187
|
raise RuntimeError("Can't find the 'secret' of the bot. Has the bot been installed?")
|
|
@@ -189,7 +189,8 @@ class TalkBot:
|
|
|
189
189
|
hmac_sign = hmac.new(secret, talk_bot_random.encode("UTF-8"), digestmod=hashlib.sha256)
|
|
190
190
|
hmac_sign.update(data_to_sign.encode("UTF-8"))
|
|
191
191
|
nc_app_cfg = BasicConfig()
|
|
192
|
-
with
|
|
192
|
+
with niquests.Session() as client:
|
|
193
|
+
client.verify = nc_app_cfg.options.nc_cert
|
|
193
194
|
return client.request(
|
|
194
195
|
method,
|
|
195
196
|
url=nc_app_cfg.endpoint + "/ocs/v2.php/apps/spreed/api/v1/bot" + url_suffix,
|
|
@@ -234,7 +235,7 @@ class AsyncTalkBot:
|
|
|
234
235
|
|
|
235
236
|
async def send_message(
|
|
236
237
|
self, message: str, reply_to_message: int | TalkBotMessage, silent: bool = False, token: str = ""
|
|
237
|
-
) -> tuple[
|
|
238
|
+
) -> tuple[niquests.Response, str]:
|
|
238
239
|
"""Send a message and returns a "reference string" to identify the message again in a "get messages" request.
|
|
239
240
|
|
|
240
241
|
:param message: The message to say.
|
|
@@ -244,7 +245,7 @@ class AsyncTalkBot:
|
|
|
244
245
|
:param silent: Flag controlling if the message should create a chat notifications for the users.
|
|
245
246
|
:param token: Token of the conversation.
|
|
246
247
|
Can be empty if ``reply_to_message`` is :py:class:`~nc_py_api.talk_bot.TalkBotMessage`.
|
|
247
|
-
:returns: Tuple, where fist element is :py:class:`
|
|
248
|
+
:returns: Tuple, where fist element is :py:class:`niquests.Response` and second is a "reference string".
|
|
248
249
|
:raises ValueError: in case of an invalid usage.
|
|
249
250
|
:raises RuntimeError: in case of a broken installation.
|
|
250
251
|
"""
|
|
@@ -260,7 +261,9 @@ class AsyncTalkBot:
|
|
|
260
261
|
}
|
|
261
262
|
return await self._sign_send_request("POST", f"/{token}/message", params, message), reference_id
|
|
262
263
|
|
|
263
|
-
async def react_to_message(
|
|
264
|
+
async def react_to_message(
|
|
265
|
+
self, message: int | TalkBotMessage, reaction: str, token: str = ""
|
|
266
|
+
) -> niquests.Response:
|
|
264
267
|
"""React to a message.
|
|
265
268
|
|
|
266
269
|
:param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to react to.
|
|
@@ -279,7 +282,7 @@ class AsyncTalkBot:
|
|
|
279
282
|
}
|
|
280
283
|
return await self._sign_send_request("POST", f"/{token}/reaction/{message_id}", params, reaction)
|
|
281
284
|
|
|
282
|
-
async def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") ->
|
|
285
|
+
async def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
|
|
283
286
|
"""Removes reaction from a message.
|
|
284
287
|
|
|
285
288
|
:param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to remove reaction from.
|
|
@@ -298,7 +301,9 @@ class AsyncTalkBot:
|
|
|
298
301
|
}
|
|
299
302
|
return await self._sign_send_request("DELETE", f"/{token}/reaction/{message_id}", params, reaction)
|
|
300
303
|
|
|
301
|
-
async def _sign_send_request(
|
|
304
|
+
async def _sign_send_request(
|
|
305
|
+
self, method: str, url_suffix: str, data: dict, data_to_sign: str
|
|
306
|
+
) -> niquests.Response:
|
|
302
307
|
secret = await aget_bot_secret(self.callback_url)
|
|
303
308
|
if secret is None:
|
|
304
309
|
raise RuntimeError("Can't find the 'secret' of the bot. Has the bot been installed?")
|
|
@@ -306,7 +311,9 @@ class AsyncTalkBot:
|
|
|
306
311
|
hmac_sign = hmac.new(secret, talk_bot_random.encode("UTF-8"), digestmod=hashlib.sha256)
|
|
307
312
|
hmac_sign.update(data_to_sign.encode("UTF-8"))
|
|
308
313
|
nc_app_cfg = BasicConfig()
|
|
309
|
-
async with
|
|
314
|
+
async with niquests.AsyncSession() as aclient:
|
|
315
|
+
aclient.verify = nc_app_cfg.options.nc_cert
|
|
316
|
+
|
|
310
317
|
return await aclient.request(
|
|
311
318
|
method,
|
|
312
319
|
url=nc_app_cfg.endpoint + "/ocs/v2.php/apps/spreed/api/v1/bot" + url_suffix,
|
|
@@ -207,6 +207,10 @@ class _UsersAPI:
|
|
|
207
207
|
"""Disables user on the Nextcloud server."""
|
|
208
208
|
self._session.ocs("PUT", f"{self._ep_base}/{user_id}/disable")
|
|
209
209
|
|
|
210
|
+
def wipe(self, user_id: str) -> None:
|
|
211
|
+
"""Disconnects user from the Nextcloud server and deletes all local data stored on the user's devices."""
|
|
212
|
+
self._session.ocs("POST", f"/ocs/v2.php/cloud/users/{user_id}/wipe")
|
|
213
|
+
|
|
210
214
|
def resend_welcome_email(self, user_id: str) -> None:
|
|
211
215
|
"""Send welcome email for specified user again."""
|
|
212
216
|
self._session.ocs("POST", f"{self._ep_base}/{user_id}/welcome")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|