nc-py-api 0.7.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nc_py_api/_session.py CHANGED
@@ -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
nc_py_api/_version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version of nc_py_api."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.8.0"
@@ -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
nc_py_api/ex_app/defs.py CHANGED
@@ -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."""
nc_py_api/ex_app/misc.py CHANGED
@@ -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 for current app."""
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(
nc_py_api/files/files.py CHANGED
@@ -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
- if isinstance(fp, str | Path):
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
- with self._session.adapter.stream(
120
- "GET", "/index.php/apps/files/ajax/download.php", params={"dir": path}
121
- ) as response:
122
- check_error(response, f"download_directory_as_zip: user={self._session.user}, path={path}")
123
- result_path = local_path if local_path else os.path.basename(path)
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
- _dav_path = dav_get_obj_path(self._session.user, "nc-py-api-" + random_string(56), root_path="/uploads")
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
- if isinstance(fp, str | Path):
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
- async with self._session.adapter.stream(
584
- "GET", "/index.php/apps/files/ajax/download.php", params={"dir": path}
585
- ) as response:
586
- check_error(response, f"download_directory_as_zip: user={await self._session.user}, path={path}")
587
- result_path = local_path if local_path else os.path.basename(path)
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
- _dav_path = dav_get_obj_path(await self._session.user, "nc-py-api-" + random_string(56), root_path="/uploads")
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:
nc_py_api/nextcloud.py CHANGED
@@ -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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nc-py-api
3
- Version: 0.7.1
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
- | Text Processing* | N/A | | |
104
- | SpeechToText* | N/A | ❌ | ❌ |
105
-
106
- &ast;_available only for NextcloudApp_<br>
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
+ &ast;_available only for **NextcloudApp**_<br>
106
+ &ast;&ast;_available only for **NextcloudApp**: SpeechToText, TextProcessing_<br>
107
107
  &ast;&ast;&ast;_Activity, Notes_
108
108
 
109
109
  ### Differences between the Nextcloud and NextcloudApp classes
@@ -4,14 +4,14 @@ nc_py_api/_exceptions.py,sha256=7vbUECaLmD7RJBCU27t4fuP6NmQK6r4508u_gS4szhI,2298
4
4
  nc_py_api/_misc.py,sha256=dUzCP9VmyhtICTsn1aexlFAYUioBm40k6Zh-YE5WwCY,3333
5
5
  nc_py_api/_preferences.py,sha256=OtovFZuGHnHYKjdDjSnUappO795tW8Oxj7qVaejHWpQ,2479
6
6
  nc_py_api/_preferences_ex.py,sha256=gW9bWVHayE6TifoA5hY_RWYUkdFgWrd0OabPvuNSAe0,7091
7
- nc_py_api/_session.py,sha256=y1tKqootIRI7c2jS70fsnmcm1imEjviJ2GzFWYLB7dg,17895
7
+ nc_py_api/_session.py,sha256=FEvwKofUkqH4b-s0ieDWeb_nFpaczFSHN4iQAuMs-JQ,19761
8
8
  nc_py_api/_talk_api.py,sha256=sIEBATbTVuLtLvcyOPwpUSURMyGl8ZpGB7hnEWKQbpM,51033
9
9
  nc_py_api/_theming.py,sha256=hTr3nuOemSuRFZaPy9iXNmBM7rDgQHECH43tHMWGqEY,1870
10
- nc_py_api/_version.py,sha256=wKplGHRmG8XcnEoJl1Yx2Zhykyx1f7YBUfMrnHri8DI,51
10
+ nc_py_api/_version.py,sha256=21vz1dOI_hTdZ_xG9sYAdYVNNIUxBWfMJUMbAqsqFRI,51
11
11
  nc_py_api/activity.py,sha256=t9VDSnnaXRNOvALqOSGCeXSQZ-426pCOMSfQ96JHys4,9574
12
12
  nc_py_api/apps.py,sha256=6vOFFs6vNHCCvZ_SwXxPq5-X1xfgyLjW8uZSfJKduC8,9774
13
13
  nc_py_api/calendar.py,sha256=-T6CJ8cRbJZTLtxSEDWuuYpD29DMJGCTfLONmtxZV9w,1445
14
- nc_py_api/nextcloud.py,sha256=xz5nnE-SJitvj5HQ2HY-n85KYbOs3jI39GnDXYA2QcA,20908
14
+ nc_py_api/nextcloud.py,sha256=RsPX0Rqr01RGhUUNJHunNVEaSwF9MAqgK35vTYNS8Pw,22328
15
15
  nc_py_api/notes.py,sha256=ljO3TOe-Qg0bJ0mlFQwjg--Pxmj-XFknoLbcbJmII0A,15106
16
16
  nc_py_api/notifications.py,sha256=WgzV21TuLOhLk-UEjhBSbMsIi2isa5MmAx6cbe0pc2Y,9187
17
17
  nc_py_api/options.py,sha256=K5co-fIfFVbwF6r3sqWsJF3cKgAbS2CjLAXdyTOkP9s,1717
@@ -21,23 +21,27 @@ nc_py_api/user_status.py,sha256=AwixZTY_KBSbqTA-VLZs_auIs8hmi_wSrv-abf2lit8,1328
21
21
  nc_py_api/users.py,sha256=B6By-H9oUNvJj2omaRCMPIsQ17xeeDV_udn0VioRO7A,15488
22
22
  nc_py_api/users_groups.py,sha256=IPxw-Ks5NjCm6r8_HC9xmf3IYptH00ulITbp5iazhAo,6289
23
23
  nc_py_api/weather_status.py,sha256=wAkjuJPjxc0Rxe4za0BzfwB0XeUmkCXoisJtTH3-qdQ,7582
24
- nc_py_api/ex_app/__init__.py,sha256=bdUoi6IbjSam3w300C_bs7pi0yy-lfVhEpKJThz7Ej8,356
25
- nc_py_api/ex_app/defs.py,sha256=3CY-g9FO7dsH9loJLP3SfdqKaZnbUmZs41-w4qph_XE,1238
24
+ nc_py_api/ex_app/__init__.py,sha256=g4Xrv86x1Z3JNvhYJQrmsmDCAqsHX-C2fgLsplJgpzA,372
25
+ nc_py_api/ex_app/defs.py,sha256=CeUrY20Sf0zzhHrTVsfAvPn4N7PHwwPQh_4X49qAhHM,1352
26
26
  nc_py_api/ex_app/integration_fastapi.py,sha256=1wDNFIV238zkGeV8cdoGlj_pErytd63d3JroEMbu12I,8050
27
- nc_py_api/ex_app/misc.py,sha256=qOhcgobKIR_EyksDuyxK0jS6jyz47oHvU4ymJZ2-9Hg,1742
27
+ nc_py_api/ex_app/misc.py,sha256=6tPZuDVQa3lyXhK9M66M0dLNFQvrECRtXmULMjIa8-w,2098
28
28
  nc_py_api/ex_app/persist_transformers_cache.py,sha256=ZoEBb1RnNaQrtxK_CjSZ8LZ36Oakz2Xciau_ZpNp8Ic,213
29
29
  nc_py_api/ex_app/uvicorn_fastapi.py,sha256=WLtNmWXMBKN6CMip2uhKcgy4mC2Ch9AmNfwRScBUsP0,752
30
+ nc_py_api/ex_app/providers/__init__.py,sha256=jmUBdbAgzUCdYyHl8V5UCNYJI-FFpxPQQ4iEAoURKQs,43
31
+ nc_py_api/ex_app/providers/providers.py,sha256=mcHh-5rK7gb_bVEeROlJRi4DlD0aHHONMqLEjjwklxc,1172
32
+ nc_py_api/ex_app/providers/speech_to_text.py,sha256=aLqgsxeCwDmFCTfeQzI5hldeYAk8KNKTUkTFgKXPZLw,5017
33
+ nc_py_api/ex_app/providers/text_processing.py,sha256=fqc1MSlKBXvteUy2LXJYfA1eYaedCwan7O7jqs11dpY,5325
30
34
  nc_py_api/ex_app/ui/__init__.py,sha256=jUMU7_miFF-Q8BQNT90KZYQiLy_a3OvEyK6y8eRMKRk,38
31
- nc_py_api/ex_app/ui/files_actions.py,sha256=0OX0cp_jpW4gIlaF2J3bxFUmAWxa46iAKmub9UPTg_U,7428
35
+ nc_py_api/ex_app/ui/files_actions.py,sha256=j2e8PXFoEmpFKF2z4YLChj0GN3S94lmwiddii1VmQKs,7412
32
36
  nc_py_api/ex_app/ui/resources.py,sha256=Qgy3KB9n68EGGz8jRlHa2Y72WxXOdaqDy2iFbQ4pvhA,12457
33
37
  nc_py_api/ex_app/ui/top_menu.py,sha256=6BvNtm03rF-fqmOiGM2mVrn0sJRLOWLfhrvzXj7ND7U,5076
34
38
  nc_py_api/ex_app/ui/ui.py,sha256=dplr1ZIqjtHvOH3lIHBLWmP9vr-m7EIk_oJU5K62low,1284
35
39
  nc_py_api/files/__init__.py,sha256=sVidF669ocg9vSoo4TBKAwGRRVUJHxtyENNUko11qTs,11798
36
40
  nc_py_api/files/_files.py,sha256=IJibj0_y3UQyyVhVI2RgQapPFPsOAV6tyH2sFlLy09I,12226
37
- nc_py_api/files/files.py,sha256=n6SsuYJVO3BA1mofBuO6mOl0tflkMdgzgY3uxYlSGeo,46557
41
+ nc_py_api/files/files.py,sha256=298GqszMB1ya2CYPld9q6O2QhNGEIFAZShjQ_O84XZs,44904
38
42
  nc_py_api/files/sharing.py,sha256=bZRSsFdlaXJKheXtyTjPJwpQSpYDnRcyDosMwrvRlRc,14395
39
- nc_py_api-0.7.1.dist-info/METADATA,sha256=khc0FpYq3frTqngrbfcHaIlkuoR5gRQ8ACuXTOrkRno,8571
40
- nc_py_api-0.7.1.dist-info/WHEEL,sha256=mRYSEL3Ih6g5a_CVMIcwiF__0Ae4_gLYh01YFNwiq1k,87
41
- nc_py_api-0.7.1.dist-info/licenses/AUTHORS,sha256=Y1omFHyI8ned9k4jJXs2ATgmgi1GmQ7EZ6S1gxqnX2k,572
42
- nc_py_api-0.7.1.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
43
- nc_py_api-0.7.1.dist-info/RECORD,,
43
+ nc_py_api-0.8.0.dist-info/METADATA,sha256=PR4pUcgSErWO3axutjUghxlsXoXMt4jnKNlkwJmYVQs,8778
44
+ nc_py_api-0.8.0.dist-info/WHEEL,sha256=mRYSEL3Ih6g5a_CVMIcwiF__0Ae4_gLYh01YFNwiq1k,87
45
+ nc_py_api-0.8.0.dist-info/licenses/AUTHORS,sha256=Y1omFHyI8ned9k4jJXs2ATgmgi1GmQ7EZ6S1gxqnX2k,572
46
+ nc_py_api-0.8.0.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
47
+ nc_py_api-0.8.0.dist-info/RECORD,,