nc-py-api 0.21.0__py3-none-any.whl → 0.22.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/__init__.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from . import ex_app, options
4
4
  from ._exceptions import (
5
+ ModelFetchError,
5
6
  NextcloudException,
6
7
  NextcloudExceptionNotFound,
7
8
  NextcloudMissingCapabilities,
nc_py_api/_exceptions.py CHANGED
@@ -65,3 +65,7 @@ def check_error(response: Response, info: str = ""):
65
65
  response.raise_for_status()
66
66
  except HTTPError as e:
67
67
  raise NextcloudException(status_code, reason=response.reason, info=info) from e
68
+
69
+
70
+ class ModelFetchError(Exception):
71
+ """Exception raised when model fetching fails."""
nc_py_api/_version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version of nc_py_api."""
2
2
 
3
- __version__ = "0.21.0"
3
+ __version__ = "0.22.0"
@@ -7,6 +7,7 @@ import hashlib
7
7
  import json
8
8
  import os
9
9
  import typing
10
+ from traceback import format_exc
10
11
  from urllib.parse import urlparse
11
12
 
12
13
  import niquests
@@ -22,10 +23,10 @@ from fastapi.responses import JSONResponse, PlainTextResponse
22
23
  from starlette.requests import HTTPConnection, Request
23
24
  from starlette.types import ASGIApp, Receive, Scope, Send
24
25
 
26
+ from .._exceptions import ModelFetchError
25
27
  from .._misc import get_username_secret_from_headers
26
28
  from ..nextcloud import AsyncNextcloudApp, NextcloudApp
27
29
  from ..talk_bot import TalkBotMessage
28
- from .defs import LogLvl
29
30
  from .misc import persistent_storage
30
31
 
31
32
 
@@ -60,6 +61,7 @@ def set_handlers(
60
61
  default_init: bool = True,
61
62
  models_to_fetch: dict[str, dict] | None = None,
62
63
  map_app_static: bool = True,
64
+ trigger_handler: typing.Callable[[str], typing.Awaitable[None] | None] | None = None,
63
65
  ):
64
66
  """Defines handlers for the application.
65
67
 
@@ -70,13 +72,30 @@ def set_handlers(
70
72
 
71
73
  .. note:: When this parameter is ``False``, the provision of ``models_to_fetch`` is not allowed.
72
74
 
73
- :param models_to_fetch: Dictionary describing which models should be downloaded during `init`.
75
+ :param models_to_fetch: Dictionary describing which models should be downloaded during `init` of the form:
76
+ .. code-block:: python
77
+ {
78
+ "model_url_1": {
79
+ "save_path": "path_or_filename_to_save_the_model_to",
80
+ },
81
+ "huggingface_model_name_1": {
82
+ "max_workers": 4,
83
+ "cache_dir": "path_to_cache_dir",
84
+ "revision": "revision_to_fetch",
85
+ ...
86
+ },
87
+ ...
88
+ }
89
+
74
90
 
75
91
  .. note:: ``huggingface_hub`` package should be present for automatic models fetching.
92
+ All model options are optional and can be left empty.
76
93
 
77
94
  :param map_app_static: Should be folders ``js``, ``css``, ``l10n``, ``img`` automatically mounted in FastAPI or not.
78
95
 
79
96
  .. note:: First, presence of these directories in the current working dir is checked, then one directory higher.
97
+
98
+ :param trigger_handler: callback that is called for task processing `trigger` events with the id of the provider.
80
99
  """
81
100
  if models_to_fetch is not None and default_init is False:
82
101
  raise ValueError("`models_to_fetch` can be defined only with `default_init`=True.")
@@ -109,6 +128,21 @@ def set_handlers(
109
128
  if map_app_static:
110
129
  __map_app_static_folders(fast_api_app)
111
130
 
131
+ if trigger_handler:
132
+ if asyncio.iscoroutinefunction(trigger_handler):
133
+
134
+ @fast_api_app.post("/trigger")
135
+ async def trigger_callback(providerId: str): # pylint: disable=invalid-name
136
+ await trigger_handler(providerId)
137
+ return JSONResponse(content={})
138
+
139
+ else:
140
+
141
+ @fast_api_app.post("/trigger")
142
+ def trigger_callback(providerId: str): # pylint: disable=invalid-name
143
+ trigger_handler(providerId)
144
+ return JSONResponse(content={})
145
+
112
146
 
113
147
  def __map_app_static_folders(fast_api_app: FastAPI):
114
148
  """Function to mount all necessary static folders to FastAPI."""
@@ -121,74 +155,98 @@ def __map_app_static_folders(fast_api_app: FastAPI):
121
155
 
122
156
 
123
157
  def fetch_models_task(nc: NextcloudApp, models: dict[str, dict], progress_init_start_value: int) -> None:
124
- """Use for cases when you want to define custom `/init` but still need to easy download models."""
158
+ """Use for cases when you want to define custom `/init` but still need to easy download models.
159
+
160
+ :param nc: NextcloudApp instance.
161
+ :param models_to_fetch: Dictionary describing which models should be downloaded of the form:
162
+ .. code-block:: python
163
+ {
164
+ "model_url_1": {
165
+ "save_path": "path_or_filename_to_save_the_model_to",
166
+ },
167
+ "huggingface_model_name_1": {
168
+ "max_workers": 4,
169
+ "cache_dir": "path_to_cache_dir",
170
+ "revision": "revision_to_fetch",
171
+ ...
172
+ },
173
+ ...
174
+ }
175
+
176
+ .. note:: ``huggingface_hub`` package should be present for automatic models fetching.
177
+ All model options are optional and can be left empty.
178
+
179
+ :param progress_init_start_value: Integer value defining from which percent the progress should start.
180
+
181
+ :raises ModelFetchError: in case of a model download error.
182
+ :raises NextcloudException: in case of a network error reaching the Nextcloud server.
183
+ """
125
184
  if models:
126
185
  current_progress = progress_init_start_value
127
186
  percent_for_each = min(int((100 - progress_init_start_value) / len(models)), 99)
128
187
  for model in models:
129
- if model.startswith(("http://", "https://")):
130
- models[model]["path"] = __fetch_model_as_file(
131
- current_progress, percent_for_each, nc, model, models[model]
132
- )
133
- else:
134
- models[model]["path"] = __fetch_model_as_snapshot(
135
- current_progress, percent_for_each, nc, model, models[model]
136
- )
137
- current_progress += percent_for_each
188
+ try:
189
+ if model.startswith(("http://", "https://")):
190
+ models[model]["path"] = __fetch_model_as_file(
191
+ current_progress, percent_for_each, nc, model, models[model]
192
+ )
193
+ else:
194
+ models[model]["path"] = __fetch_model_as_snapshot(
195
+ current_progress, percent_for_each, nc, model, models[model]
196
+ )
197
+ current_progress += percent_for_each
198
+ except BaseException as e: # noqa pylint: disable=broad-exception-caught
199
+ nc.set_init_status(current_progress, f"Downloading of '{model}' failed: {e}: {format_exc()}")
200
+ raise ModelFetchError(f"Downloading of '{model}' failed.") from e
138
201
  nc.set_init_status(100)
139
202
 
140
203
 
141
204
  def __fetch_model_as_file(
142
205
  current_progress: int, progress_for_task: int, nc: NextcloudApp, model_path: str, download_options: dict
143
- ) -> str | None:
206
+ ) -> str:
144
207
  result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
145
- try:
146
-
147
- with niquests.get("GET", model_path, stream=True) as response:
148
- if not response.is_success:
149
- nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
150
- return None
151
- downloaded_size = 0
152
- linked_etag = ""
153
- for each_history in response.history:
154
- linked_etag = each_history.headers.get("X-Linked-ETag", "")
155
- if linked_etag:
156
- break
157
- if not linked_etag:
158
- linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
159
- total_size = int(response.headers.get("Content-Length"))
160
- try:
161
- existing_size = os.path.getsize(result_path)
162
- except OSError:
163
- existing_size = 0
164
- if linked_etag and total_size == existing_size:
165
- with builtins.open(result_path, "rb") as file:
166
- sha256_hash = hashlib.sha256()
167
- for byte_block in iter(lambda: file.read(4096), b""):
168
- sha256_hash.update(byte_block)
169
- if f'"{sha256_hash.hexdigest()}"' == linked_etag:
170
- nc.set_init_status(min(current_progress + progress_for_task, 99))
171
- return None
172
-
173
- with builtins.open(result_path, "wb") as file:
174
- last_progress = current_progress
175
- for chunk in response.iter_raw(-1):
176
- downloaded_size += file.write(chunk)
177
- if total_size:
178
- new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
179
- if new_progress != last_progress:
180
- nc.set_init_status(new_progress)
181
- last_progress = new_progress
182
-
183
- return result_path
184
- except Exception as e: # noqa pylint: disable=broad-exception-caught
185
- nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' raised an exception: {e}")
186
-
187
- return None
208
+ with niquests.get(model_path, stream=True) as response:
209
+ if not response.ok:
210
+ raise ModelFetchError(
211
+ f"Downloading of '{model_path}' failed, returned ({response.status_code}) {response.text}"
212
+ )
213
+ downloaded_size = 0
214
+ linked_etag = ""
215
+ for each_history in response.history:
216
+ linked_etag = each_history.headers.get("X-Linked-ETag", "")
217
+ if linked_etag:
218
+ break
219
+ if not linked_etag:
220
+ linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
221
+ total_size = int(response.headers.get("Content-Length"))
222
+ try:
223
+ existing_size = os.path.getsize(result_path)
224
+ except OSError:
225
+ existing_size = 0
226
+ if linked_etag and total_size == existing_size:
227
+ with builtins.open(result_path, "rb") as file:
228
+ sha256_hash = hashlib.sha256()
229
+ for byte_block in iter(lambda: file.read(4096), b""):
230
+ sha256_hash.update(byte_block)
231
+ if f'"{sha256_hash.hexdigest()}"' == linked_etag:
232
+ nc.set_init_status(min(current_progress + progress_for_task, 99))
233
+ return result_path
234
+
235
+ with builtins.open(result_path, "wb") as file:
236
+ last_progress = current_progress
237
+ for chunk in response.iter_raw(-1):
238
+ downloaded_size += file.write(chunk)
239
+ if total_size:
240
+ new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
241
+ if new_progress != last_progress:
242
+ nc.set_init_status(new_progress)
243
+ last_progress = new_progress
244
+
245
+ return result_path
188
246
 
189
247
 
190
248
  def __fetch_model_as_snapshot(
191
- current_progress: int, progress_for_task, nc: NextcloudApp, mode_name: str, download_options: dict
249
+ current_progress: int, progress_for_task, nc: NextcloudApp, model_name: str, download_options: dict
192
250
  ) -> str:
193
251
  from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
194
252
  from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
@@ -201,7 +259,7 @@ def __fetch_model_as_snapshot(
201
259
  workers = download_options.pop("max_workers", 2)
202
260
  cache = download_options.pop("cache_dir", persistent_storage())
203
261
  return snapshot_download(
204
- mode_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache
262
+ model_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache
205
263
  )
206
264
 
207
265
 
nc_py_api/options.py CHANGED
@@ -4,6 +4,8 @@ Each setting only affects newly created instances of **Nextcloud**/**NextcloudAp
4
4
  Specifying options in **kwargs** has higher priority than this.
5
5
  """
6
6
 
7
+ # pylint: disable=invalid-name
8
+
7
9
  from os import environ
8
10
 
9
11
  from dotenv import load_dotenv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-py-api
3
- Version: 0.21.0
3
+ Version: 0.22.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/
@@ -1,13 +1,13 @@
1
- nc_py_api/__init__.py,sha256=FOju0DYizwYYzWBgID8Ebx0GeId-faNqd4Z_3K0216o,439
1
+ nc_py_api/__init__.py,sha256=S5IytGqp8PkWnmcZXRFPSVEwfeb4iZ8xPeCmSf4sTFQ,460
2
2
  nc_py_api/_deffered_error.py,sha256=BpEe_tBqflwfj2Zolb67nhW-K16XX-WbcY2IH_6u8fo,319
3
- nc_py_api/_exceptions.py,sha256=0jcbjsNnLaxaWdxXCCaO5gpcHuzIGowIwcirJMb_i6E,2324
3
+ nc_py_api/_exceptions.py,sha256=_4k-uzpKdW5_WjqVE_saoGFJ1YqWx7W6kr-wgpBume0,2414
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=Y6sDBrFJc7lk8BoDUfjC_iwOfjSbPPNPpcSxsL1fyIM,6391
7
7
  nc_py_api/_session.py,sha256=u45U4USdgbegBj3oAnm-OErFng2fAyleH1dWgshJSso,22018
8
8
  nc_py_api/_talk_api.py,sha256=0Uo7OduYniuuX3UQPb468RyGJJ-PWBCgJ5HoPuz5Qa0,51068
9
9
  nc_py_api/_theming.py,sha256=hTr3nuOemSuRFZaPy9iXNmBM7rDgQHECH43tHMWGqEY,1870
10
- nc_py_api/_version.py,sha256=nv6lCFaAcG4Ms4JWx1XWaoZkKMQx16XOvb93dfYRBFo,52
10
+ nc_py_api/_version.py,sha256=hUeDIevdzurgTsk1Uo0m8u0An4IWucrh8zxG1wW377o,52
11
11
  nc_py_api/activity.py,sha256=t9VDSnnaXRNOvALqOSGCeXSQZ-426pCOMSfQ96JHys4,9574
12
12
  nc_py_api/apps.py,sha256=Us2y2lszdxXlD8t6kxwd5_Nrrmazc0EvZXIH9O-ol80,9315
13
13
  nc_py_api/calendar_api.py,sha256=a2Q5EGf5_swWPYkUbHnoEg6h1S9KTEUQD7f7DljGHYY,1442
@@ -15,7 +15,7 @@ nc_py_api/loginflow_v2.py,sha256=QgR99Q59Q1My5U_PeLFkIAvEKhX_H7bIRrBZdddvmo4,576
15
15
  nc_py_api/nextcloud.py,sha256=cKdsw0n_yFNdI-N8IIVIQKkSqmEzJnQtLBVqo24EVdM,22849
16
16
  nc_py_api/notes.py,sha256=aM0SLVGKv8nBv_qI3z8sN08Z2721wLJUmEdwlo2EK1g,15112
17
17
  nc_py_api/notifications.py,sha256=WgzV21TuLOhLk-UEjhBSbMsIi2isa5MmAx6cbe0pc2Y,9187
18
- nc_py_api/options.py,sha256=W9RSLTtltW1W-Y3iSllRr2-VikFB5wARSiBEaAcw-AI,1719
18
+ nc_py_api/options.py,sha256=fuAiua3HiFUZYLB7X3-6L7rp3oxYOJCLWCTyeA0nUIA,1751
19
19
  nc_py_api/talk.py,sha256=OZFemYkDOaM6o4xAK3EvQbjMFiK75E5qnsCDyihIElg,29368
20
20
  nc_py_api/talk_bot.py,sha256=QXaKKwIRRcOAnqoJgfzK3ub0aDPTUmTkagls-a0XZW0,16840
21
21
  nc_py_api/user_status.py,sha256=I101nwYS8X1WvC8AnLa2f3qJUCPDPHrbq-ke0h1VT4E,13282
@@ -25,7 +25,7 @@ nc_py_api/weather_status.py,sha256=wAkjuJPjxc0Rxe4za0BzfwB0XeUmkCXoisJtTH3-qdQ,7
25
25
  nc_py_api/webhooks.py,sha256=BGHRtankgbUkcqBRJTFShjRLpaVoFNcjLsrVitoNziM,8083
26
26
  nc_py_api/ex_app/__init__.py,sha256=6Lwid4bBXOSrZf_ocf5m8qkkO1OgYxG0GTs4q6Nw72o,691
27
27
  nc_py_api/ex_app/defs.py,sha256=FaQInH3jLugKxDUqpwrXdkMT-lBxmoqWmXJXc11fa6A,727
28
- nc_py_api/ex_app/integration_fastapi.py,sha256=0ILut8galBUO7S_oq17qoU512d407ok5siHb9zwaJuQ,11037
28
+ nc_py_api/ex_app/integration_fastapi.py,sha256=BxE7oMZ5q2D5mRjTsd7k9mZEU7XH2yYp8fQvBHHUWgc,13392
29
29
  nc_py_api/ex_app/logger.py,sha256=nAHLObuPvl3UBLrlqZulgoxxVaAJ661iP4F6bTW-V-Y,1475
30
30
  nc_py_api/ex_app/misc.py,sha256=c7B0uE8isaIi4SQbxURGUuWjZaaXiLg3Ov6cqvRYplE,2298
31
31
  nc_py_api/ex_app/occ_commands.py,sha256=hb2BJuvFKIigvLycSCyAe9v6hedq4Gfu2junQZTaK_M,5219
@@ -45,8 +45,8 @@ nc_py_api/files/_files.py,sha256=t7KBZE3fHmfVk0WZuQF9usCMY_b4rzX1ct1aIZx3RYw,143
45
45
  nc_py_api/files/files.py,sha256=A05iT_s5cYMK1MtarqdjTZP9QA1l_SiINq805VLrzuY,24999
46
46
  nc_py_api/files/files_async.py,sha256=GoTPswMgozcx3Vkbj4YSitNeP7vpPJ4lIbrCnj77rP4,25858
47
47
  nc_py_api/files/sharing.py,sha256=VRZCl-TYK6dbu9rUHPs3_jcVozu1EO8bLGZwoRpiLsU,14439
48
- nc_py_api-0.21.0.dist-info/METADATA,sha256=1_bTNOf0ffu3Qt3KKuFiHgjLNncKXDKSCziG5g-Suo8,8024
49
- nc_py_api-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
- nc_py_api-0.21.0.dist-info/licenses/AUTHORS,sha256=B2Q9q9XH3PAxJp0V3GiKQc1l0z7vtGDpDHqda-ISWKM,616
51
- nc_py_api-0.21.0.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
52
- nc_py_api-0.21.0.dist-info/RECORD,,
48
+ nc_py_api-0.22.0.dist-info/METADATA,sha256=6CGpk5oqgcosb-sjxhP4RVmyHd92ratTVSZjbPaZL1E,8024
49
+ nc_py_api-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
+ nc_py_api-0.22.0.dist-info/licenses/AUTHORS,sha256=B2Q9q9XH3PAxJp0V3GiKQc1l0z7vtGDpDHqda-ISWKM,616
51
+ nc_py_api-0.22.0.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
52
+ nc_py_api-0.22.0.dist-info/RECORD,,