nc-py-api 0.7.1__tar.gz → 0.8.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.7.1 → nc_py_api-0.8.0}/CHANGELOG.md +22 -2
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/PKG-INFO +16 -16
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/README.md +15 -15
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_session.py +38 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_version.py +1 -1
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/__init__.py +1 -1
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/defs.py +5 -1
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/misc.py +7 -0
- nc_py_api-0.8.0/nc_py_api/ex_app/providers/__init__.py +1 -0
- nc_py_api-0.8.0/nc_py_api/ex_app/providers/providers.py +31 -0
- nc_py_api-0.8.0/nc_py_api/ex_app/providers/speech_to_text.py +130 -0
- nc_py_api-0.8.0/nc_py_api/ex_app/providers/text_processing.py +137 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/ui/files_actions.py +1 -1
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/files/files.py +45 -80
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/nextcloud.py +31 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/pyproject.toml +1 -1
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/.gitignore +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/AUTHORS +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/LICENSE.txt +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/__init__.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_deffered_error.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_exceptions.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_misc.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_preferences.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_preferences_ex.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_talk_api.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/_theming.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/activity.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/apps.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/calendar.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/integration_fastapi.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/persist_transformers_cache.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/ui/__init__.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/ui/resources.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/ui/top_menu.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/ui/ui.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/ex_app/uvicorn_fastapi.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/files/__init__.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/files/_files.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/files/sharing.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/notes.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/notifications.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/options.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/talk.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/talk_bot.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/user_status.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/users.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/users_groups.py +0 -0
- {nc_py_api-0.7.1 → nc_py_api-0.8.0}/nc_py_api/weather_status.py +0 -0
|
@@ -2,13 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.8.0 - 2024-01-12]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `download_log` method to download `nextcloud.log`. #199
|
|
10
|
+
- NextcloudApp: API for registering `Speech to Text` providers(*avalaible from Nextcloud 29*). #196
|
|
11
|
+
- NextcloudApp: API for registering `Text Processing` providers(*avalaible from Nextcloud 29*). #198
|
|
12
|
+
- NextcloudApp: added `get_model_path` wrapper around huggingface_hub:snapshot_download. #202
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- OCS: Correctly handling of `HTTP 204 No Content` status. #197
|
|
17
|
+
|
|
18
|
+
## [0.7.2 - 2023-12-28]
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- files: proper url encoding of special chars in `mkdir` and `delete` methods. #191 Thanks to @tobenary
|
|
23
|
+
- files: proper url encoding of special chars in all other `DAV` methods. #194
|
|
24
|
+
|
|
25
|
+
## [0.7.1 - 2023-12-21]
|
|
6
26
|
|
|
7
27
|
### Added
|
|
8
28
|
|
|
9
29
|
- The `ocs` method is now public, making it easy to use Nextcloud OCS that has not yet been described. #187
|
|
10
30
|
|
|
11
|
-
## [0.7.0 -
|
|
31
|
+
## [0.7.0 - 2023-12-17]
|
|
12
32
|
|
|
13
33
|
### Added
|
|
14
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nc-py-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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/
|
|
@@ -89,21 +89,21 @@ Python library that provides a robust and well-documented API that allows develo
|
|
|
89
89
|
* **Sync + Async**: Provides both sync and async APIs.
|
|
90
90
|
|
|
91
91
|
### Capabilities
|
|
92
|
-
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 |
|
|
93
|
-
|
|
94
|
-
| Calendar | ✅ | ✅ | ✅ |
|
|
95
|
-
| File System & Tags | ✅ | ✅ | ✅ |
|
|
96
|
-
| Nextcloud Talk | ✅ | ✅ | ✅ |
|
|
97
|
-
| Notifications | ✅ | ✅ | ✅ |
|
|
98
|
-
| Shares | ✅ | ✅ | ✅ |
|
|
99
|
-
| Users & Groups | ✅ | ✅ | ✅ |
|
|
100
|
-
| User & Weather status | ✅ | ✅ | ✅ |
|
|
101
|
-
| Other APIs*** | ✅ | ✅ | ✅ |
|
|
102
|
-
| Talk Bot API* | N/A | ✅ | ✅ |
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*_available only for
|
|
92
|
+
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 | Nextcloud 29 |
|
|
93
|
+
|-----------------------|:------------:|:------------:|:------------:|:------------:|
|
|
94
|
+
| Calendar | ✅ | ✅ | ✅ | ✅ |
|
|
95
|
+
| File System & Tags | ✅ | ✅ | ✅ | ✅ |
|
|
96
|
+
| Nextcloud Talk | ✅ | ✅ | ✅ | ✅ |
|
|
97
|
+
| Notifications | ✅ | ✅ | ✅ | ✅ |
|
|
98
|
+
| Shares | ✅ | ✅ | ✅ | ✅ |
|
|
99
|
+
| Users & Groups | ✅ | ✅ | ✅ | ✅ |
|
|
100
|
+
| User & Weather status | ✅ | ✅ | ✅ | ✅ |
|
|
101
|
+
| Other APIs*** | ✅ | ✅ | ✅ | ✅ |
|
|
102
|
+
| Talk Bot API* | N/A | ✅ | ✅ | ✅ |
|
|
103
|
+
| AI Providers API** | N/A | N/A | N/A | ✅ |
|
|
104
|
+
|
|
105
|
+
*_available only for **NextcloudApp**_<br>
|
|
106
|
+
**_available only for **NextcloudApp**: SpeechToText, TextProcessing_<br>
|
|
107
107
|
***_Activity, Notes_
|
|
108
108
|
|
|
109
109
|
### Differences between the Nextcloud and NextcloudApp classes
|
|
@@ -24,21 +24,21 @@ Python library that provides a robust and well-documented API that allows develo
|
|
|
24
24
|
* **Sync + Async**: Provides both sync and async APIs.
|
|
25
25
|
|
|
26
26
|
### Capabilities
|
|
27
|
-
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 |
|
|
28
|
-
|
|
29
|
-
| Calendar | ✅ | ✅ | ✅ |
|
|
30
|
-
| File System & Tags | ✅ | ✅ | ✅ |
|
|
31
|
-
| Nextcloud Talk | ✅ | ✅ | ✅ |
|
|
32
|
-
| Notifications | ✅ | ✅ | ✅ |
|
|
33
|
-
| Shares | ✅ | ✅ | ✅ |
|
|
34
|
-
| Users & Groups | ✅ | ✅ | ✅ |
|
|
35
|
-
| User & Weather status | ✅ | ✅ | ✅ |
|
|
36
|
-
| Other APIs*** | ✅ | ✅ | ✅ |
|
|
37
|
-
| Talk Bot API* | N/A | ✅ | ✅ |
|
|
38
|
-
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*_available only for
|
|
27
|
+
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 | Nextcloud 29 |
|
|
28
|
+
|-----------------------|:------------:|:------------:|:------------:|:------------:|
|
|
29
|
+
| Calendar | ✅ | ✅ | ✅ | ✅ |
|
|
30
|
+
| File System & Tags | ✅ | ✅ | ✅ | ✅ |
|
|
31
|
+
| Nextcloud Talk | ✅ | ✅ | ✅ | ✅ |
|
|
32
|
+
| Notifications | ✅ | ✅ | ✅ | ✅ |
|
|
33
|
+
| Shares | ✅ | ✅ | ✅ | ✅ |
|
|
34
|
+
| Users & Groups | ✅ | ✅ | ✅ | ✅ |
|
|
35
|
+
| User & Weather status | ✅ | ✅ | ✅ | ✅ |
|
|
36
|
+
| Other APIs*** | ✅ | ✅ | ✅ | ✅ |
|
|
37
|
+
| Talk Bot API* | N/A | ✅ | ✅ | ✅ |
|
|
38
|
+
| AI Providers API** | N/A | N/A | N/A | ✅ |
|
|
39
|
+
|
|
40
|
+
*_available only for **NextcloudApp**_<br>
|
|
41
|
+
**_available only for **NextcloudApp**: SpeechToText, TextProcessing_<br>
|
|
42
42
|
***_Activity, Notes_
|
|
43
43
|
|
|
44
44
|
### Differences between the Nextcloud and NextcloudApp classes
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Session represents one connection to Nextcloud. All related stuff for these live here."""
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
4
|
+
import pathlib
|
|
3
5
|
import re
|
|
4
6
|
import typing
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
@@ -198,6 +200,8 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
198
200
|
raise NextcloudException(408, info=info) from None
|
|
199
201
|
|
|
200
202
|
check_error(response, info)
|
|
203
|
+
if response.status_code == 204: # NO_CONTENT
|
|
204
|
+
return []
|
|
201
205
|
response_data = loads(response.text)
|
|
202
206
|
ocs_meta = response_data["ocs"]["meta"]
|
|
203
207
|
if ocs_meta["status"] != "ok":
|
|
@@ -248,6 +252,15 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
248
252
|
def set_user(self, user_id: str) -> None:
|
|
249
253
|
self._user = user_id
|
|
250
254
|
|
|
255
|
+
def download2stream(self, url_path: str, fp, dav: bool = False, **kwargs):
|
|
256
|
+
if isinstance(fp, str | pathlib.Path):
|
|
257
|
+
with builtins.open(fp, "wb") as f:
|
|
258
|
+
self.download2fp(url_path, f, dav, **kwargs)
|
|
259
|
+
elif hasattr(fp, "write"):
|
|
260
|
+
self.download2fp(url_path, fp, dav, **kwargs)
|
|
261
|
+
else:
|
|
262
|
+
raise TypeError("`fp` must be a path to file or an object with `write` method.")
|
|
263
|
+
|
|
251
264
|
def _get_adapter_kwargs(self, dav: bool) -> dict[str, typing.Any]:
|
|
252
265
|
if dav:
|
|
253
266
|
return {
|
|
@@ -274,6 +287,13 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
274
287
|
return
|
|
275
288
|
self.response_headers = response.headers
|
|
276
289
|
|
|
290
|
+
def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
291
|
+
adapter = self.adapter_dav if dav else self.adapter
|
|
292
|
+
with adapter.stream("GET", url_path, params=params) as response:
|
|
293
|
+
check_error(response)
|
|
294
|
+
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
295
|
+
fp.write(data_chunk)
|
|
296
|
+
|
|
277
297
|
|
|
278
298
|
class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
279
299
|
adapter: AsyncClient
|
|
@@ -298,6 +318,8 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
298
318
|
raise NextcloudException(408, info=info) from None
|
|
299
319
|
|
|
300
320
|
check_error(response, info)
|
|
321
|
+
if response.status_code == 204: # NO_CONTENT
|
|
322
|
+
return []
|
|
301
323
|
response_data = loads(response.text)
|
|
302
324
|
ocs_meta = response_data["ocs"]["meta"]
|
|
303
325
|
if ocs_meta["status"] != "ok":
|
|
@@ -350,6 +372,15 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
350
372
|
def set_user(self, user: str) -> None:
|
|
351
373
|
self._user = user
|
|
352
374
|
|
|
375
|
+
async def download2stream(self, url_path: str, fp, dav: bool = False, **kwargs):
|
|
376
|
+
if isinstance(fp, str | pathlib.Path):
|
|
377
|
+
with builtins.open(fp, "wb") as f:
|
|
378
|
+
await self.download2fp(url_path, f, dav, **kwargs)
|
|
379
|
+
elif hasattr(fp, "write"):
|
|
380
|
+
await self.download2fp(url_path, fp, dav, **kwargs)
|
|
381
|
+
else:
|
|
382
|
+
raise TypeError("`fp` must be a path to file or an object with `write` method.")
|
|
383
|
+
|
|
353
384
|
def _get_adapter_kwargs(self, dav: bool) -> dict[str, typing.Any]:
|
|
354
385
|
if dav:
|
|
355
386
|
return {
|
|
@@ -376,6 +407,13 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
376
407
|
return
|
|
377
408
|
self.response_headers = response.headers
|
|
378
409
|
|
|
410
|
+
async def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
411
|
+
adapter = self.adapter_dav if dav else self.adapter
|
|
412
|
+
async with adapter.stream("GET", url_path, params=params) as response:
|
|
413
|
+
check_error(response)
|
|
414
|
+
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
415
|
+
fp.write(data_chunk)
|
|
416
|
+
|
|
379
417
|
|
|
380
418
|
class NcSession(NcSessionBasic):
|
|
381
419
|
cfg: Config
|
|
@@ -8,6 +8,6 @@ from .integration_fastapi import (
|
|
|
8
8
|
set_handlers,
|
|
9
9
|
talk_bot_app,
|
|
10
10
|
)
|
|
11
|
-
from .misc import persistent_storage, verify_version
|
|
11
|
+
from .misc import get_model_path, persistent_storage, verify_version
|
|
12
12
|
from .ui.files_actions import UiActionFileInfo
|
|
13
13
|
from .uvicorn_fastapi import run_app
|
|
@@ -39,7 +39,11 @@ class ApiScope(enum.IntEnum):
|
|
|
39
39
|
"""Allows access to Talk API endpoints."""
|
|
40
40
|
TALK_BOT = 60
|
|
41
41
|
"""Allows to register Talk Bots."""
|
|
42
|
+
AI_PROVIDERS = 61
|
|
43
|
+
"""Allows to register AI providers."""
|
|
42
44
|
ACTIVITIES = 110
|
|
43
45
|
"""Activity App endpoints."""
|
|
44
46
|
NOTES = 120
|
|
45
|
-
"""Notes App endpoints"""
|
|
47
|
+
"""Notes App endpoints."""
|
|
48
|
+
ALL = 9999
|
|
49
|
+
"""All endpoints allowed."""
|
|
@@ -43,3 +43,10 @@ def verify_version(finalize_update: bool = True) -> tuple[str, str] | None:
|
|
|
43
43
|
version_file.write(os.environ["APP_VERSION"])
|
|
44
44
|
version_file.truncate()
|
|
45
45
|
return r
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_model_path(model_name: str) -> str:
|
|
49
|
+
"""Wrapper around hugging_face's ``snapshot_download`` to return path to downloaded model directory."""
|
|
50
|
+
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
51
|
+
|
|
52
|
+
return snapshot_download(model_name, local_files_only=True, cache_dir=persistent_storage())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""APIs related to Nextcloud Providers."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Nextcloud API for AI Providers."""
|
|
2
|
+
|
|
3
|
+
from ..._session import AsyncNcSessionApp, NcSessionApp
|
|
4
|
+
from .speech_to_text import _AsyncSpeechToTextProviderAPI, _SpeechToTextProviderAPI
|
|
5
|
+
from .text_processing import _AsyncTextProcessingProviderAPI, _TextProcessingProviderAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProvidersApi:
|
|
9
|
+
"""Class that encapsulates all AI Providers functionality."""
|
|
10
|
+
|
|
11
|
+
speech_to_text: _SpeechToTextProviderAPI
|
|
12
|
+
"""SpeechToText Provider API."""
|
|
13
|
+
text_processing: _TextProcessingProviderAPI
|
|
14
|
+
"""TextProcessing Provider API."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session: NcSessionApp):
|
|
17
|
+
self.speech_to_text = _SpeechToTextProviderAPI(session)
|
|
18
|
+
self.text_processing = _TextProcessingProviderAPI(session)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncProvidersApi:
|
|
22
|
+
"""Class that encapsulates all AI Providers functionality."""
|
|
23
|
+
|
|
24
|
+
speech_to_text: _AsyncSpeechToTextProviderAPI
|
|
25
|
+
"""SpeechToText Provider API."""
|
|
26
|
+
text_processing: _AsyncTextProcessingProviderAPI
|
|
27
|
+
"""TextProcessing Provider API."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, session: AsyncNcSessionApp):
|
|
30
|
+
self.speech_to_text = _AsyncSpeechToTextProviderAPI(session)
|
|
31
|
+
self.text_processing = _AsyncTextProcessingProviderAPI(session)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Nextcloud API for declaring SpeechToText provider."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
from ..._exceptions import NextcloudException, NextcloudExceptionNotFound
|
|
7
|
+
from ..._misc import require_capabilities
|
|
8
|
+
from ..._session import AsyncNcSessionApp, NcSessionApp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class SpeechToTextProvider:
|
|
13
|
+
"""Speech2Text provider description."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, raw_data: dict):
|
|
16
|
+
self._raw_data = raw_data
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
"""Unique ID for the provider."""
|
|
21
|
+
return self._raw_data["name"]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def display_name(self) -> str:
|
|
25
|
+
"""Providers display name."""
|
|
26
|
+
return self._raw_data["display_name"]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def action_handler(self) -> str:
|
|
30
|
+
"""Relative ExApp url which will be called by Nextcloud."""
|
|
31
|
+
return self._raw_data["action_handler"]
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return f"<{self.__class__.__name__} name={self.name}, handler={self.action_handler}>"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _SpeechToTextProviderAPI:
|
|
38
|
+
"""API for registering Speech2Text providers."""
|
|
39
|
+
|
|
40
|
+
_ep_suffix: str = "ai_provider/speech_to_text"
|
|
41
|
+
|
|
42
|
+
def __init__(self, session: NcSessionApp):
|
|
43
|
+
self._session = session
|
|
44
|
+
|
|
45
|
+
def register(self, name: str, display_name: str, callback_url: str) -> None:
|
|
46
|
+
"""Registers or edit the SpeechToText provider."""
|
|
47
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
48
|
+
params = {
|
|
49
|
+
"name": name,
|
|
50
|
+
"displayName": display_name,
|
|
51
|
+
"actionHandler": callback_url,
|
|
52
|
+
}
|
|
53
|
+
self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)
|
|
54
|
+
|
|
55
|
+
def unregister(self, name: str, not_fail=True) -> None:
|
|
56
|
+
"""Removes SpeechToText provider."""
|
|
57
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
58
|
+
try:
|
|
59
|
+
self._session.ocs("DELETE", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
60
|
+
except NextcloudExceptionNotFound as e:
|
|
61
|
+
if not not_fail:
|
|
62
|
+
raise e from None
|
|
63
|
+
|
|
64
|
+
def get_entry(self, name: str) -> SpeechToTextProvider | None:
|
|
65
|
+
"""Get information of the SpeechToText."""
|
|
66
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
67
|
+
try:
|
|
68
|
+
return SpeechToTextProvider(
|
|
69
|
+
self._session.ocs("GET", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
70
|
+
)
|
|
71
|
+
except NextcloudExceptionNotFound:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def report_result(self, task_id: int, result: str = "", error: str = "") -> None:
|
|
75
|
+
"""Report results of speech to text task to Nextcloud."""
|
|
76
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
77
|
+
with contextlib.suppress(NextcloudException):
|
|
78
|
+
self._session.ocs(
|
|
79
|
+
"PUT",
|
|
80
|
+
f"{self._session.ae_url}/{self._ep_suffix}",
|
|
81
|
+
json={"taskId": task_id, "result": result, "error": error},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _AsyncSpeechToTextProviderAPI:
|
|
86
|
+
"""API for registering Speech2Text providers."""
|
|
87
|
+
|
|
88
|
+
_ep_suffix: str = "ai_provider/speech_to_text"
|
|
89
|
+
|
|
90
|
+
def __init__(self, session: AsyncNcSessionApp):
|
|
91
|
+
self._session = session
|
|
92
|
+
|
|
93
|
+
async def register(self, name: str, display_name: str, callback_url: str) -> None:
|
|
94
|
+
"""Registers or edit the SpeechToText provider."""
|
|
95
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
96
|
+
params = {
|
|
97
|
+
"name": name,
|
|
98
|
+
"displayName": display_name,
|
|
99
|
+
"actionHandler": callback_url,
|
|
100
|
+
}
|
|
101
|
+
await self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)
|
|
102
|
+
|
|
103
|
+
async def unregister(self, name: str, not_fail=True) -> None:
|
|
104
|
+
"""Removes SpeechToText provider."""
|
|
105
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
106
|
+
try:
|
|
107
|
+
await self._session.ocs("DELETE", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
108
|
+
except NextcloudExceptionNotFound as e:
|
|
109
|
+
if not not_fail:
|
|
110
|
+
raise e from None
|
|
111
|
+
|
|
112
|
+
async def get_entry(self, name: str) -> SpeechToTextProvider | None:
|
|
113
|
+
"""Get information of the SpeechToText."""
|
|
114
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
115
|
+
try:
|
|
116
|
+
return SpeechToTextProvider(
|
|
117
|
+
await self._session.ocs("GET", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
118
|
+
)
|
|
119
|
+
except NextcloudExceptionNotFound:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
async def report_result(self, task_id: int, result: str = "", error: str = "") -> None:
|
|
123
|
+
"""Report results of speech to text task to Nextcloud."""
|
|
124
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
125
|
+
with contextlib.suppress(NextcloudException):
|
|
126
|
+
await self._session.ocs(
|
|
127
|
+
"PUT",
|
|
128
|
+
f"{self._session.ae_url}/{self._ep_suffix}",
|
|
129
|
+
json={"taskId": task_id, "result": result, "error": error},
|
|
130
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Nextcloud API for declaring TextProcessing provider."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
from ..._exceptions import NextcloudException, NextcloudExceptionNotFound
|
|
7
|
+
from ..._misc import require_capabilities
|
|
8
|
+
from ..._session import AsyncNcSessionApp, NcSessionApp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class TextProcessingProvider:
|
|
13
|
+
"""TextProcessing provider description."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, raw_data: dict):
|
|
16
|
+
self._raw_data = raw_data
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
"""Unique ID for the provider."""
|
|
21
|
+
return self._raw_data["name"]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def display_name(self) -> str:
|
|
25
|
+
"""Providers display name."""
|
|
26
|
+
return self._raw_data["display_name"]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def action_handler(self) -> str:
|
|
30
|
+
"""Relative ExApp url which will be called by Nextcloud."""
|
|
31
|
+
return self._raw_data["action_handler"]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def task_type(self) -> str:
|
|
35
|
+
"""The TaskType provided by this provider."""
|
|
36
|
+
return self._raw_data["task_type"]
|
|
37
|
+
|
|
38
|
+
def __repr__(self):
|
|
39
|
+
return f"<{self.__class__.__name__} name={self.name}, type={self.task_type}, handler={self.action_handler}>"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _TextProcessingProviderAPI:
|
|
43
|
+
"""API for registering TextProcessing providers."""
|
|
44
|
+
|
|
45
|
+
_ep_suffix: str = "ai_provider/text_processing"
|
|
46
|
+
|
|
47
|
+
def __init__(self, session: NcSessionApp):
|
|
48
|
+
self._session = session
|
|
49
|
+
|
|
50
|
+
def register(self, name: str, display_name: str, callback_url: str, task_type: str) -> None:
|
|
51
|
+
"""Registers or edit the TextProcessing provider."""
|
|
52
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
53
|
+
params = {
|
|
54
|
+
"name": name,
|
|
55
|
+
"displayName": display_name,
|
|
56
|
+
"actionHandler": callback_url,
|
|
57
|
+
"taskType": task_type,
|
|
58
|
+
}
|
|
59
|
+
self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)
|
|
60
|
+
|
|
61
|
+
def unregister(self, name: str, not_fail=True) -> None:
|
|
62
|
+
"""Removes TextProcessing provider."""
|
|
63
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
64
|
+
try:
|
|
65
|
+
self._session.ocs("DELETE", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
66
|
+
except NextcloudExceptionNotFound as e:
|
|
67
|
+
if not not_fail:
|
|
68
|
+
raise e from None
|
|
69
|
+
|
|
70
|
+
def get_entry(self, name: str) -> TextProcessingProvider | None:
|
|
71
|
+
"""Get information of the TextProcessing."""
|
|
72
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
73
|
+
try:
|
|
74
|
+
return TextProcessingProvider(
|
|
75
|
+
self._session.ocs("GET", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
76
|
+
)
|
|
77
|
+
except NextcloudExceptionNotFound:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def report_result(self, task_id: int, result: str = "", error: str = "") -> None:
|
|
81
|
+
"""Report results of the text processing to Nextcloud."""
|
|
82
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
83
|
+
with contextlib.suppress(NextcloudException):
|
|
84
|
+
self._session.ocs(
|
|
85
|
+
"PUT",
|
|
86
|
+
f"{self._session.ae_url}/{self._ep_suffix}",
|
|
87
|
+
json={"taskId": task_id, "result": result, "error": error},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _AsyncTextProcessingProviderAPI:
|
|
92
|
+
"""API for registering TextProcessing providers."""
|
|
93
|
+
|
|
94
|
+
_ep_suffix: str = "ai_provider/text_processing"
|
|
95
|
+
|
|
96
|
+
def __init__(self, session: AsyncNcSessionApp):
|
|
97
|
+
self._session = session
|
|
98
|
+
|
|
99
|
+
async def register(self, name: str, display_name: str, callback_url: str, task_type: str) -> None:
|
|
100
|
+
"""Registers or edit the TextProcessing provider."""
|
|
101
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
102
|
+
params = {
|
|
103
|
+
"name": name,
|
|
104
|
+
"displayName": display_name,
|
|
105
|
+
"actionHandler": callback_url,
|
|
106
|
+
"taskType": task_type,
|
|
107
|
+
}
|
|
108
|
+
await self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)
|
|
109
|
+
|
|
110
|
+
async def unregister(self, name: str, not_fail=True) -> None:
|
|
111
|
+
"""Removes TextProcessing provider."""
|
|
112
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
113
|
+
try:
|
|
114
|
+
await self._session.ocs("DELETE", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
115
|
+
except NextcloudExceptionNotFound as e:
|
|
116
|
+
if not not_fail:
|
|
117
|
+
raise e from None
|
|
118
|
+
|
|
119
|
+
async def get_entry(self, name: str) -> TextProcessingProvider | None:
|
|
120
|
+
"""Get information of the TextProcessing."""
|
|
121
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
122
|
+
try:
|
|
123
|
+
return TextProcessingProvider(
|
|
124
|
+
await self._session.ocs("GET", f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
|
|
125
|
+
)
|
|
126
|
+
except NextcloudExceptionNotFound:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
async def report_result(self, task_id: int, result: str = "", error: str = "") -> None:
|
|
130
|
+
"""Report results of the text processing to Nextcloud."""
|
|
131
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
132
|
+
with contextlib.suppress(NextcloudException):
|
|
133
|
+
await self._session.ocs(
|
|
134
|
+
"PUT",
|
|
135
|
+
f"{self._session.ae_url}/{self._ep_suffix}",
|
|
136
|
+
json={"taskId": task_id, "result": result, "error": error},
|
|
137
|
+
)
|
|
@@ -150,7 +150,7 @@ class _UiFilesActionsAPI:
|
|
|
150
150
|
raise e from None
|
|
151
151
|
|
|
152
152
|
def get_entry(self, name: str) -> UiFileActionEntry | None:
|
|
153
|
-
"""Get information of the file action meny entry
|
|
153
|
+
"""Get information of the file action meny entry."""
|
|
154
154
|
require_capabilities("app_api", self._session.capabilities)
|
|
155
155
|
try:
|
|
156
156
|
return UiFileActionEntry(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import builtins
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from urllib.parse import quote
|
|
6
7
|
|
|
7
8
|
from httpx import Headers
|
|
8
9
|
|
|
@@ -85,7 +86,7 @@ class FilesAPI:
|
|
|
85
86
|
def download(self, path: str | FsNode) -> bytes:
|
|
86
87
|
"""Downloads and returns the content of a file."""
|
|
87
88
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
88
|
-
response = self._session.adapter_dav.get(dav_get_obj_path(self._session.user, path))
|
|
89
|
+
response = self._session.adapter_dav.get(quote(dav_get_obj_path(self._session.user, path)))
|
|
89
90
|
check_error(response, f"download: user={self._session.user}, path={path}")
|
|
90
91
|
return response.content
|
|
91
92
|
|
|
@@ -97,14 +98,8 @@ class FilesAPI:
|
|
|
97
98
|
The object must implement the ``file.write`` method and be able to write binary data.
|
|
98
99
|
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
|
|
99
100
|
"""
|
|
100
|
-
path = path.user_path if isinstance(path, FsNode) else path
|
|
101
|
-
|
|
102
|
-
with builtins.open(fp, "wb") as f:
|
|
103
|
-
self.__download2stream(path, f, **kwargs)
|
|
104
|
-
elif hasattr(fp, "write"):
|
|
105
|
-
self.__download2stream(path, fp, **kwargs)
|
|
106
|
-
else:
|
|
107
|
-
raise TypeError("`fp` must be a path to file or an object with `write` method.")
|
|
101
|
+
path = quote(dav_get_obj_path(self._session.user, path.user_path if isinstance(path, FsNode) else path))
|
|
102
|
+
self._session.download2stream(path, fp, dav=True, **kwargs)
|
|
108
103
|
|
|
109
104
|
def download_directory_as_zip(self, path: str | FsNode, local_path: str | Path | None = None, **kwargs) -> Path:
|
|
110
105
|
"""Downloads a remote directory as zip archive.
|
|
@@ -116,17 +111,11 @@ class FilesAPI:
|
|
|
116
111
|
.. note:: This works only for directories, you should not use this to download a file.
|
|
117
112
|
"""
|
|
118
113
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
with open(
|
|
125
|
-
result_path,
|
|
126
|
-
"wb",
|
|
127
|
-
) as fp:
|
|
128
|
-
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
129
|
-
fp.write(data_chunk)
|
|
114
|
+
result_path = local_path if local_path else os.path.basename(path)
|
|
115
|
+
with open(result_path, "wb") as fp:
|
|
116
|
+
self._session.download2fp(
|
|
117
|
+
"/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
|
|
118
|
+
)
|
|
130
119
|
return Path(result_path)
|
|
131
120
|
|
|
132
121
|
def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
|
|
@@ -137,7 +126,7 @@ class FilesAPI:
|
|
|
137
126
|
"""
|
|
138
127
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
139
128
|
full_path = dav_get_obj_path(self._session.user, path)
|
|
140
|
-
response = self._session.adapter_dav.put(full_path, content=content)
|
|
129
|
+
response = self._session.adapter_dav.put(quote(full_path), content=content)
|
|
141
130
|
check_error(response, f"upload: user={self._session.user}, path={path}, size={len(content)}")
|
|
142
131
|
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
143
132
|
|
|
@@ -166,7 +155,7 @@ class FilesAPI:
|
|
|
166
155
|
"""
|
|
167
156
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
168
157
|
full_path = dav_get_obj_path(self._session.user, path)
|
|
169
|
-
response = self._session.adapter_dav.request("MKCOL", full_path)
|
|
158
|
+
response = self._session.adapter_dav.request("MKCOL", quote(full_path))
|
|
170
159
|
check_error(response)
|
|
171
160
|
full_path += "/" if not full_path.endswith("/") else ""
|
|
172
161
|
return FsNode(full_path.lstrip("/"), **etag_fileid_from_response(response))
|
|
@@ -201,7 +190,7 @@ class FilesAPI:
|
|
|
201
190
|
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
202
191
|
"""
|
|
203
192
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
204
|
-
response = self._session.adapter_dav.delete(dav_get_obj_path(self._session.user, path))
|
|
193
|
+
response = self._session.adapter_dav.delete(quote(dav_get_obj_path(self._session.user, path)))
|
|
205
194
|
if response.status_code == 404 and not_fail:
|
|
206
195
|
return
|
|
207
196
|
check_error(response)
|
|
@@ -218,11 +207,11 @@ class FilesAPI:
|
|
|
218
207
|
full_dest_path = dav_get_obj_path(
|
|
219
208
|
self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
220
209
|
)
|
|
221
|
-
dest = self._session.cfg.dav_endpoint + full_dest_path
|
|
210
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
222
211
|
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
223
212
|
response = self._session.adapter_dav.request(
|
|
224
213
|
"MOVE",
|
|
225
|
-
dav_get_obj_path(self._session.user, path_src),
|
|
214
|
+
quote(dav_get_obj_path(self._session.user, path_src)),
|
|
226
215
|
headers=headers,
|
|
227
216
|
)
|
|
228
217
|
check_error(response, f"move: user={self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
@@ -240,11 +229,11 @@ class FilesAPI:
|
|
|
240
229
|
full_dest_path = dav_get_obj_path(
|
|
241
230
|
self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
242
231
|
)
|
|
243
|
-
dest = self._session.cfg.dav_endpoint + full_dest_path
|
|
232
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
244
233
|
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
245
234
|
response = self._session.adapter_dav.request(
|
|
246
235
|
"COPY",
|
|
247
|
-
dav_get_obj_path(self._session.user, path_src),
|
|
236
|
+
quote(dav_get_obj_path(self._session.user, path_src)),
|
|
248
237
|
headers=headers,
|
|
249
238
|
)
|
|
250
239
|
check_error(response, f"copy: user={self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
@@ -276,7 +265,7 @@ class FilesAPI:
|
|
|
276
265
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
277
266
|
root = build_setfav_req(value)
|
|
278
267
|
webdav_response = self._session.adapter_dav.request(
|
|
279
|
-
"PROPPATCH", dav_get_obj_path(self._session.user, path), content=element_tree_as_str(root)
|
|
268
|
+
"PROPPATCH", quote(dav_get_obj_path(self._session.user, path)), content=element_tree_as_str(root)
|
|
280
269
|
)
|
|
281
270
|
check_error(webdav_response, f"setfav: path={path}, value={value}")
|
|
282
271
|
|
|
@@ -300,7 +289,7 @@ class FilesAPI:
|
|
|
300
289
|
headers = Headers({"Destination": dest}, encoding="utf-8")
|
|
301
290
|
response = self._session.adapter_dav.request(
|
|
302
291
|
"MOVE",
|
|
303
|
-
f"/trashbin/{self._session.user}/{path}",
|
|
292
|
+
quote(f"/trashbin/{self._session.user}/{path}"),
|
|
304
293
|
headers=headers,
|
|
305
294
|
)
|
|
306
295
|
check_error(response, f"trashbin_restore: user={self._session.user}, src={path}, dest={dest}")
|
|
@@ -312,7 +301,7 @@ class FilesAPI:
|
|
|
312
301
|
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
313
302
|
"""
|
|
314
303
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
315
|
-
response = self._session.adapter_dav.delete(f"/trashbin/{self._session.user}/{path}")
|
|
304
|
+
response = self._session.adapter_dav.delete(quote(f"/trashbin/{self._session.user}/{path}"))
|
|
316
305
|
if response.status_code == 404 and not_fail:
|
|
317
306
|
return
|
|
318
307
|
check_error(response)
|
|
@@ -430,7 +419,7 @@ class FilesAPI:
|
|
|
430
419
|
root, dav_path = build_listdir_req(user, path, properties, prop_type)
|
|
431
420
|
webdav_response = self._session.adapter_dav.request(
|
|
432
421
|
"PROPFIND",
|
|
433
|
-
dav_path,
|
|
422
|
+
quote(dav_path),
|
|
434
423
|
content=element_tree_as_str(root),
|
|
435
424
|
headers={"Depth": "infinity" if depth == -1 else str(depth)},
|
|
436
425
|
)
|
|
@@ -438,17 +427,12 @@ class FilesAPI:
|
|
|
438
427
|
self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
|
|
439
428
|
)
|
|
440
429
|
|
|
441
|
-
def __download2stream(self, path: str, fp, **kwargs) -> None:
|
|
442
|
-
with self._session.adapter_dav.stream("GET", dav_get_obj_path(self._session.user, path)) as response:
|
|
443
|
-
check_error(response, f"download_stream: user={self._session.user}, path={path}")
|
|
444
|
-
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
445
|
-
fp.write(data_chunk)
|
|
446
|
-
|
|
447
430
|
def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
|
|
448
|
-
|
|
431
|
+
_tmp_path = "nc-py-api-" + random_string(56)
|
|
432
|
+
_dav_path = quote(dav_get_obj_path(self._session.user, _tmp_path, root_path="/uploads"))
|
|
449
433
|
_v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
|
|
450
434
|
full_path = dav_get_obj_path(self._session.user, path)
|
|
451
|
-
headers = Headers({"Destination": self._session.cfg.dav_endpoint + full_path}, encoding="utf-8")
|
|
435
|
+
headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
|
|
452
436
|
if _v2:
|
|
453
437
|
response = self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
|
|
454
438
|
else:
|
|
@@ -547,7 +531,7 @@ class AsyncFilesAPI:
|
|
|
547
531
|
async def download(self, path: str | FsNode) -> bytes:
|
|
548
532
|
"""Downloads and returns the content of a file."""
|
|
549
533
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
550
|
-
response = await self._session.adapter_dav.get(dav_get_obj_path(await self._session.user, path))
|
|
534
|
+
response = await self._session.adapter_dav.get(quote(dav_get_obj_path(await self._session.user, path)))
|
|
551
535
|
check_error(response, f"download: user={await self._session.user}, path={path}")
|
|
552
536
|
return response.content
|
|
553
537
|
|
|
@@ -559,14 +543,8 @@ class AsyncFilesAPI:
|
|
|
559
543
|
The object must implement the ``file.write`` method and be able to write binary data.
|
|
560
544
|
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
|
|
561
545
|
"""
|
|
562
|
-
path = path.user_path if isinstance(path, FsNode) else path
|
|
563
|
-
|
|
564
|
-
with builtins.open(fp, "wb") as f:
|
|
565
|
-
await self.__download2stream(path, f, **kwargs)
|
|
566
|
-
elif hasattr(fp, "write"):
|
|
567
|
-
await self.__download2stream(path, fp, **kwargs)
|
|
568
|
-
else:
|
|
569
|
-
raise TypeError("`fp` must be a path to file or an object with `write` method.")
|
|
546
|
+
path = quote(dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path))
|
|
547
|
+
await self._session.download2stream(path, fp, dav=True, **kwargs)
|
|
570
548
|
|
|
571
549
|
async def download_directory_as_zip(
|
|
572
550
|
self, path: str | FsNode, local_path: str | Path | None = None, **kwargs
|
|
@@ -580,17 +558,11 @@ class AsyncFilesAPI:
|
|
|
580
558
|
.. note:: This works only for directories, you should not use this to download a file.
|
|
581
559
|
"""
|
|
582
560
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
with open(
|
|
589
|
-
result_path,
|
|
590
|
-
"wb",
|
|
591
|
-
) as fp:
|
|
592
|
-
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
593
|
-
fp.write(data_chunk)
|
|
561
|
+
result_path = local_path if local_path else os.path.basename(path)
|
|
562
|
+
with open(result_path, "wb") as fp:
|
|
563
|
+
await self._session.download2fp(
|
|
564
|
+
"/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
|
|
565
|
+
)
|
|
594
566
|
return Path(result_path)
|
|
595
567
|
|
|
596
568
|
async def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
|
|
@@ -601,7 +573,7 @@ class AsyncFilesAPI:
|
|
|
601
573
|
"""
|
|
602
574
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
603
575
|
full_path = dav_get_obj_path(await self._session.user, path)
|
|
604
|
-
response = await self._session.adapter_dav.put(full_path, content=content)
|
|
576
|
+
response = await self._session.adapter_dav.put(quote(full_path), content=content)
|
|
605
577
|
check_error(response, f"upload: user={await self._session.user}, path={path}, size={len(content)}")
|
|
606
578
|
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
607
579
|
|
|
@@ -630,7 +602,7 @@ class AsyncFilesAPI:
|
|
|
630
602
|
"""
|
|
631
603
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
632
604
|
full_path = dav_get_obj_path(await self._session.user, path)
|
|
633
|
-
response = await self._session.adapter_dav.request("MKCOL", full_path)
|
|
605
|
+
response = await self._session.adapter_dav.request("MKCOL", quote(full_path))
|
|
634
606
|
check_error(response)
|
|
635
607
|
full_path += "/" if not full_path.endswith("/") else ""
|
|
636
608
|
return FsNode(full_path.lstrip("/"), **etag_fileid_from_response(response))
|
|
@@ -665,7 +637,7 @@ class AsyncFilesAPI:
|
|
|
665
637
|
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
666
638
|
"""
|
|
667
639
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
668
|
-
response = await self._session.adapter_dav.delete(dav_get_obj_path(await self._session.user, path))
|
|
640
|
+
response = await self._session.adapter_dav.delete(quote(dav_get_obj_path(await self._session.user, path)))
|
|
669
641
|
if response.status_code == 404 and not_fail:
|
|
670
642
|
return
|
|
671
643
|
check_error(response)
|
|
@@ -682,11 +654,11 @@ class AsyncFilesAPI:
|
|
|
682
654
|
full_dest_path = dav_get_obj_path(
|
|
683
655
|
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
684
656
|
)
|
|
685
|
-
dest = self._session.cfg.dav_endpoint + full_dest_path
|
|
657
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
686
658
|
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
687
659
|
response = await self._session.adapter_dav.request(
|
|
688
660
|
"MOVE",
|
|
689
|
-
dav_get_obj_path(await self._session.user, path_src),
|
|
661
|
+
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
690
662
|
headers=headers,
|
|
691
663
|
)
|
|
692
664
|
check_error(response, f"move: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
@@ -704,11 +676,11 @@ class AsyncFilesAPI:
|
|
|
704
676
|
full_dest_path = dav_get_obj_path(
|
|
705
677
|
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
706
678
|
)
|
|
707
|
-
dest = self._session.cfg.dav_endpoint + full_dest_path
|
|
679
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
708
680
|
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
709
681
|
response = await self._session.adapter_dav.request(
|
|
710
682
|
"COPY",
|
|
711
|
-
dav_get_obj_path(await self._session.user, path_src),
|
|
683
|
+
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
712
684
|
headers=headers,
|
|
713
685
|
)
|
|
714
686
|
check_error(response, f"copy: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
@@ -740,7 +712,7 @@ class AsyncFilesAPI:
|
|
|
740
712
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
741
713
|
root = build_setfav_req(value)
|
|
742
714
|
webdav_response = await self._session.adapter_dav.request(
|
|
743
|
-
"PROPPATCH", dav_get_obj_path(await self._session.user, path), content=element_tree_as_str(root)
|
|
715
|
+
"PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), content=element_tree_as_str(root)
|
|
744
716
|
)
|
|
745
717
|
check_error(webdav_response, f"setfav: path={path}, value={value}")
|
|
746
718
|
|
|
@@ -769,7 +741,7 @@ class AsyncFilesAPI:
|
|
|
769
741
|
headers = Headers({"Destination": dest}, encoding="utf-8")
|
|
770
742
|
response = await self._session.adapter_dav.request(
|
|
771
743
|
"MOVE",
|
|
772
|
-
f"/trashbin/{await self._session.user}/{path}",
|
|
744
|
+
quote(f"/trashbin/{await self._session.user}/{path}"),
|
|
773
745
|
headers=headers,
|
|
774
746
|
)
|
|
775
747
|
check_error(response, f"trashbin_restore: user={await self._session.user}, src={path}, dest={dest}")
|
|
@@ -781,7 +753,7 @@ class AsyncFilesAPI:
|
|
|
781
753
|
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
782
754
|
"""
|
|
783
755
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
784
|
-
response = await self._session.adapter_dav.delete(f"/trashbin/{await self._session.user}/{path}")
|
|
756
|
+
response = await self._session.adapter_dav.delete(quote(f"/trashbin/{await self._session.user}/{path}"))
|
|
785
757
|
if response.status_code == 404 and not_fail:
|
|
786
758
|
return
|
|
787
759
|
check_error(response)
|
|
@@ -899,7 +871,7 @@ class AsyncFilesAPI:
|
|
|
899
871
|
root, dav_path = build_listdir_req(user, path, properties, prop_type)
|
|
900
872
|
webdav_response = await self._session.adapter_dav.request(
|
|
901
873
|
"PROPFIND",
|
|
902
|
-
dav_path,
|
|
874
|
+
quote(dav_path),
|
|
903
875
|
content=element_tree_as_str(root),
|
|
904
876
|
headers={"Depth": "infinity" if depth == -1 else str(depth)},
|
|
905
877
|
)
|
|
@@ -907,19 +879,12 @@ class AsyncFilesAPI:
|
|
|
907
879
|
self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
|
|
908
880
|
)
|
|
909
881
|
|
|
910
|
-
async def __download2stream(self, path: str, fp, **kwargs) -> None:
|
|
911
|
-
async with self._session.adapter_dav.stream(
|
|
912
|
-
"GET", dav_get_obj_path(await self._session.user, path)
|
|
913
|
-
) as response:
|
|
914
|
-
check_error(response, f"download_stream: user={await self._session.user}, path={path}")
|
|
915
|
-
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
916
|
-
fp.write(data_chunk)
|
|
917
|
-
|
|
918
882
|
async def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
|
|
919
|
-
|
|
883
|
+
_tmp_path = "nc-py-api-" + random_string(56)
|
|
884
|
+
_dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
|
|
920
885
|
_v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
|
|
921
886
|
full_path = dav_get_obj_path(await self._session.user, path)
|
|
922
|
-
headers = Headers({"Destination": self._session.cfg.dav_endpoint + full_path}, encoding="utf-8")
|
|
887
|
+
headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
|
|
923
888
|
if _v2:
|
|
924
889
|
response = await self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
|
|
925
890
|
else:
|
|
@@ -31,6 +31,7 @@ from .activity import _ActivityAPI, _AsyncActivityAPI
|
|
|
31
31
|
from .apps import _AppsAPI, _AsyncAppsAPI
|
|
32
32
|
from .calendar import _CalendarAPI
|
|
33
33
|
from .ex_app.defs import ApiScope, LogLvl
|
|
34
|
+
from .ex_app.providers.providers import AsyncProvidersApi, ProvidersApi
|
|
34
35
|
from .ex_app.ui.ui import AsyncUiApi, UiApi
|
|
35
36
|
from .files.files import AsyncFilesAPI, FilesAPI
|
|
36
37
|
from .notes import _AsyncNotesAPI, _NotesAPI
|
|
@@ -113,6 +114,14 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
113
114
|
"""Returns Theme information."""
|
|
114
115
|
return get_parsed_theme(self.capabilities["theming"]) if "theming" in self.capabilities else None
|
|
115
116
|
|
|
117
|
+
def perform_login(self) -> bool:
|
|
118
|
+
"""Performs login into Nextcloud if not already logged in; manual invocation of this method is unnecessary."""
|
|
119
|
+
try:
|
|
120
|
+
self.update_server_info()
|
|
121
|
+
except Exception: # noqa pylint: disable=broad-exception-caught
|
|
122
|
+
return False
|
|
123
|
+
return True
|
|
124
|
+
|
|
116
125
|
def ocs(
|
|
117
126
|
self,
|
|
118
127
|
method: str,
|
|
@@ -126,6 +135,10 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
126
135
|
"""Performs OCS call and returns OCS response payload data."""
|
|
127
136
|
return self._session.ocs(method, path, content=content, json=json, params=params, **kwargs)
|
|
128
137
|
|
|
138
|
+
def download_log(self, fp) -> None:
|
|
139
|
+
"""Downloads Nextcloud log file. Requires Admin privileges."""
|
|
140
|
+
self._session.download2stream("/index.php/settings/admin/log/download", fp)
|
|
141
|
+
|
|
129
142
|
|
|
130
143
|
class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
131
144
|
apps: _AsyncAppsAPI
|
|
@@ -199,6 +212,14 @@ class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
199
212
|
"""Returns Theme information."""
|
|
200
213
|
return get_parsed_theme((await self.capabilities)["theming"]) if "theming" in await self.capabilities else None
|
|
201
214
|
|
|
215
|
+
async def perform_login(self) -> bool:
|
|
216
|
+
"""Performs login into Nextcloud if not already logged in; manual invocation of this method is unnecessary."""
|
|
217
|
+
try:
|
|
218
|
+
await self.update_server_info()
|
|
219
|
+
except Exception: # noqa pylint: disable=broad-exception-caught
|
|
220
|
+
return False
|
|
221
|
+
return True
|
|
222
|
+
|
|
202
223
|
async def ocs(
|
|
203
224
|
self,
|
|
204
225
|
method: str,
|
|
@@ -212,6 +233,10 @@ class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
212
233
|
"""Performs OCS call and returns OCS response payload data."""
|
|
213
234
|
return await self._session.ocs(method, path, content=content, json=json, params=params, **kwargs)
|
|
214
235
|
|
|
236
|
+
async def download_log(self, fp) -> None:
|
|
237
|
+
"""Downloads Nextcloud log file. Requires Admin privileges."""
|
|
238
|
+
await self._session.download2stream("/index.php/settings/admin/log/download", fp)
|
|
239
|
+
|
|
215
240
|
|
|
216
241
|
class Nextcloud(_NextcloudBasic):
|
|
217
242
|
"""Nextcloud client class.
|
|
@@ -278,6 +303,8 @@ class NextcloudApp(_NextcloudBasic):
|
|
|
278
303
|
"""Nextcloud User Preferences API for ExApps"""
|
|
279
304
|
ui: UiApi
|
|
280
305
|
"""Nextcloud UI API for ExApps"""
|
|
306
|
+
providers: ProvidersApi
|
|
307
|
+
"""API for registering providers for Nextcloud"""
|
|
281
308
|
|
|
282
309
|
def __init__(self, **kwargs):
|
|
283
310
|
"""The parameters will be taken from the environment.
|
|
@@ -289,6 +316,7 @@ class NextcloudApp(_NextcloudBasic):
|
|
|
289
316
|
self.appconfig_ex = AppConfigExAPI(self._session)
|
|
290
317
|
self.preferences_ex = PreferencesExAPI(self._session)
|
|
291
318
|
self.ui = UiApi(self._session)
|
|
319
|
+
self.providers = ProvidersApi(self._session)
|
|
292
320
|
|
|
293
321
|
def log(self, log_lvl: LogLvl, content: str) -> None:
|
|
294
322
|
"""Writes log to the Nextcloud log file."""
|
|
@@ -415,6 +443,8 @@ class AsyncNextcloudApp(_AsyncNextcloudBasic):
|
|
|
415
443
|
"""Nextcloud User Preferences API for ExApps"""
|
|
416
444
|
ui: AsyncUiApi
|
|
417
445
|
"""Nextcloud UI API for ExApps"""
|
|
446
|
+
providers: AsyncProvidersApi
|
|
447
|
+
"""API for registering providers for Nextcloud"""
|
|
418
448
|
|
|
419
449
|
def __init__(self, **kwargs):
|
|
420
450
|
"""The parameters will be taken from the environment.
|
|
@@ -426,6 +456,7 @@ class AsyncNextcloudApp(_AsyncNextcloudBasic):
|
|
|
426
456
|
self.appconfig_ex = AsyncAppConfigExAPI(self._session)
|
|
427
457
|
self.preferences_ex = AsyncPreferencesExAPI(self._session)
|
|
428
458
|
self.ui = AsyncUiApi(self._session)
|
|
459
|
+
self.providers = AsyncProvidersApi(self._session)
|
|
429
460
|
|
|
430
461
|
async def log(self, log_lvl: LogLvl, content: str) -> None:
|
|
431
462
|
"""Writes log to the Nextcloud log file."""
|
|
@@ -143,7 +143,7 @@ basic.good-names = [
|
|
|
143
143
|
]
|
|
144
144
|
reports.output-format = "colorized"
|
|
145
145
|
similarities.ignore-imports = "yes"
|
|
146
|
-
similarities.min-similarity-lines =
|
|
146
|
+
similarities.min-similarity-lines = 10
|
|
147
147
|
messages_control.disable = [
|
|
148
148
|
"missing-class-docstring",
|
|
149
149
|
"missing-function-docstring",
|
|
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
|