nc-py-api 0.9.0__py3-none-any.whl → 0.11.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 +1 -1
- nc_py_api/_preferences_ex.py +2 -2
- nc_py_api/_talk_api.py +1 -1
- nc_py_api/_version.py +1 -1
- nc_py_api/ex_app/__init__.py +1 -0
- nc_py_api/ex_app/defs.py +1 -1
- nc_py_api/ex_app/integration_fastapi.py +120 -89
- nc_py_api/ex_app/providers/speech_to_text.py +2 -2
- nc_py_api/ex_app/providers/text_processing.py +2 -2
- nc_py_api/ex_app/providers/translations.py +11 -2
- nc_py_api/ex_app/ui/files_actions.py +1 -1
- nc_py_api/ex_app/ui/resources.py +1 -1
- nc_py_api/ex_app/ui/settings.py +178 -0
- nc_py_api/ex_app/ui/top_menu.py +1 -1
- nc_py_api/ex_app/ui/ui.py +7 -0
- nc_py_api/files/__init__.py +61 -0
- nc_py_api/files/_files.py +32 -10
- nc_py_api/files/files.py +65 -8
- nc_py_api/files/sharing.py +1 -1
- nc_py_api/nextcloud.py +1 -48
- nc_py_api/user_status.py +1 -1
- {nc_py_api-0.9.0.dist-info → nc_py_api-0.11.0.dist-info}/METADATA +9 -7
- nc_py_api-0.11.0.dist-info/RECORD +49 -0
- nc_py_api-0.9.0.dist-info/RECORD +0 -48
- {nc_py_api-0.9.0.dist-info → nc_py_api-0.11.0.dist-info}/WHEEL +0 -0
- {nc_py_api-0.9.0.dist-info → nc_py_api-0.11.0.dist-info}/licenses/AUTHORS +0 -0
- {nc_py_api-0.9.0.dist-info → nc_py_api-0.11.0.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
|
|
10
|
+
from .files import FilePermissions, FsNode, LockType
|
|
11
11
|
from .files.sharing import ShareType
|
|
12
12
|
from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp
|
nc_py_api/_preferences_ex.py
CHANGED
|
@@ -106,7 +106,7 @@ class _AsyncBasicAppCfgPref:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
class PreferencesExAPI(_BasicAppCfgPref):
|
|
109
|
-
"""User specific preferences API."""
|
|
109
|
+
"""User specific preferences API, avalaible as **nc.preferences_ex.<method>**."""
|
|
110
110
|
|
|
111
111
|
_url_suffix = "ex-app/preference"
|
|
112
112
|
|
|
@@ -134,7 +134,7 @@ class AsyncPreferencesExAPI(_AsyncBasicAppCfgPref):
|
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
class AppConfigExAPI(_BasicAppCfgPref):
|
|
137
|
-
"""Non-user(App) specific preferences API."""
|
|
137
|
+
"""Non-user(App) specific preferences API, avalaible as **nc.appconfig_ex.<method>**."""
|
|
138
138
|
|
|
139
139
|
_url_suffix = "ex-app/config"
|
|
140
140
|
|
nc_py_api/_talk_api.py
CHANGED
|
@@ -26,7 +26,7 @@ from .talk import (
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class _TalkAPI:
|
|
29
|
-
"""Class provides API to work with Nextcloud Talk."""
|
|
29
|
+
"""Class provides API to work with Nextcloud Talk, avalaible as **nc.talk.<method>**."""
|
|
30
30
|
|
|
31
31
|
_ep_base: str = "/ocs/v2.php/apps/spreed"
|
|
32
32
|
config_sha: str
|
nc_py_api/_version.py
CHANGED
nc_py_api/ex_app/__init__.py
CHANGED
|
@@ -11,4 +11,5 @@ from .integration_fastapi import (
|
|
|
11
11
|
)
|
|
12
12
|
from .misc import get_model_path, persistent_storage, verify_version
|
|
13
13
|
from .ui.files_actions import UiActionFileInfo
|
|
14
|
+
from .ui.settings import SettingsField, SettingsFieldType, SettingsForm
|
|
14
15
|
from .uvicorn_fastapi import run_app
|
nc_py_api/ex_app/defs.py
CHANGED
|
@@ -19,7 +19,7 @@ class LogLvl(enum.IntEnum):
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ApiScope(enum.IntEnum):
|
|
22
|
-
"""
|
|
22
|
+
"""Defined API scopes."""
|
|
23
23
|
|
|
24
24
|
SYSTEM = 2
|
|
25
25
|
"""Allows access to the System APIs."""
|
|
@@ -1,49 +1,45 @@
|
|
|
1
1
|
"""FastAPI directly related stuff."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import builtins
|
|
5
|
+
import fnmatch
|
|
6
|
+
import hashlib
|
|
4
7
|
import json
|
|
5
8
|
import os
|
|
6
9
|
import typing
|
|
10
|
+
from urllib.parse import urlparse
|
|
7
11
|
|
|
12
|
+
import httpx
|
|
8
13
|
from fastapi import (
|
|
9
14
|
BackgroundTasks,
|
|
10
15
|
Depends,
|
|
11
16
|
FastAPI,
|
|
12
17
|
HTTPException,
|
|
13
|
-
responses,
|
|
14
18
|
staticfiles,
|
|
15
19
|
status,
|
|
16
20
|
)
|
|
21
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
17
22
|
from starlette.requests import HTTPConnection, Request
|
|
18
23
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
19
24
|
|
|
20
25
|
from .._misc import get_username_secret_from_headers
|
|
21
26
|
from ..nextcloud import AsyncNextcloudApp, NextcloudApp
|
|
22
27
|
from ..talk_bot import TalkBotMessage
|
|
28
|
+
from .defs import LogLvl
|
|
23
29
|
from .misc import persistent_storage
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def nc_app(request: HTTPConnection) -> NextcloudApp:
|
|
27
33
|
"""Authentication handler for requests from Nextcloud to the application."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)[0]
|
|
31
|
-
request_id = request.headers.get("AA-REQUEST-ID", None)
|
|
32
|
-
nextcloud_app = NextcloudApp(user=user, headers={"AA-REQUEST-ID": request_id} if request_id else {})
|
|
33
|
-
if not nextcloud_app.request_sign_check(request):
|
|
34
|
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
34
|
+
nextcloud_app = NextcloudApp(**__nc_app(request))
|
|
35
|
+
__request_sign_check_if_needed(request, nextcloud_app)
|
|
35
36
|
return nextcloud_app
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def anc_app(request: HTTPConnection) -> AsyncNextcloudApp:
|
|
39
40
|
"""Async Authentication handler for requests from Nextcloud to the application."""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)[0]
|
|
43
|
-
request_id = request.headers.get("AA-REQUEST-ID", None)
|
|
44
|
-
nextcloud_app = AsyncNextcloudApp(user=user, headers={"AA-REQUEST-ID": request_id} if request_id else {})
|
|
45
|
-
if not nextcloud_app.request_sign_check(request):
|
|
46
|
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
41
|
+
nextcloud_app = AsyncNextcloudApp(**__nc_app(request))
|
|
42
|
+
__request_sign_check_if_needed(request, nextcloud_app)
|
|
47
43
|
return nextcloud_app
|
|
48
44
|
|
|
49
45
|
|
|
@@ -60,8 +56,8 @@ async def atalk_bot_msg(request: Request) -> TalkBotMessage:
|
|
|
60
56
|
def set_handlers(
|
|
61
57
|
fast_api_app: FastAPI,
|
|
62
58
|
enabled_handler: typing.Callable[[bool, AsyncNextcloudApp | NextcloudApp], typing.Awaitable[str] | str],
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
default_heartbeat: bool = True,
|
|
60
|
+
default_init: bool = True,
|
|
65
61
|
models_to_fetch: dict[str, dict] | None = None,
|
|
66
62
|
map_app_static: bool = True,
|
|
67
63
|
):
|
|
@@ -69,85 +65,46 @@ def set_handlers(
|
|
|
69
65
|
|
|
70
66
|
:param fast_api_app: FastAPI() call return value.
|
|
71
67
|
:param enabled_handler: ``Required``, callback which will be called for `enabling`/`disabling` app event.
|
|
72
|
-
:param
|
|
73
|
-
:param
|
|
68
|
+
:param default_heartbeat: Set to ``False`` to disable the default `heartbeat` route handler.
|
|
69
|
+
:param default_init: Set to ``False`` to disable the default `init` route handler.
|
|
74
70
|
|
|
75
|
-
.. note::
|
|
71
|
+
.. note:: When this parameter is ``False``, the provision of ``models_to_fetch`` is not allowed.
|
|
76
72
|
|
|
77
73
|
:param models_to_fetch: Dictionary describing which models should be downloaded during `init`.
|
|
78
74
|
|
|
79
|
-
.. note::
|
|
75
|
+
.. note:: ``huggingface_hub`` package should be present for automatic models fetching.
|
|
80
76
|
|
|
81
77
|
:param map_app_static: Should be folders ``js``, ``css``, ``l10n``, ``img`` automatically mounted in FastAPI or not.
|
|
82
78
|
|
|
83
79
|
.. note:: First, presence of these directories in the current working dir is checked, then one directory higher.
|
|
84
80
|
"""
|
|
85
|
-
if models_to_fetch is not None and
|
|
86
|
-
raise ValueError("
|
|
81
|
+
if models_to_fetch is not None and default_init is False:
|
|
82
|
+
raise ValueError("`models_to_fetch` can be defined only with `default_init`=True.")
|
|
87
83
|
|
|
88
84
|
if asyncio.iscoroutinefunction(enabled_handler):
|
|
89
85
|
|
|
90
86
|
@fast_api_app.put("/enabled")
|
|
91
87
|
async def enabled_callback(enabled: bool, nc: typing.Annotated[AsyncNextcloudApp, Depends(anc_app)]):
|
|
92
|
-
return
|
|
88
|
+
return JSONResponse(content={"error": await enabled_handler(enabled, nc)})
|
|
93
89
|
|
|
94
90
|
else:
|
|
95
91
|
|
|
96
92
|
@fast_api_app.put("/enabled")
|
|
97
93
|
def enabled_callback(enabled: bool, nc: typing.Annotated[NextcloudApp, Depends(nc_app)]):
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
if heartbeat_handler is None:
|
|
101
|
-
|
|
102
|
-
@fast_api_app.get("/heartbeat")
|
|
103
|
-
async def heartbeat_callback():
|
|
104
|
-
return responses.JSONResponse(content={"status": "ok"}, status_code=200)
|
|
94
|
+
return JSONResponse(content={"error": enabled_handler(enabled, nc)})
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
if default_heartbeat:
|
|
107
97
|
|
|
108
98
|
@fast_api_app.get("/heartbeat")
|
|
109
99
|
async def heartbeat_callback():
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
else:
|
|
113
|
-
|
|
114
|
-
@fast_api_app.get("/heartbeat")
|
|
115
|
-
def heartbeat_callback():
|
|
116
|
-
return responses.JSONResponse(content={"status": heartbeat_handler()}, status_code=200)
|
|
117
|
-
|
|
118
|
-
if init_handler is None:
|
|
100
|
+
return JSONResponse(content={"status": "ok"})
|
|
119
101
|
|
|
120
|
-
|
|
121
|
-
async def init_callback(
|
|
122
|
-
background_tasks: BackgroundTasks,
|
|
123
|
-
nc: typing.Annotated[NextcloudApp, Depends(nc_app)],
|
|
124
|
-
):
|
|
125
|
-
background_tasks.add_task(
|
|
126
|
-
__fetch_models_task,
|
|
127
|
-
nc,
|
|
128
|
-
models_to_fetch if models_to_fetch else {},
|
|
129
|
-
)
|
|
130
|
-
return responses.JSONResponse(content={}, status_code=200)
|
|
131
|
-
|
|
132
|
-
elif asyncio.iscoroutinefunction(init_handler):
|
|
133
|
-
|
|
134
|
-
@fast_api_app.post("/init")
|
|
135
|
-
async def init_callback(
|
|
136
|
-
background_tasks: BackgroundTasks,
|
|
137
|
-
nc: typing.Annotated[AsyncNextcloudApp, Depends(anc_app)],
|
|
138
|
-
):
|
|
139
|
-
background_tasks.add_task(init_handler, nc)
|
|
140
|
-
return responses.JSONResponse(content={}, status_code=200)
|
|
141
|
-
|
|
142
|
-
else:
|
|
102
|
+
if default_init:
|
|
143
103
|
|
|
144
104
|
@fast_api_app.post("/init")
|
|
145
|
-
def init_callback(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
):
|
|
149
|
-
background_tasks.add_task(init_handler, nc)
|
|
150
|
-
return responses.JSONResponse(content={}, status_code=200)
|
|
105
|
+
async def init_callback(b_tasks: BackgroundTasks, nc: typing.Annotated[NextcloudApp, Depends(nc_app)]):
|
|
106
|
+
b_tasks.add_task(__fetch_models_task, nc, models_to_fetch if models_to_fetch else {})
|
|
107
|
+
return JSONResponse(content={})
|
|
151
108
|
|
|
152
109
|
if map_app_static:
|
|
153
110
|
__map_app_static_folders(fast_api_app)
|
|
@@ -163,26 +120,100 @@ def __map_app_static_folders(fast_api_app: FastAPI):
|
|
|
163
120
|
fast_api_app.mount(f"/{mnt_dir}", staticfiles.StaticFiles(directory=mnt_dir_path), name=mnt_dir)
|
|
164
121
|
|
|
165
122
|
|
|
166
|
-
def __fetch_models_task(
|
|
167
|
-
nc: NextcloudApp,
|
|
168
|
-
models: dict[str, dict],
|
|
169
|
-
) -> None:
|
|
123
|
+
def __fetch_models_task(nc: NextcloudApp, models: dict[str, dict]) -> None:
|
|
170
124
|
if models:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
class TqdmProgress(tqdm):
|
|
175
|
-
def display(self, msg=None, pos=None):
|
|
176
|
-
nc.set_init_status(min(int((self.n * 100 / self.total) / len(models)), 100))
|
|
177
|
-
return super().display(msg, pos)
|
|
178
|
-
|
|
125
|
+
current_progress = 0
|
|
126
|
+
percent_for_each = min(int(100 / len(models)), 99)
|
|
179
127
|
for model in models:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
128
|
+
if model.startswith(("http://", "https://")):
|
|
129
|
+
__fetch_model_as_file(current_progress, percent_for_each, nc, model, models[model])
|
|
130
|
+
else:
|
|
131
|
+
__fetch_model_as_snapshot(current_progress, percent_for_each, nc, model, models[model])
|
|
132
|
+
current_progress += percent_for_each
|
|
183
133
|
nc.set_init_status(100)
|
|
184
134
|
|
|
185
135
|
|
|
136
|
+
def __fetch_model_as_file(
|
|
137
|
+
current_progress: int, progress_for_task: int, nc: NextcloudApp, model_path: str, download_options: dict
|
|
138
|
+
) -> None:
|
|
139
|
+
result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
|
|
140
|
+
try:
|
|
141
|
+
with httpx.stream("GET", model_path, follow_redirects=True) as response:
|
|
142
|
+
if not response.is_success:
|
|
143
|
+
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
|
|
144
|
+
return
|
|
145
|
+
downloaded_size = 0
|
|
146
|
+
linked_etag = ""
|
|
147
|
+
for each_history in response.history:
|
|
148
|
+
linked_etag = each_history.headers.get("X-Linked-ETag", "")
|
|
149
|
+
if linked_etag:
|
|
150
|
+
break
|
|
151
|
+
if not linked_etag:
|
|
152
|
+
linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
|
|
153
|
+
total_size = int(response.headers.get("Content-Length"))
|
|
154
|
+
try:
|
|
155
|
+
existing_size = os.path.getsize(result_path)
|
|
156
|
+
except OSError:
|
|
157
|
+
existing_size = 0
|
|
158
|
+
if linked_etag and total_size == existing_size:
|
|
159
|
+
with builtins.open(result_path, "rb") as file:
|
|
160
|
+
sha256_hash = hashlib.sha256()
|
|
161
|
+
for byte_block in iter(lambda: file.read(4096), b""):
|
|
162
|
+
sha256_hash.update(byte_block)
|
|
163
|
+
if f'"{sha256_hash.hexdigest()}"' == linked_etag:
|
|
164
|
+
nc.set_init_status(min(current_progress + progress_for_task, 99))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
with builtins.open(result_path, "wb") as file:
|
|
168
|
+
last_progress = current_progress
|
|
169
|
+
for chunk in response.iter_bytes(5 * 1024 * 1024):
|
|
170
|
+
downloaded_size += file.write(chunk)
|
|
171
|
+
if total_size:
|
|
172
|
+
new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
|
|
173
|
+
if new_progress != last_progress:
|
|
174
|
+
nc.set_init_status(new_progress)
|
|
175
|
+
last_progress = new_progress
|
|
176
|
+
except Exception as e: # noqa pylint: disable=broad-exception-caught
|
|
177
|
+
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' raised an exception: {e}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def __fetch_model_as_snapshot(
|
|
181
|
+
current_progress: int, progress_for_task, nc: NextcloudApp, mode_name: str, download_options: dict
|
|
182
|
+
) -> None:
|
|
183
|
+
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
184
|
+
from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
|
|
185
|
+
|
|
186
|
+
class TqdmProgress(tqdm):
|
|
187
|
+
def display(self, msg=None, pos=None):
|
|
188
|
+
nc.set_init_status(min(current_progress + int(progress_for_task * self.n / self.total), 99))
|
|
189
|
+
return super().display(msg, pos)
|
|
190
|
+
|
|
191
|
+
workers = download_options.pop("max_workers", 2)
|
|
192
|
+
cache = download_options.pop("cache_dir", persistent_storage())
|
|
193
|
+
snapshot_download(mode_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def __nc_app(request: HTTPConnection) -> dict:
|
|
197
|
+
user = get_username_secret_from_headers(
|
|
198
|
+
{"AUTHORIZATION-APP-API": request.headers.get("AUTHORIZATION-APP-API", "")}
|
|
199
|
+
)[0]
|
|
200
|
+
request_id = request.headers.get("AA-REQUEST-ID", None)
|
|
201
|
+
return {"user": user, "headers": {"AA-REQUEST-ID": request_id} if request_id else {}}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def __request_sign_check_if_needed(request: HTTPConnection, nextcloud_app: NextcloudApp | AsyncNextcloudApp) -> None:
|
|
205
|
+
if not [i for i in getattr(request.app, "user_middleware", []) if i.cls == AppAPIAuthMiddleware]:
|
|
206
|
+
_request_sign_check(request, nextcloud_app)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _request_sign_check(request: HTTPConnection, nextcloud_app: NextcloudApp | AsyncNextcloudApp) -> None:
|
|
210
|
+
try:
|
|
211
|
+
nextcloud_app._session.sign_check(request) # noqa pylint: disable=protected-access
|
|
212
|
+
except ValueError as e:
|
|
213
|
+
print(e)
|
|
214
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
|
|
215
|
+
|
|
216
|
+
|
|
186
217
|
class AppAPIAuthMiddleware:
|
|
187
218
|
"""Pure ASGI AppAPIAuth Middleware."""
|
|
188
219
|
|
|
@@ -205,9 +236,9 @@ class AppAPIAuthMiddleware:
|
|
|
205
236
|
|
|
206
237
|
conn = HTTPConnection(scope)
|
|
207
238
|
url_path = conn.url.path.lstrip("/")
|
|
208
|
-
if
|
|
239
|
+
if not fnmatch.filter(self._disable_for, url_path):
|
|
209
240
|
try:
|
|
210
|
-
|
|
241
|
+
_request_sign_check(conn, AsyncNextcloudApp())
|
|
211
242
|
except HTTPException as exc:
|
|
212
243
|
response = self._on_error(exc.status_code, exc.detail)
|
|
213
244
|
await response(scope, receive, send)
|
|
@@ -216,5 +247,5 @@ class AppAPIAuthMiddleware:
|
|
|
216
247
|
await self.app(scope, receive, send)
|
|
217
248
|
|
|
218
249
|
@staticmethod
|
|
219
|
-
def _on_error(status_code: int = 400, content: str = "") ->
|
|
220
|
-
return
|
|
250
|
+
def _on_error(status_code: int = 400, content: str = "") -> PlainTextResponse:
|
|
251
|
+
return PlainTextResponse(content, status_code=status_code)
|
|
@@ -37,7 +37,7 @@ class SpeechToTextProvider:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class _SpeechToTextProviderAPI:
|
|
40
|
-
"""API for Speech2Text providers."""
|
|
40
|
+
"""API for Speech2Text providers, avalaible as **nc.providers.text_processing.<method>**."""
|
|
41
41
|
|
|
42
42
|
def __init__(self, session: NcSessionApp):
|
|
43
43
|
self._session = session
|
|
@@ -83,7 +83,7 @@ class _SpeechToTextProviderAPI:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class _AsyncSpeechToTextProviderAPI:
|
|
86
|
-
"""API for Speech2Text providers."""
|
|
86
|
+
"""Async API for Speech2Text providers."""
|
|
87
87
|
|
|
88
88
|
def __init__(self, session: AsyncNcSessionApp):
|
|
89
89
|
self._session = session
|
|
@@ -42,7 +42,7 @@ class TextProcessingProvider:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class _TextProcessingProviderAPI:
|
|
45
|
-
"""API for TextProcessing providers."""
|
|
45
|
+
"""API for TextProcessing providers, avalaible as **nc.providers.speech_to_text.<method>**."""
|
|
46
46
|
|
|
47
47
|
def __init__(self, session: NcSessionApp):
|
|
48
48
|
self._session = session
|
|
@@ -89,7 +89,7 @@ class _TextProcessingProviderAPI:
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
class _AsyncTextProcessingProviderAPI:
|
|
92
|
-
"""API for TextProcessing providers."""
|
|
92
|
+
"""Async API for TextProcessing providers."""
|
|
93
93
|
|
|
94
94
|
def __init__(self, session: AsyncNcSessionApp):
|
|
95
95
|
self._session = session
|
|
@@ -42,12 +42,17 @@ class TranslationsProvider:
|
|
|
42
42
|
"""Relative ExApp url which will be called by Nextcloud."""
|
|
43
43
|
return self._raw_data["action_handler"]
|
|
44
44
|
|
|
45
|
+
@property
|
|
46
|
+
def action_handler_detect_lang(self) -> str:
|
|
47
|
+
"""Relative ExApp url which will be called by Nextcloud to detect language."""
|
|
48
|
+
return self._raw_data.get("action_detect_lang", "")
|
|
49
|
+
|
|
45
50
|
def __repr__(self):
|
|
46
51
|
return f"<{self.__class__.__name__} name={self.name}, handler={self.action_handler}>"
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
class _TranslationsProviderAPI:
|
|
50
|
-
"""API for Translations providers."""
|
|
55
|
+
"""API for Translations providers, avalaible as **nc.providers.translations.<method>**."""
|
|
51
56
|
|
|
52
57
|
def __init__(self, session: NcSessionApp):
|
|
53
58
|
self._session = session
|
|
@@ -59,6 +64,7 @@ class _TranslationsProviderAPI:
|
|
|
59
64
|
callback_url: str,
|
|
60
65
|
from_languages: dict[str, str],
|
|
61
66
|
to_languages: dict[str, str],
|
|
67
|
+
detect_lang_callback_url: str = "",
|
|
62
68
|
) -> None:
|
|
63
69
|
"""Registers or edit the Translations provider."""
|
|
64
70
|
require_capabilities("app_api", self._session.capabilities)
|
|
@@ -68,6 +74,7 @@ class _TranslationsProviderAPI:
|
|
|
68
74
|
"fromLanguages": from_languages,
|
|
69
75
|
"toLanguages": to_languages,
|
|
70
76
|
"actionHandler": callback_url,
|
|
77
|
+
"actionDetectLang": detect_lang_callback_url,
|
|
71
78
|
}
|
|
72
79
|
self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
|
|
73
80
|
|
|
@@ -102,7 +109,7 @@ class _TranslationsProviderAPI:
|
|
|
102
109
|
|
|
103
110
|
|
|
104
111
|
class _AsyncTranslationsProviderAPI:
|
|
105
|
-
"""API for Translations providers."""
|
|
112
|
+
"""Async API for Translations providers."""
|
|
106
113
|
|
|
107
114
|
def __init__(self, session: AsyncNcSessionApp):
|
|
108
115
|
self._session = session
|
|
@@ -114,6 +121,7 @@ class _AsyncTranslationsProviderAPI:
|
|
|
114
121
|
callback_url: str,
|
|
115
122
|
from_languages: dict[str, str],
|
|
116
123
|
to_languages: dict[str, str],
|
|
124
|
+
detect_lang_callback_url: str = "",
|
|
117
125
|
) -> None:
|
|
118
126
|
"""Registers or edit the Translations provider."""
|
|
119
127
|
require_capabilities("app_api", await self._session.capabilities)
|
|
@@ -123,6 +131,7 @@ class _AsyncTranslationsProviderAPI:
|
|
|
123
131
|
"fromLanguages": from_languages,
|
|
124
132
|
"toLanguages": to_languages,
|
|
125
133
|
"actionHandler": callback_url,
|
|
134
|
+
"actionDetectLang": detect_lang_callback_url,
|
|
126
135
|
}
|
|
127
136
|
await self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=params)
|
|
128
137
|
|
|
@@ -119,7 +119,7 @@ class UiActionFileInfo(BaseModel):
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
class _UiFilesActionsAPI:
|
|
122
|
-
"""API for the drop-down menu in Nextcloud **Files app
|
|
122
|
+
"""API for the drop-down menu in Nextcloud **Files app**, avalaible as **nc.ui.files_dropdown_menu.<method>**."""
|
|
123
123
|
|
|
124
124
|
_ep_suffix: str = "ui/files-actions-menu"
|
|
125
125
|
|
nc_py_api/ex_app/ui/resources.py
CHANGED
|
@@ -77,7 +77,7 @@ class UiStyle(UiBase):
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
class _UiResources:
|
|
80
|
-
"""API for adding scripts, styles, initial-states to the
|
|
80
|
+
"""API for adding scripts, styles, initial-states to the pages, avalaible as **nc.ui.resources.<method>**."""
|
|
81
81
|
|
|
82
82
|
_ep_suffix_init_state: str = "ui/initial-state"
|
|
83
83
|
_ep_suffix_js: str = "ui/script"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Nextcloud API for declaring UI for settings."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from ..._exceptions import NextcloudExceptionNotFound
|
|
8
|
+
from ..._misc import require_capabilities
|
|
9
|
+
from ..._session import AsyncNcSessionApp, NcSessionApp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SettingsFieldType(enum.Enum): # StrEnum
|
|
13
|
+
"""Declarative Settings Field Type."""
|
|
14
|
+
|
|
15
|
+
TEXT = "text"
|
|
16
|
+
"""NcInputField type text"""
|
|
17
|
+
PASSWORD = "password" # noqa
|
|
18
|
+
"""NcInputField type password"""
|
|
19
|
+
EMAIL = "email"
|
|
20
|
+
"""NcInputField type email"""
|
|
21
|
+
TEL = "tel"
|
|
22
|
+
"""NcInputField type tel"""
|
|
23
|
+
URL = "url"
|
|
24
|
+
"""NcInputField type url"""
|
|
25
|
+
NUMBER = "number"
|
|
26
|
+
"""NcInputField type number"""
|
|
27
|
+
CHECKBOX = "checkbox"
|
|
28
|
+
"""NcCheckboxRadioSwitch type checkbox"""
|
|
29
|
+
MULTI_CHECKBOX = "multi-checkbox"
|
|
30
|
+
"""Multiple NcCheckboxRadioSwitch type checkbox representing a one config value (saved as JSON object)"""
|
|
31
|
+
RADIO = "radio"
|
|
32
|
+
"""NcCheckboxRadioSwitch type radio"""
|
|
33
|
+
SELECT = "select"
|
|
34
|
+
"""NcSelect"""
|
|
35
|
+
MULTI_SELECT = "multi-select"
|
|
36
|
+
"""Multiple NcSelect representing a one config value (saved as JSON array)"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass
|
|
40
|
+
class SettingsField:
|
|
41
|
+
"""Section field."""
|
|
42
|
+
|
|
43
|
+
id: str
|
|
44
|
+
title: str
|
|
45
|
+
type: SettingsFieldType
|
|
46
|
+
default: bool | int | float | str | list[bool | int | float | str] | dict[str, typing.Any]
|
|
47
|
+
options: dict | list = dataclasses.field(default_factory=dict)
|
|
48
|
+
description: str = ""
|
|
49
|
+
placeholder: str = ""
|
|
50
|
+
label: str = ""
|
|
51
|
+
notify = False # to be supported in future
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict) -> "SettingsField":
|
|
55
|
+
"""Creates instance of class from dict, ignoring unknown keys."""
|
|
56
|
+
filtered_data = {
|
|
57
|
+
k: SettingsFieldType(v) if k == "type" else v for k, v in data.items() if k in cls.__annotations__
|
|
58
|
+
}
|
|
59
|
+
return cls(**filtered_data)
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
"""Returns data in format that is accepted by AppAPI."""
|
|
63
|
+
return {
|
|
64
|
+
"id": self.id,
|
|
65
|
+
"title": self.title,
|
|
66
|
+
"type": self.type.value,
|
|
67
|
+
"default": self.default,
|
|
68
|
+
"description": self.description,
|
|
69
|
+
"options": (
|
|
70
|
+
[{"name": key, "value": value} for key, value in self.options.items()]
|
|
71
|
+
if isinstance(self.options, dict)
|
|
72
|
+
else self.options
|
|
73
|
+
),
|
|
74
|
+
"placeholder": self.placeholder,
|
|
75
|
+
"label": self.label,
|
|
76
|
+
"notify": self.notify,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclasses.dataclass
|
|
81
|
+
class SettingsForm:
|
|
82
|
+
"""Settings Form and Section."""
|
|
83
|
+
|
|
84
|
+
id: str
|
|
85
|
+
section_id: str
|
|
86
|
+
title: str
|
|
87
|
+
fields: list[SettingsField] = dataclasses.field(default_factory=list)
|
|
88
|
+
description: str = ""
|
|
89
|
+
priority: int = 50
|
|
90
|
+
doc_url: str = ""
|
|
91
|
+
section_type: str = "personal"
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict) -> "SettingsForm":
|
|
95
|
+
"""Creates instance of class from dict, ignoring unknown keys."""
|
|
96
|
+
filtered_data = {k: v for k, v in data.items() if k in cls.__annotations__}
|
|
97
|
+
filtered_data["fields"] = [SettingsField.from_dict(i) for i in filtered_data.get("fields", [])]
|
|
98
|
+
return cls(**filtered_data)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
"""Returns data in format that is accepted by AppAPI."""
|
|
102
|
+
return {
|
|
103
|
+
"id": self.id,
|
|
104
|
+
"priority": self.priority,
|
|
105
|
+
"section_type": self.section_type,
|
|
106
|
+
"section_id": self.section_id,
|
|
107
|
+
"title": self.title,
|
|
108
|
+
"description": self.description,
|
|
109
|
+
"doc_url": self.doc_url,
|
|
110
|
+
"fields": [i.to_dict() for i in self.fields],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_EP_SUFFIX: str = "ui/settings"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _DeclarativeSettingsAPI:
|
|
118
|
+
"""Class providing API for creating UI for the ExApp settings, avalaible as **nc.ui.settings.<method>**."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, session: NcSessionApp):
|
|
121
|
+
self._session = session
|
|
122
|
+
|
|
123
|
+
def register_form(self, form_schema: SettingsForm | dict[str, typing.Any]) -> None:
|
|
124
|
+
"""Registers or edit the Settings UI Form."""
|
|
125
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
126
|
+
param = {"formScheme": form_schema.to_dict() if isinstance(form_schema, SettingsForm) else form_schema}
|
|
127
|
+
self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=param)
|
|
128
|
+
|
|
129
|
+
def unregister_form(self, form_id: str, not_fail=True) -> None:
|
|
130
|
+
"""Removes Settings UI Form."""
|
|
131
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
132
|
+
try:
|
|
133
|
+
self._session.ocs("DELETE", f"{self._session.ae_url}/{_EP_SUFFIX}", params={"formId": form_id})
|
|
134
|
+
except NextcloudExceptionNotFound as e:
|
|
135
|
+
if not not_fail:
|
|
136
|
+
raise e from None
|
|
137
|
+
|
|
138
|
+
def get_entry(self, form_id: str) -> SettingsForm | None:
|
|
139
|
+
"""Get information of the Settings UI Form."""
|
|
140
|
+
require_capabilities("app_api", self._session.capabilities)
|
|
141
|
+
try:
|
|
142
|
+
return SettingsForm.from_dict(
|
|
143
|
+
self._session.ocs("GET", f"{self._session.ae_url}/{_EP_SUFFIX}", params={"formId": form_id})
|
|
144
|
+
)
|
|
145
|
+
except NextcloudExceptionNotFound:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _AsyncDeclarativeSettingsAPI:
|
|
150
|
+
"""Class providing async API for creating UI for the ExApp settings."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, session: AsyncNcSessionApp):
|
|
153
|
+
self._session = session
|
|
154
|
+
|
|
155
|
+
async def register_form(self, form_schema: SettingsForm | dict[str, typing.Any]) -> None:
|
|
156
|
+
"""Registers or edit the Settings UI Form."""
|
|
157
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
158
|
+
param = {"formScheme": form_schema.to_dict() if isinstance(form_schema, SettingsForm) else form_schema}
|
|
159
|
+
await self._session.ocs("POST", f"{self._session.ae_url}/{_EP_SUFFIX}", json=param)
|
|
160
|
+
|
|
161
|
+
async def unregister_form(self, form_id: str, not_fail=True) -> None:
|
|
162
|
+
"""Removes Settings UI Form."""
|
|
163
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
164
|
+
try:
|
|
165
|
+
await self._session.ocs("DELETE", f"{self._session.ae_url}/{_EP_SUFFIX}", params={"formId": form_id})
|
|
166
|
+
except NextcloudExceptionNotFound as e:
|
|
167
|
+
if not not_fail:
|
|
168
|
+
raise e from None
|
|
169
|
+
|
|
170
|
+
async def get_entry(self, form_id: str) -> SettingsForm | None:
|
|
171
|
+
"""Get information of the Settings UI Form."""
|
|
172
|
+
require_capabilities("app_api", await self._session.capabilities)
|
|
173
|
+
try:
|
|
174
|
+
return SettingsForm.from_dict(
|
|
175
|
+
await self._session.ocs("GET", f"{self._session.ae_url}/{_EP_SUFFIX}", params={"formId": form_id})
|
|
176
|
+
)
|
|
177
|
+
except NextcloudExceptionNotFound:
|
|
178
|
+
return None
|