nc-py-api 0.7.2__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 +38 -0
- nc_py_api/_version.py +1 -1
- nc_py_api/ex_app/__init__.py +1 -1
- nc_py_api/ex_app/defs.py +5 -1
- nc_py_api/ex_app/misc.py +7 -0
- nc_py_api/ex_app/providers/__init__.py +1 -0
- nc_py_api/ex_app/providers/providers.py +31 -0
- nc_py_api/ex_app/providers/speech_to_text.py +130 -0
- nc_py_api/ex_app/providers/text_processing.py +137 -0
- nc_py_api/ex_app/ui/files_actions.py +1 -1
- nc_py_api/files/files.py +14 -52
- nc_py_api/nextcloud.py +15 -0
- {nc_py_api-0.7.2.dist-info → nc_py_api-0.8.0.dist-info}/METADATA +16 -16
- {nc_py_api-0.7.2.dist-info → nc_py_api-0.8.0.dist-info}/RECORD +17 -13
- {nc_py_api-0.7.2.dist-info → nc_py_api-0.8.0.dist-info}/WHEEL +0 -0
- {nc_py_api-0.7.2.dist-info → nc_py_api-0.8.0.dist-info}/licenses/AUTHORS +0 -0
- {nc_py_api-0.7.2.dist-info → nc_py_api-0.8.0.dist-info}/licenses/LICENSE.txt +0 -0
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
nc_py_api/ex_app/__init__.py
CHANGED
|
@@ -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
|
|
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
|
@@ -98,14 +98,8 @@ class FilesAPI:
|
|
|
98
98
|
The object must implement the ``file.write`` method and be able to write binary data.
|
|
99
99
|
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
|
|
100
100
|
"""
|
|
101
|
-
path = path.user_path if isinstance(path, FsNode) else path
|
|
102
|
-
|
|
103
|
-
with builtins.open(fp, "wb") as f:
|
|
104
|
-
self.__download2stream(path, f, **kwargs)
|
|
105
|
-
elif hasattr(fp, "write"):
|
|
106
|
-
self.__download2stream(path, fp, **kwargs)
|
|
107
|
-
else:
|
|
108
|
-
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)
|
|
109
103
|
|
|
110
104
|
def download_directory_as_zip(self, path: str | FsNode, local_path: str | Path | None = None, **kwargs) -> Path:
|
|
111
105
|
"""Downloads a remote directory as zip archive.
|
|
@@ -117,17 +111,11 @@ class FilesAPI:
|
|
|
117
111
|
.. note:: This works only for directories, you should not use this to download a file.
|
|
118
112
|
"""
|
|
119
113
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
with open(
|
|
126
|
-
result_path,
|
|
127
|
-
"wb",
|
|
128
|
-
) as fp:
|
|
129
|
-
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
130
|
-
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
|
+
)
|
|
131
119
|
return Path(result_path)
|
|
132
120
|
|
|
133
121
|
def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
|
|
@@ -439,12 +427,6 @@ class FilesAPI:
|
|
|
439
427
|
self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
|
|
440
428
|
)
|
|
441
429
|
|
|
442
|
-
def __download2stream(self, path: str, fp, **kwargs) -> None:
|
|
443
|
-
with self._session.adapter_dav.stream("GET", quote(dav_get_obj_path(self._session.user, path))) as response:
|
|
444
|
-
check_error(response, f"download_stream: user={self._session.user}, path={path}")
|
|
445
|
-
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
446
|
-
fp.write(data_chunk)
|
|
447
|
-
|
|
448
430
|
def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
|
|
449
431
|
_tmp_path = "nc-py-api-" + random_string(56)
|
|
450
432
|
_dav_path = quote(dav_get_obj_path(self._session.user, _tmp_path, root_path="/uploads"))
|
|
@@ -561,14 +543,8 @@ class AsyncFilesAPI:
|
|
|
561
543
|
The object must implement the ``file.write`` method and be able to write binary data.
|
|
562
544
|
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
|
|
563
545
|
"""
|
|
564
|
-
path = path.user_path if isinstance(path, FsNode) else path
|
|
565
|
-
|
|
566
|
-
with builtins.open(fp, "wb") as f:
|
|
567
|
-
await self.__download2stream(path, f, **kwargs)
|
|
568
|
-
elif hasattr(fp, "write"):
|
|
569
|
-
await self.__download2stream(path, fp, **kwargs)
|
|
570
|
-
else:
|
|
571
|
-
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)
|
|
572
548
|
|
|
573
549
|
async def download_directory_as_zip(
|
|
574
550
|
self, path: str | FsNode, local_path: str | Path | None = None, **kwargs
|
|
@@ -582,17 +558,11 @@ class AsyncFilesAPI:
|
|
|
582
558
|
.. note:: This works only for directories, you should not use this to download a file.
|
|
583
559
|
"""
|
|
584
560
|
path = path.user_path if isinstance(path, FsNode) else path
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
with open(
|
|
591
|
-
result_path,
|
|
592
|
-
"wb",
|
|
593
|
-
) as fp:
|
|
594
|
-
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
595
|
-
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
|
+
)
|
|
596
566
|
return Path(result_path)
|
|
597
567
|
|
|
598
568
|
async def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
|
|
@@ -909,14 +879,6 @@ class AsyncFilesAPI:
|
|
|
909
879
|
self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
|
|
910
880
|
)
|
|
911
881
|
|
|
912
|
-
async def __download2stream(self, path: str, fp, **kwargs) -> None:
|
|
913
|
-
async with self._session.adapter_dav.stream(
|
|
914
|
-
"GET", quote(dav_get_obj_path(await self._session.user, path))
|
|
915
|
-
) as response:
|
|
916
|
-
check_error(response, f"download_stream: user={await self._session.user}, path={path}")
|
|
917
|
-
async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
|
|
918
|
-
fp.write(data_chunk)
|
|
919
|
-
|
|
920
882
|
async def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
|
|
921
883
|
_tmp_path = "nc-py-api-" + random_string(56)
|
|
922
884
|
_dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
|
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
|
|
@@ -134,6 +135,10 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
134
135
|
"""Performs OCS call and returns OCS response payload data."""
|
|
135
136
|
return self._session.ocs(method, path, content=content, json=json, params=params, **kwargs)
|
|
136
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
|
+
|
|
137
142
|
|
|
138
143
|
class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
139
144
|
apps: _AsyncAppsAPI
|
|
@@ -228,6 +233,10 @@ class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
|
|
|
228
233
|
"""Performs OCS call and returns OCS response payload data."""
|
|
229
234
|
return await self._session.ocs(method, path, content=content, json=json, params=params, **kwargs)
|
|
230
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
|
+
|
|
231
240
|
|
|
232
241
|
class Nextcloud(_NextcloudBasic):
|
|
233
242
|
"""Nextcloud client class.
|
|
@@ -294,6 +303,8 @@ class NextcloudApp(_NextcloudBasic):
|
|
|
294
303
|
"""Nextcloud User Preferences API for ExApps"""
|
|
295
304
|
ui: UiApi
|
|
296
305
|
"""Nextcloud UI API for ExApps"""
|
|
306
|
+
providers: ProvidersApi
|
|
307
|
+
"""API for registering providers for Nextcloud"""
|
|
297
308
|
|
|
298
309
|
def __init__(self, **kwargs):
|
|
299
310
|
"""The parameters will be taken from the environment.
|
|
@@ -305,6 +316,7 @@ class NextcloudApp(_NextcloudBasic):
|
|
|
305
316
|
self.appconfig_ex = AppConfigExAPI(self._session)
|
|
306
317
|
self.preferences_ex = PreferencesExAPI(self._session)
|
|
307
318
|
self.ui = UiApi(self._session)
|
|
319
|
+
self.providers = ProvidersApi(self._session)
|
|
308
320
|
|
|
309
321
|
def log(self, log_lvl: LogLvl, content: str) -> None:
|
|
310
322
|
"""Writes log to the Nextcloud log file."""
|
|
@@ -431,6 +443,8 @@ class AsyncNextcloudApp(_AsyncNextcloudBasic):
|
|
|
431
443
|
"""Nextcloud User Preferences API for ExApps"""
|
|
432
444
|
ui: AsyncUiApi
|
|
433
445
|
"""Nextcloud UI API for ExApps"""
|
|
446
|
+
providers: AsyncProvidersApi
|
|
447
|
+
"""API for registering providers for Nextcloud"""
|
|
434
448
|
|
|
435
449
|
def __init__(self, **kwargs):
|
|
436
450
|
"""The parameters will be taken from the environment.
|
|
@@ -442,6 +456,7 @@ class AsyncNextcloudApp(_AsyncNextcloudBasic):
|
|
|
442
456
|
self.appconfig_ex = AsyncAppConfigExAPI(self._session)
|
|
443
457
|
self.preferences_ex = AsyncPreferencesExAPI(self._session)
|
|
444
458
|
self.ui = AsyncUiApi(self._session)
|
|
459
|
+
self.providers = AsyncProvidersApi(self._session)
|
|
445
460
|
|
|
446
461
|
async def log(self, log_lvl: LogLvl, content: str) -> None:
|
|
447
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.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Nextcloud Python Framework
|
|
5
5
|
Project-URL: Changelog, https://github.com/cloud-py-api/nc_py_api/blob/main/CHANGELOG.md
|
|
6
6
|
Project-URL: Documentation, https://cloud-py-api.github.io/nc_py_api/
|
|
@@ -89,21 +89,21 @@ Python library that provides a robust and well-documented API that allows develo
|
|
|
89
89
|
* **Sync + Async**: Provides both sync and async APIs.
|
|
90
90
|
|
|
91
91
|
### Capabilities
|
|
92
|
-
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 |
|
|
93
|
-
|
|
94
|
-
| Calendar | ✅ | ✅ | ✅ |
|
|
95
|
-
| File System & Tags | ✅ | ✅ | ✅ |
|
|
96
|
-
| Nextcloud Talk | ✅ | ✅ | ✅ |
|
|
97
|
-
| Notifications | ✅ | ✅ | ✅ |
|
|
98
|
-
| Shares | ✅ | ✅ | ✅ |
|
|
99
|
-
| Users & Groups | ✅ | ✅ | ✅ |
|
|
100
|
-
| User & Weather status | ✅ | ✅ | ✅ |
|
|
101
|
-
| Other APIs*** | ✅ | ✅ | ✅ |
|
|
102
|
-
| Talk Bot API* | N/A | ✅ | ✅ |
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*_available only for
|
|
92
|
+
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 | Nextcloud 29 |
|
|
93
|
+
|-----------------------|:------------:|:------------:|:------------:|:------------:|
|
|
94
|
+
| Calendar | ✅ | ✅ | ✅ | ✅ |
|
|
95
|
+
| File System & Tags | ✅ | ✅ | ✅ | ✅ |
|
|
96
|
+
| Nextcloud Talk | ✅ | ✅ | ✅ | ✅ |
|
|
97
|
+
| Notifications | ✅ | ✅ | ✅ | ✅ |
|
|
98
|
+
| Shares | ✅ | ✅ | ✅ | ✅ |
|
|
99
|
+
| Users & Groups | ✅ | ✅ | ✅ | ✅ |
|
|
100
|
+
| User & Weather status | ✅ | ✅ | ✅ | ✅ |
|
|
101
|
+
| Other APIs*** | ✅ | ✅ | ✅ | ✅ |
|
|
102
|
+
| Talk Bot API* | N/A | ✅ | ✅ | ✅ |
|
|
103
|
+
| AI Providers API** | N/A | N/A | N/A | ✅ |
|
|
104
|
+
|
|
105
|
+
*_available only for **NextcloudApp**_<br>
|
|
106
|
+
**_available only for **NextcloudApp**: SpeechToText, TextProcessing_<br>
|
|
107
107
|
***_Activity, Notes_
|
|
108
108
|
|
|
109
109
|
### Differences between the Nextcloud and NextcloudApp classes
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
nc_py_api/ex_app/defs.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
40
|
-
nc_py_api-0.
|
|
41
|
-
nc_py_api-0.
|
|
42
|
-
nc_py_api-0.
|
|
43
|
-
nc_py_api-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|