nc-py-api 0.11.0__py3-none-any.whl → 0.18.1__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 +1 -1
- nc_py_api/_session.py +27 -14
- nc_py_api/_version.py +1 -1
- nc_py_api/apps.py +0 -13
- nc_py_api/ex_app/__init__.py +13 -3
- nc_py_api/ex_app/defs.py +17 -29
- nc_py_api/ex_app/events_listener.py +137 -0
- nc_py_api/ex_app/integration_fastapi.py +25 -14
- nc_py_api/ex_app/logging.py +46 -0
- nc_py_api/ex_app/misc.py +6 -1
- nc_py_api/ex_app/occ_commands.py +153 -0
- nc_py_api/ex_app/providers/providers.py +7 -21
- nc_py_api/ex_app/providers/task_processing.py +261 -0
- nc_py_api/ex_app/ui/files_actions.py +45 -61
- nc_py_api/files/__init__.py +76 -6
- nc_py_api/files/_files.py +12 -0
- nc_py_api/files/files.py +26 -488
- nc_py_api/files/files_async.py +528 -0
- nc_py_api/loginflow_v2.py +161 -0
- nc_py_api/nextcloud.py +77 -21
- nc_py_api/talk_bot.py +5 -0
- nc_py_api/users.py +3 -3
- nc_py_api/webhooks.py +224 -0
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/METADATA +35 -23
- nc_py_api-0.18.1.dist-info/RECORD +53 -0
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/WHEEL +1 -1
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/AUTHORS +1 -0
- nc_py_api/ex_app/providers/speech_to_text.py +0 -128
- nc_py_api/ex_app/providers/text_processing.py +0 -135
- nc_py_api/ex_app/providers/translations.py +0 -165
- nc_py_api-0.11.0.dist-info/RECORD +0 -49
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/LICENSE.txt +0 -0
nc_py_api/__init__.py
CHANGED
|
@@ -7,6 +7,6 @@ from ._exceptions import (
|
|
|
7
7
|
NextcloudMissingCapabilities,
|
|
8
8
|
)
|
|
9
9
|
from ._version import __version__
|
|
10
|
-
from .files import FilePermissions, FsNode, LockType
|
|
10
|
+
from .files import FilePermissions, FsNode, LockType, SystemTag
|
|
11
11
|
from .files.sharing import ShareType
|
|
12
12
|
from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp
|
nc_py_api/_session.py
CHANGED
|
@@ -103,7 +103,10 @@ class Config(BasicConfig):
|
|
|
103
103
|
|
|
104
104
|
def __init__(self, **kwargs):
|
|
105
105
|
super().__init__(**kwargs)
|
|
106
|
-
|
|
106
|
+
nc_auth_user = self._get_config_value("nc_auth_user", raise_not_found=False, **kwargs)
|
|
107
|
+
nc_auth_pass = self._get_config_value("nc_auth_pass", raise_not_found=False, **kwargs)
|
|
108
|
+
if nc_auth_user and nc_auth_pass:
|
|
109
|
+
self.auth = (nc_auth_user, nc_auth_pass)
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
@dataclass
|
|
@@ -123,7 +126,7 @@ class AppConfig(BasicConfig):
|
|
|
123
126
|
super().__init__(**kwargs)
|
|
124
127
|
self.aa_version = self._get_config_value("aa_version", raise_not_found=False, **kwargs)
|
|
125
128
|
if not self.aa_version:
|
|
126
|
-
self.aa_version = "
|
|
129
|
+
self.aa_version = "2.2.0"
|
|
127
130
|
self.app_name = self._get_config_value("app_id", **kwargs)
|
|
128
131
|
self.app_version = self._get_config_value("app_version", **kwargs)
|
|
129
132
|
self.app_secret = self._get_config_value("app_secret", **kwargs)
|
|
@@ -147,7 +150,7 @@ class NcSessionBase(ABC):
|
|
|
147
150
|
self.init_adapter()
|
|
148
151
|
self.init_adapter_dav()
|
|
149
152
|
self.response_headers = Headers()
|
|
150
|
-
self._ocs_regexp = re.compile(r"/ocs/v[12]\.php/")
|
|
153
|
+
self._ocs_regexp = re.compile(r"/ocs/v[12]\.php/|/apps/groupfolders/")
|
|
151
154
|
|
|
152
155
|
def init_adapter(self, restart=False) -> None:
|
|
153
156
|
if getattr(self, "adapter", None) is None or restart:
|
|
@@ -173,9 +176,14 @@ class NcSessionBase(ABC):
|
|
|
173
176
|
|
|
174
177
|
@property
|
|
175
178
|
def ae_url(self) -> str:
|
|
176
|
-
"""Return base url for the
|
|
179
|
+
"""Return base url for the AppAPI endpoints."""
|
|
177
180
|
return "/ocs/v1.php/apps/app_api/api/v1"
|
|
178
181
|
|
|
182
|
+
@property
|
|
183
|
+
def ae_url_v2(self) -> str:
|
|
184
|
+
"""Return base url for the AppAPI endpoints(version 2)."""
|
|
185
|
+
return "/ocs/v1.php/apps/app_api/api/v2"
|
|
186
|
+
|
|
179
187
|
|
|
180
188
|
class NcSessionBasic(NcSessionBase, ABC):
|
|
181
189
|
adapter: Client
|
|
@@ -189,13 +197,16 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
189
197
|
content: bytes | str | typing.Iterable[bytes] | typing.AsyncIterable[bytes] | None = None,
|
|
190
198
|
json: dict | list | None = None,
|
|
191
199
|
params: dict | None = None,
|
|
200
|
+
files: dict | None = None,
|
|
192
201
|
**kwargs,
|
|
193
202
|
):
|
|
194
203
|
self.init_adapter()
|
|
195
204
|
info = f"request: {method} {path}"
|
|
196
205
|
nested_req = kwargs.pop("nested_req", False)
|
|
197
206
|
try:
|
|
198
|
-
response = self.adapter.request(
|
|
207
|
+
response = self.adapter.request(
|
|
208
|
+
method, path, content=content, json=json, params=params, files=files, **kwargs
|
|
209
|
+
)
|
|
199
210
|
except ReadTimeout:
|
|
200
211
|
raise NextcloudException(408, info=info) from None
|
|
201
212
|
|
|
@@ -278,6 +289,7 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
278
289
|
str_url = str(request.url)
|
|
279
290
|
if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
|
|
280
291
|
request.url = request.url.copy_merge_params({"format": "json"})
|
|
292
|
+
request.headers["Accept"] = "application/json"
|
|
281
293
|
|
|
282
294
|
def _response_event(self, response: Response) -> None:
|
|
283
295
|
str_url = str(response.request.url)
|
|
@@ -289,7 +301,7 @@ class NcSessionBasic(NcSessionBase, ABC):
|
|
|
289
301
|
|
|
290
302
|
def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
291
303
|
adapter = self.adapter_dav if dav else self.adapter
|
|
292
|
-
with adapter.stream("GET", url_path, params=params) as response:
|
|
304
|
+
with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
|
|
293
305
|
check_error(response)
|
|
294
306
|
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
295
307
|
fp.write(data_chunk)
|
|
@@ -307,13 +319,16 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
307
319
|
content: bytes | str | typing.Iterable[bytes] | typing.AsyncIterable[bytes] | None = None,
|
|
308
320
|
json: dict | list | None = None,
|
|
309
321
|
params: dict | None = None,
|
|
322
|
+
files: dict | None = None,
|
|
310
323
|
**kwargs,
|
|
311
324
|
):
|
|
312
325
|
self.init_adapter()
|
|
313
326
|
info = f"request: {method} {path}"
|
|
314
327
|
nested_req = kwargs.pop("nested_req", False)
|
|
315
328
|
try:
|
|
316
|
-
response = await self.adapter.request(
|
|
329
|
+
response = await self.adapter.request(
|
|
330
|
+
method, path, content=content, json=json, params=params, files=files, **kwargs
|
|
331
|
+
)
|
|
317
332
|
except ReadTimeout:
|
|
318
333
|
raise NextcloudException(408, info=info) from None
|
|
319
334
|
|
|
@@ -398,6 +413,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
398
413
|
str_url = str(request.url)
|
|
399
414
|
if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
|
|
400
415
|
request.url = request.url.copy_merge_params({"format": "json"})
|
|
416
|
+
request.headers["Accept"] = "application/json"
|
|
401
417
|
|
|
402
418
|
async def _response_event(self, response: Response) -> None:
|
|
403
419
|
str_url = str(response.request.url)
|
|
@@ -409,7 +425,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
|
|
|
409
425
|
|
|
410
426
|
async def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
|
|
411
427
|
adapter = self.adapter_dav if dav else self.adapter
|
|
412
|
-
async with adapter.stream("GET", url_path, params=params) as response:
|
|
428
|
+
async with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
|
|
413
429
|
check_error(response)
|
|
414
430
|
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
415
431
|
fp.write(data_chunk)
|
|
@@ -459,7 +475,7 @@ class NcSessionAppBasic(ABC):
|
|
|
459
475
|
self.cfg = AppConfig(**kwargs)
|
|
460
476
|
super().__init__(**kwargs)
|
|
461
477
|
|
|
462
|
-
def sign_check(self, request: HTTPConnection) ->
|
|
478
|
+
def sign_check(self, request: HTTPConnection) -> str:
|
|
463
479
|
headers = {
|
|
464
480
|
"AA-VERSION": request.headers.get("AA-VERSION", ""),
|
|
465
481
|
"EX-APP-ID": request.headers.get("EX-APP-ID", ""),
|
|
@@ -474,13 +490,10 @@ class NcSessionAppBasic(ABC):
|
|
|
474
490
|
if headers["EX-APP-ID"] != self.cfg.app_name:
|
|
475
491
|
raise ValueError(f"Invalid EX-APP-ID:{headers['EX-APP-ID']} != {self.cfg.app_name}")
|
|
476
492
|
|
|
477
|
-
|
|
478
|
-
if headers["EX-APP-VERSION"] != our_version:
|
|
479
|
-
raise ValueError(f"Invalid EX-APP-VERSION:{headers['EX-APP-VERSION']} <=> {our_version}")
|
|
480
|
-
|
|
481
|
-
app_secret = get_username_secret_from_headers(headers)[1]
|
|
493
|
+
username, app_secret = get_username_secret_from_headers(headers)
|
|
482
494
|
if app_secret != self.cfg.app_secret:
|
|
483
495
|
raise ValueError(f"Invalid App secret:{app_secret} != {self.cfg.app_secret}")
|
|
496
|
+
return username
|
|
484
497
|
|
|
485
498
|
|
|
486
499
|
class NcSessionApp(NcSessionAppBasic, NcSessionBasic):
|
nc_py_api/_version.py
CHANGED
nc_py_api/apps.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Nextcloud API for working with applications."""
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
-
import datetime
|
|
5
4
|
|
|
6
5
|
from ._misc import require_capabilities
|
|
7
6
|
from ._session import AsyncNcSessionBasic, NcSessionBasic
|
|
@@ -34,18 +33,6 @@ class ExAppInfo:
|
|
|
34
33
|
"""Flag indicating if the application enabled."""
|
|
35
34
|
return bool(self._raw_data["enabled"])
|
|
36
35
|
|
|
37
|
-
@property
|
|
38
|
-
def last_check_time(self) -> datetime.datetime:
|
|
39
|
-
"""Time of the last successful application check."""
|
|
40
|
-
return datetime.datetime.utcfromtimestamp(int(self._raw_data["last_check_time"])).replace(
|
|
41
|
-
tzinfo=datetime.timezone.utc
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def system(self) -> bool:
|
|
46
|
-
"""Flag indicating if the application is a system application."""
|
|
47
|
-
return bool(self._raw_data["system"])
|
|
48
|
-
|
|
49
36
|
def __repr__(self):
|
|
50
37
|
return f"<{self.__class__.__name__} id={self.app_id}, ver={self.version}>"
|
|
51
38
|
|
nc_py_api/ex_app/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""All possible ExApp stuff for NextcloudApp that can be used."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from ..files import ActionFileInfo
|
|
4
|
+
from .defs import FileSystemEventNotification, LogLvl
|
|
4
5
|
from .integration_fastapi import (
|
|
5
6
|
AppAPIAuthMiddleware,
|
|
6
7
|
anc_app,
|
|
@@ -9,7 +10,16 @@ from .integration_fastapi import (
|
|
|
9
10
|
set_handlers,
|
|
10
11
|
talk_bot_msg,
|
|
11
12
|
)
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
13
|
+
from .logging import setup_nextcloud_logging
|
|
14
|
+
from .misc import (
|
|
15
|
+
get_computation_device,
|
|
16
|
+
get_model_path,
|
|
17
|
+
persistent_storage,
|
|
18
|
+
verify_version,
|
|
19
|
+
)
|
|
14
20
|
from .ui.settings import SettingsField, SettingsFieldType, SettingsForm
|
|
15
21
|
from .uvicorn_fastapi import run_app
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UiActionFileInfo(ActionFileInfo):
|
|
25
|
+
"""``Deprecated``: use :py:class:`~nc_py_api.files.ActionFileInfo` instead."""
|
nc_py_api/ex_app/defs.py
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..files import ActionFileInfo
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
class LogLvl(enum.IntEnum):
|
|
7
11
|
"""Log levels."""
|
|
@@ -18,32 +22,16 @@ class LogLvl(enum.IntEnum):
|
|
|
18
22
|
"""Fatal log level"""
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
NOTIFICATIONS = 32
|
|
35
|
-
"""Allows access to APIs that provide Notifications."""
|
|
36
|
-
WEATHER_STATUS = 33
|
|
37
|
-
"""Allows access to APIs that provide Weather status."""
|
|
38
|
-
TALK = 50
|
|
39
|
-
"""Allows access to Talk API endpoints."""
|
|
40
|
-
TALK_BOT = 60
|
|
41
|
-
"""Allows to register Talk Bots."""
|
|
42
|
-
AI_PROVIDERS = 61
|
|
43
|
-
"""Allows to register AI providers."""
|
|
44
|
-
ACTIVITIES = 110
|
|
45
|
-
"""Activity App endpoints."""
|
|
46
|
-
NOTES = 120
|
|
47
|
-
"""Notes App endpoints."""
|
|
48
|
-
ALL = 9999
|
|
49
|
-
"""All endpoints allowed."""
|
|
25
|
+
class FileSystemEventData(BaseModel):
|
|
26
|
+
"""FileSystem events format."""
|
|
27
|
+
|
|
28
|
+
target: ActionFileInfo
|
|
29
|
+
source: ActionFileInfo | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FileSystemEventNotification(BaseModel):
|
|
33
|
+
"""AppAPI event notification common data."""
|
|
34
|
+
|
|
35
|
+
event_type: str
|
|
36
|
+
event_subtype: str
|
|
37
|
+
event_data: FileSystemEventData
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Nextcloud API for registering Events listeners for ExApps."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
|
|
5
|
+
from .._exceptions import NextcloudExceptionNotFound
|
|
6
|
+
from .._misc import require_capabilities
|
|
7
|
+
from .._session import AsyncNcSessionApp, NcSessionApp
|
|
8
|
+
|
|
9
|
+
_EP_SUFFIX: str = "events_listener"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class EventsListener:
|
|
14
|
+
"""EventsListener description."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, raw_data: dict):
|
|
17
|
+
self._raw_data = raw_data
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def event_type(self) -> str:
|
|
21
|
+
"""Main type of event, e.g. ``node_event``."""
|
|
22
|
+
return self._raw_data["event_type"]
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def event_subtypes(self) -> str:
|
|
26
|
+
"""Subtypes for which fire event, e.g. ``NodeCreatedEvent``, ``NodeDeletedEvent``."""
|
|
27
|
+
return self._raw_data["event_subtypes"]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def action_handler(self) -> str:
|
|
31
|
+
"""Relative ExApp url which will be called by Nextcloud."""
|
|
32
|
+
return self._raw_data["action_handler"]
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"<{self.__class__.__name__} event_type={self.event_type}, handler={self.action_handler}>"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EventsListenerAPI:
|
|
39
|
+
"""API for registering Events listeners, avalaible as **nc.events_handler.<method>**."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, session: NcSessionApp):
|
|
42
|
+
self._session = session
|
|
43
|
+
|
|
44
|
+
def register(
|
|
45
|
+
self,
|
|
46
|
+
event_type: str,
|
|
47
|
+
callback_url: str,
|
|
48
|
+
event_subtypes: list[str] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Registers or edits the events listener."""
|
|
51
|
+
if event_subtypes is None:
|
|
52
|
+
event_subtypes = []
|
|
53
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
54
|
+
params = {
|
|
55
|
+
"eventType": event_type,
|
|
56
|
+
"actionHandler": callback_url,
|
|
57
|
+
"eventSubtypes": event_subtypes,
|
|
58
|
+
}
|
|
59
|
+
self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
|
|
60
|
+
|
|
61
|
+
def unregister(self, event_type: str, not_fail=True) -> None:
|
|
62
|
+
"""Removes the events listener."""
|
|
63
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
64
|
+
try:
|
|
65
|
+
self._session.ocs(
|
|
66
|
+
"DELETE",
|
|
67
|
+
f"{self._session.ae_url}/{_EP_SUFFIX}",
|
|
68
|
+
params={"eventType": event_type},
|
|
69
|
+
)
|
|
70
|
+
except NextcloudExceptionNotFound as e:
|
|
71
|
+
if not not_fail:
|
|
72
|
+
raise e from None
|
|
73
|
+
|
|
74
|
+
def get_entry(self, event_type: str) -> EventsListener | None:
|
|
75
|
+
"""Get information about the event listener."""
|
|
76
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
77
|
+
try:
|
|
78
|
+
return EventsListener(
|
|
79
|
+
self._session.ocs(
|
|
80
|
+
"GET",
|
|
81
|
+
f"{self._session.ae_url}/{_EP_SUFFIX}",
|
|
82
|
+
params={"eventType": event_type},
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
except NextcloudExceptionNotFound:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AsyncEventsListenerAPI:
|
|
90
|
+
"""API for registering Events listeners, avalaible as **nc.events_handler.<method>**."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, session: AsyncNcSessionApp):
|
|
93
|
+
self._session = session
|
|
94
|
+
|
|
95
|
+
async def register(
|
|
96
|
+
self,
|
|
97
|
+
event_type: str,
|
|
98
|
+
callback_url: str,
|
|
99
|
+
event_subtypes: list[str] | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Registers or edits the events listener."""
|
|
102
|
+
if event_subtypes is None:
|
|
103
|
+
event_subtypes = []
|
|
104
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
105
|
+
params = {
|
|
106
|
+
"eventType": event_type,
|
|
107
|
+
"actionHandler": callback_url,
|
|
108
|
+
"eventSubtypes": event_subtypes,
|
|
109
|
+
}
|
|
110
|
+
await self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
|
|
111
|
+
|
|
112
|
+
async def unregister(self, event_type: str, not_fail=True) -> None:
|
|
113
|
+
"""Removes the events listener."""
|
|
114
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
115
|
+
try:
|
|
116
|
+
await self._session.ocs(
|
|
117
|
+
"DELETE",
|
|
118
|
+
f"{self._session.ae_url}/{_EP_SUFFIX}",
|
|
119
|
+
params={"eventType": event_type},
|
|
120
|
+
)
|
|
121
|
+
except NextcloudExceptionNotFound as e:
|
|
122
|
+
if not not_fail:
|
|
123
|
+
raise e from None
|
|
124
|
+
|
|
125
|
+
async def get_entry(self, event_type: str) -> EventsListener | None:
|
|
126
|
+
"""Get information about the event listener."""
|
|
127
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
128
|
+
try:
|
|
129
|
+
return EventsListener(
|
|
130
|
+
await self._session.ocs(
|
|
131
|
+
"GET",
|
|
132
|
+
f"{self._session.ae_url}/{_EP_SUFFIX}",
|
|
133
|
+
params={"eventType": event_type},
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
except NextcloudExceptionNotFound:
|
|
137
|
+
return None
|
|
@@ -103,7 +103,7 @@ def set_handlers(
|
|
|
103
103
|
|
|
104
104
|
@fast_api_app.post("/init")
|
|
105
105
|
async def init_callback(b_tasks: BackgroundTasks, nc: typing.Annotated[NextcloudApp, Depends(nc_app)]):
|
|
106
|
-
b_tasks.add_task(
|
|
106
|
+
b_tasks.add_task(fetch_models_task, nc, models_to_fetch if models_to_fetch else {}, 0)
|
|
107
107
|
return JSONResponse(content={})
|
|
108
108
|
|
|
109
109
|
if map_app_static:
|
|
@@ -120,28 +120,33 @@ def __map_app_static_folders(fast_api_app: FastAPI):
|
|
|
120
120
|
fast_api_app.mount(f"/{mnt_dir}", staticfiles.StaticFiles(directory=mnt_dir_path), name=mnt_dir)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def
|
|
123
|
+
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."""
|
|
124
125
|
if models:
|
|
125
|
-
current_progress =
|
|
126
|
-
percent_for_each = min(int(100 / len(models)), 99)
|
|
126
|
+
current_progress = progress_init_start_value
|
|
127
|
+
percent_for_each = min(int((100 - progress_init_start_value) / len(models)), 99)
|
|
127
128
|
for model in models:
|
|
128
129
|
if model.startswith(("http://", "https://")):
|
|
129
|
-
|
|
130
|
+
models[model]["path"] = __fetch_model_as_file(
|
|
131
|
+
current_progress, percent_for_each, nc, model, models[model]
|
|
132
|
+
)
|
|
130
133
|
else:
|
|
131
|
-
|
|
134
|
+
models[model]["path"] = __fetch_model_as_snapshot(
|
|
135
|
+
current_progress, percent_for_each, nc, model, models[model]
|
|
136
|
+
)
|
|
132
137
|
current_progress += percent_for_each
|
|
133
138
|
nc.set_init_status(100)
|
|
134
139
|
|
|
135
140
|
|
|
136
141
|
def __fetch_model_as_file(
|
|
137
142
|
current_progress: int, progress_for_task: int, nc: NextcloudApp, model_path: str, download_options: dict
|
|
138
|
-
) -> None:
|
|
143
|
+
) -> str | None:
|
|
139
144
|
result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
|
|
140
145
|
try:
|
|
141
146
|
with httpx.stream("GET", model_path, follow_redirects=True) as response:
|
|
142
147
|
if not response.is_success:
|
|
143
148
|
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
|
|
144
|
-
return
|
|
149
|
+
return None
|
|
145
150
|
downloaded_size = 0
|
|
146
151
|
linked_etag = ""
|
|
147
152
|
for each_history in response.history:
|
|
@@ -162,7 +167,7 @@ def __fetch_model_as_file(
|
|
|
162
167
|
sha256_hash.update(byte_block)
|
|
163
168
|
if f'"{sha256_hash.hexdigest()}"' == linked_etag:
|
|
164
169
|
nc.set_init_status(min(current_progress + progress_for_task, 99))
|
|
165
|
-
return
|
|
170
|
+
return None
|
|
166
171
|
|
|
167
172
|
with builtins.open(result_path, "wb") as file:
|
|
168
173
|
last_progress = current_progress
|
|
@@ -173,13 +178,17 @@ def __fetch_model_as_file(
|
|
|
173
178
|
if new_progress != last_progress:
|
|
174
179
|
nc.set_init_status(new_progress)
|
|
175
180
|
last_progress = new_progress
|
|
181
|
+
|
|
182
|
+
return result_path
|
|
176
183
|
except Exception as e: # noqa pylint: disable=broad-exception-caught
|
|
177
184
|
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' raised an exception: {e}")
|
|
178
185
|
|
|
186
|
+
return None
|
|
187
|
+
|
|
179
188
|
|
|
180
189
|
def __fetch_model_as_snapshot(
|
|
181
190
|
current_progress: int, progress_for_task, nc: NextcloudApp, mode_name: str, download_options: dict
|
|
182
|
-
) ->
|
|
191
|
+
) -> str:
|
|
183
192
|
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
184
193
|
from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
185
194
|
|
|
@@ -190,7 +199,9 @@ def __fetch_model_as_snapshot(
|
|
|
190
199
|
|
|
191
200
|
workers = download_options.pop("max_workers", 2)
|
|
192
201
|
cache = download_options.pop("cache_dir", persistent_storage())
|
|
193
|
-
snapshot_download(
|
|
202
|
+
return snapshot_download(
|
|
203
|
+
mode_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache
|
|
204
|
+
)
|
|
194
205
|
|
|
195
206
|
|
|
196
207
|
def __nc_app(request: HTTPConnection) -> dict:
|
|
@@ -206,9 +217,9 @@ def __request_sign_check_if_needed(request: HTTPConnection, nextcloud_app: Nextc
|
|
|
206
217
|
_request_sign_check(request, nextcloud_app)
|
|
207
218
|
|
|
208
219
|
|
|
209
|
-
def _request_sign_check(request: HTTPConnection, nextcloud_app: NextcloudApp | AsyncNextcloudApp) ->
|
|
220
|
+
def _request_sign_check(request: HTTPConnection, nextcloud_app: NextcloudApp | AsyncNextcloudApp) -> str:
|
|
210
221
|
try:
|
|
211
|
-
nextcloud_app._session.sign_check(request) # noqa pylint: disable=protected-access
|
|
222
|
+
return nextcloud_app._session.sign_check(request) # noqa pylint: disable=protected-access
|
|
212
223
|
except ValueError as e:
|
|
213
224
|
print(e)
|
|
214
225
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
|
|
@@ -238,7 +249,7 @@ class AppAPIAuthMiddleware:
|
|
|
238
249
|
url_path = conn.url.path.lstrip("/")
|
|
239
250
|
if not fnmatch.filter(self._disable_for, url_path):
|
|
240
251
|
try:
|
|
241
|
-
_request_sign_check(conn, AsyncNextcloudApp())
|
|
252
|
+
scope["username"] = _request_sign_check(conn, AsyncNextcloudApp())
|
|
242
253
|
except HTTPException as exc:
|
|
243
254
|
response = self._on_error(exc.status_code, exc.detail)
|
|
244
255
|
await response(scope, receive, send)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Transparent logging support to store logs in the nextcloud.log."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from ..nextcloud import NextcloudApp
|
|
7
|
+
from .defs import LogLvl
|
|
8
|
+
|
|
9
|
+
LOGLVL_MAP = {
|
|
10
|
+
logging.NOTSET: LogLvl.DEBUG,
|
|
11
|
+
logging.DEBUG: LogLvl.DEBUG,
|
|
12
|
+
logging.INFO: LogLvl.INFO,
|
|
13
|
+
logging.WARNING: LogLvl.WARNING,
|
|
14
|
+
logging.ERROR: LogLvl.ERROR,
|
|
15
|
+
logging.CRITICAL: LogLvl.FATAL,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
THREAD_LOCAL = threading.local()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _NextcloudLogsHandler(logging.Handler):
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
def emit(self, record):
|
|
26
|
+
if THREAD_LOCAL.__dict__.get("nc_py_api.loghandler", False):
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = True
|
|
31
|
+
log_entry = self.format(record)
|
|
32
|
+
log_level = record.levelno
|
|
33
|
+
NextcloudApp().log(LOGLVL_MAP.get(log_level, LogLvl.FATAL), log_entry, fast_send=True)
|
|
34
|
+
except Exception: # noqa pylint: disable=broad-exception-caught
|
|
35
|
+
self.handleError(record)
|
|
36
|
+
finally:
|
|
37
|
+
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def setup_nextcloud_logging(logger_name: str | None = None, logging_level: int = logging.DEBUG):
|
|
41
|
+
"""Function to easily send all or selected log entries to Nextcloud."""
|
|
42
|
+
logger = logging.getLogger(logger_name)
|
|
43
|
+
nextcloud_handler = _NextcloudLogsHandler()
|
|
44
|
+
nextcloud_handler.setLevel(logging_level)
|
|
45
|
+
logger.addHandler(nextcloud_handler)
|
|
46
|
+
return nextcloud_handler
|
nc_py_api/ex_app/misc.py
CHANGED
|
@@ -33,7 +33,7 @@ def verify_version(finalize_update: bool = True) -> tuple[str, str] | None:
|
|
|
33
33
|
"""
|
|
34
34
|
version_file_path = os.path.join(persistent_storage(), "_version.info")
|
|
35
35
|
r = None
|
|
36
|
-
with open(version_file_path, "a+
|
|
36
|
+
with open(version_file_path, "a+", encoding="UTF-8") as version_file:
|
|
37
37
|
version_file.seek(0)
|
|
38
38
|
old_version = version_file.read()
|
|
39
39
|
if old_version != os.environ["APP_VERSION"]:
|
|
@@ -50,3 +50,8 @@ def get_model_path(model_name: str) -> str:
|
|
|
50
50
|
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
51
51
|
|
|
52
52
|
return snapshot_download(model_name, local_files_only=True, cache_dir=persistent_storage())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_computation_device() -> str:
|
|
56
|
+
"""Returns computation device(`ROCM` or `CUDA`) if it is defined in the environment variable."""
|
|
57
|
+
return str(os.environ.get("COMPUTE_DEVICE", "")).upper()
|