Skip to content

Commit 16f44f8

Browse files
authored
Async Support (#181)
Changes proposed in this pull request: * **AsyncNextcloud** and **AsyncNextcloudApp** are here! * Reworked `set_handlers` to allow use of async handlers. * set_handlers: **denied** defining _init_handler_ and _models_to_fetch_ at the same time, as `huggingface_hub` does not support async. If you want you can provide your own sync or async `init_handler` callback and do in it what you want. If you need only automatic AI models to download(probably all will use this behaviour), `nc_py_api` will handle on its own. What is currently missing: `caldav` async for AsyncNC classes. If you need to use `CalDAV` use standard synchronous classes. _After this PR there will one more separate PR where `models_to_fetch` and `models_download_params` will be united in a single more flexed parameter._ --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 52b48f1 commit 16f44f8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+6226
-958
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -598,15 +598,32 @@ jobs:
598598
- name: Enable Talk
599599
run: php occ app:enable spreed
600600

601-
- name: Generate coverage report
601+
- name: Generate coverage report (1)
602602
working-directory: nc_py_api
603603
run: |
604-
coverage run --data-file=.coverage.talk_bot tests/_talk_bot.py &
604+
coverage run --data-file=.coverage.talk_bot tests/_talk_bot_async.py &
605605
echo $! > /tmp/_talk_bot.pid
606606
coverage run --data-file=.coverage.ci -m pytest
607607
kill -15 $(cat /tmp/_talk_bot.pid)
608608
timeout 3m tail --pid=$(cat /tmp/_talk_bot.pid) -f /dev/null
609609
coverage run --data-file=.coverage.at_the_end -m pytest tests/_tests_at_the_end.py
610+
611+
- name: Uninstall NcPyApi
612+
run: |
613+
php occ app_api:app:unregister "$APP_ID" --silent
614+
php occ app_api:daemon:unregister manual_install
615+
616+
- name: Generate coverage report (2)
617+
working-directory: nc_py_api
618+
run: |
619+
coverage run --data-file=.coverage.ci_install_models tests/_install_init_handler_models.py &
620+
echo $! > /tmp/_install_models.pid
621+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
622+
cd ..
623+
sh nc_py_api/scripts/ci_register.sh "$APP_ID" "$APP_VERSION" "$APP_SECRET" "localhost" "$APP_PORT"
624+
kill -15 $(cat /tmp/_install_models.pid)
625+
timeout 3m tail --pid=$(cat /tmp/_install_models.pid) -f /dev/null
626+
cd nc_py_api
610627
coverage combine && coverage xml && coverage html
611628
612629
- name: HTML coverage to artifacts
@@ -757,21 +774,38 @@ jobs:
757774
- name: Enable Talk
758775
run: php occ app:enable spreed
759776

760-
- name: Generate coverage report
777+
- name: Generate coverage report (1)
761778
working-directory: nc_py_api
762779
run: |
763-
coverage run --data-file=.coverage.talk_bot tests/_talk_bot.py &
780+
coverage run --data-file=.coverage.talk_bot tests/_talk_bot_async.py &
764781
echo $! > /tmp/_talk_bot.pid
765782
coverage run --data-file=.coverage.ci -m pytest
766783
kill -15 $(cat /tmp/_talk_bot.pid)
767784
timeout 3m tail --pid=$(cat /tmp/_talk_bot.pid) -f /dev/null
768785
coverage run --data-file=.coverage.at_the_end -m pytest tests/_tests_at_the_end.py
769-
coverage combine && coverage xml && coverage html
770786
env:
771787
NPA_TIMEOUT: None
772788
NPA_TIMEOUT_DAV: None
773789
NPA_NC_CERT: False
774790

791+
- name: Uninstall NcPyApi
792+
run: |
793+
php occ app_api:app:unregister "$APP_ID" --silent
794+
php occ app_api:daemon:unregister manual_install
795+
796+
- name: Generate coverage report (2)
797+
working-directory: nc_py_api
798+
run: |
799+
coverage run --data-file=.coverage.ci_install_models tests/_install_init_handler_models.py &
800+
echo $! > /tmp/_install_models.pid
801+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
802+
cd ..
803+
sh nc_py_api/scripts/ci_register.sh "$APP_ID" "$APP_VERSION" "$APP_SECRET" "localhost" "$APP_PORT"
804+
kill -15 $(cat /tmp/_install_models.pid)
805+
timeout 3m tail --pid=$(cat /tmp/_install_models.pid) -f /dev/null
806+
cd nc_py_api
807+
coverage combine && coverage xml && coverage html
808+
775809
- name: HTML coverage to artifacts
776810
uses: actions/upload-artifact@v3
777811
with:
@@ -865,7 +899,7 @@ jobs:
865899

866900
- name: Install NcPyApi
867901
working-directory: nc_py_api
868-
run: python3 -m pip -v install . pytest coverage pillow
902+
run: python3 -m pip -v install . pytest pytest-asyncio coverage pillow
869903

870904
- name: Talk Branch Main
871905
if: ${{ startsWith(matrix.nextcloud, 'master') }}

.run/aregister_nc_py_api (27).run.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="aregister_nc_py_api (27)" type="PythonConfigurationType" factoryName="Python">
3+
<module name="nc_py_api" />
4+
<option name="ENV_FILES" value="" />
5+
<option name="INTERPRETER_OPTIONS" value="" />
6+
<option name="PARENT_ENVS" value="true" />
7+
<envs>
8+
<env name="PYTHONUNBUFFERED" value="1" />
9+
<env name="APP_ID" value="nc_py_api" />
10+
<env name="APP_PORT" value="9009" />
11+
<env name="APP_SECRET" value="12345" />
12+
<env name="APP_VERSION" value="1.0.0" />
13+
<env name="NEXTCLOUD_URL" value="http://stable27.local/index.php" />
14+
<env name="APP_HOST" value="0.0.0.0" />
15+
</envs>
16+
<option name="SDK_HOME" value="" />
17+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
18+
<option name="IS_MODULE_SDK" value="true" />
19+
<option name="ADD_CONTENT_ROOTS" value="true" />
20+
<option name="ADD_SOURCE_ROOTS" value="true" />
21+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
22+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
23+
<option name="PARAMETERS" value="" />
24+
<option name="SHOW_COMMAND_LINE" value="false" />
25+
<option name="EMULATE_TERMINAL" value="false" />
26+
<option name="MODULE_MODE" value="false" />
27+
<option name="REDIRECT_INPUT" value="false" />
28+
<option name="INPUT_FILE" value="" />
29+
<method v="2" />
30+
</configuration>
31+
</component>

.run/aregister_nc_py_api (28).run.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="aregister_nc_py_api (28)" type="PythonConfigurationType" factoryName="Python">
3+
<module name="nc_py_api" />
4+
<option name="ENV_FILES" value="" />
5+
<option name="INTERPRETER_OPTIONS" value="" />
6+
<option name="PARENT_ENVS" value="true" />
7+
<envs>
8+
<env name="PYTHONUNBUFFERED" value="1" />
9+
<env name="APP_ID" value="nc_py_api" />
10+
<env name="APP_PORT" value="9009" />
11+
<env name="APP_SECRET" value="12345" />
12+
<env name="APP_VERSION" value="1.0.0" />
13+
<env name="NEXTCLOUD_URL" value="http://stable28.local/index.php" />
14+
<env name="APP_HOST" value="0.0.0.0" />
15+
</envs>
16+
<option name="SDK_HOME" value="" />
17+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
18+
<option name="IS_MODULE_SDK" value="true" />
19+
<option name="ADD_CONTENT_ROOTS" value="true" />
20+
<option name="ADD_SOURCE_ROOTS" value="true" />
21+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
22+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
23+
<option name="PARAMETERS" value="" />
24+
<option name="SHOW_COMMAND_LINE" value="false" />
25+
<option name="EMULATE_TERMINAL" value="false" />
26+
<option name="MODULE_MODE" value="false" />
27+
<option name="REDIRECT_INPUT" value="false" />
28+
<option name="INPUT_FILE" value="" />
29+
<method v="2" />
30+
</configuration>
31+
</component>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="aregister_nc_py_api (last)" type="PythonConfigurationType" factoryName="Python">
3+
<module name="nc_py_api" />
4+
<option name="ENV_FILES" value="" />
5+
<option name="INTERPRETER_OPTIONS" value="" />
6+
<option name="PARENT_ENVS" value="true" />
7+
<envs>
8+
<env name="PYTHONUNBUFFERED" value="1" />
9+
<env name="APP_ID" value="nc_py_api" />
10+
<env name="APP_PORT" value="9009" />
11+
<env name="APP_SECRET" value="12345" />
12+
<env name="APP_VERSION" value="1.0.0" />
13+
<env name="NEXTCLOUD_URL" value="http://nextcloud.local" />
14+
<env name="APP_HOST" value="0.0.0.0" />
15+
</envs>
16+
<option name="SDK_HOME" value="" />
17+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
18+
<option name="IS_MODULE_SDK" value="true" />
19+
<option name="ADD_CONTENT_ROOTS" value="true" />
20+
<option name="ADD_SOURCE_ROOTS" value="true" />
21+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
22+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
23+
<option name="PARAMETERS" value="" />
24+
<option name="SHOW_COMMAND_LINE" value="false" />
25+
<option name="EMULATE_TERMINAL" value="false" />
26+
<option name="MODULE_MODE" value="false" />
27+
<option name="REDIRECT_INPUT" value="false" />
28+
<option name="INPUT_FILE" value="" />
29+
<method v="2" />
30+
</configuration>
31+
</component>

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88

9-
- set_handlers: `enabled_handler`, `heartbeat_handler` now can be async(Coroutines). #175
9+
- implemented `AsyncNextcloud` and `AsyncNextcloudApp` classes. #181
1010

1111
### Changed
1212

13+
- set_handlers: `enabled_handler`, `heartbeat_handler`, `init_handler` now can be async(Coroutines). #175 #181
1314
- drop Python 3.9 support. #180
1415
- internal code refactoring and clean-up #177
1516

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Python library that provides a robust and well-documented API that allows develo
2121
* **Reliable**: Minimum number of incompatible changes.
2222
* **Robust**: All code is covered with tests as much as possible.
2323
* **Easy**: Designed to be easy to use with excellent documentation.
24+
* **Sync+Async**: Provides both sync and async APIs.
2425

2526
### Capabilities
2627
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 |

nc_py_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
from ._version import __version__
1010
from .files import FilePermissions, FsNode
1111
from .files.sharing import ShareType
12-
from .nextcloud import Nextcloud, NextcloudApp
12+
from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp

nc_py_api/_preferences.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Nextcloud API for working with classics app's storage with user's context (table oc_preferences)."""
22

33
from ._misc import check_capabilities, require_capabilities
4-
from ._session import NcSessionBasic
4+
from ._session import AsyncNcSessionBasic, NcSessionBasic
55

66

77
class PreferencesAPI:
@@ -26,3 +26,27 @@ def delete(self, app_name: str, key: str) -> None:
2626
"""Removes a key and its value for a specific application."""
2727
require_capabilities("provisioning_api", self._session.capabilities)
2828
self._session.ocs("DELETE", f"{self._ep_base}/{app_name}/{key}")
29+
30+
31+
class AsyncPreferencesAPI:
32+
"""Async API for setting/removing configuration values of applications that support it."""
33+
34+
_ep_base: str = "/ocs/v1.php/apps/provisioning_api/api/v1/config/users"
35+
36+
def __init__(self, session: AsyncNcSessionBasic):
37+
self._session = session
38+
39+
@property
40+
async def available(self) -> bool:
41+
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
42+
return not check_capabilities("provisioning_api", await self._session.capabilities)
43+
44+
async def set_value(self, app_name: str, key: str, value: str) -> None:
45+
"""Sets the value for the key for the specific application."""
46+
require_capabilities("provisioning_api", await self._session.capabilities)
47+
await self._session.ocs("POST", f"{self._ep_base}/{app_name}/{key}", params={"configValue": value})
48+
49+
async def delete(self, app_name: str, key: str) -> None:
50+
"""Removes a key and its value for a specific application."""
51+
require_capabilities("provisioning_api", await self._session.capabilities)
52+
await self._session.ocs("DELETE", f"{self._ep_base}/{app_name}/{key}")

nc_py_api/_preferences_ex.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from ._exceptions import NextcloudExceptionNotFound
66
from ._misc import require_capabilities
7-
from ._session import NcSessionBasic
7+
from ._session import AsyncNcSessionBasic, NcSessionBasic
88

99

1010
@dataclasses.dataclass
@@ -62,6 +62,49 @@ def delete(self, keys: str | list[str], not_fail=True) -> None:
6262
raise e from None
6363

6464

65+
class _AsyncBasicAppCfgPref:
66+
_url_suffix: str
67+
68+
def __init__(self, session: AsyncNcSessionBasic):
69+
self._session = session
70+
71+
async def get_value(self, key: str, default=None) -> str | None:
72+
"""Returns the value of the key, if found, or the specified default value."""
73+
if not key:
74+
raise ValueError("`key` parameter can not be empty")
75+
require_capabilities("app_api", await self._session.capabilities)
76+
r = await self.get_values([key])
77+
if r:
78+
return r[0].value
79+
return default
80+
81+
async def get_values(self, keys: list[str]) -> list[CfgRecord]:
82+
"""Returns the :py:class:`CfgRecord` for each founded key."""
83+
if not keys:
84+
return []
85+
if not all(keys):
86+
raise ValueError("`key` parameter can not be empty")
87+
require_capabilities("app_api", await self._session.capabilities)
88+
data = {"configKeys": keys}
89+
results = await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}/get-values", json=data)
90+
return [CfgRecord(i) for i in results]
91+
92+
async def delete(self, keys: str | list[str], not_fail=True) -> None:
93+
"""Deletes config/preference entries by the provided keys."""
94+
if isinstance(keys, str):
95+
keys = [keys]
96+
if not keys:
97+
return
98+
if not all(keys):
99+
raise ValueError("`key` parameter can not be empty")
100+
require_capabilities("app_api", await self._session.capabilities)
101+
try:
102+
await self._session.ocs("DELETE", f"{self._session.ae_url}/{self._url_suffix}", json={"configKeys": keys})
103+
except NextcloudExceptionNotFound as e:
104+
if not not_fail:
105+
raise e from None
106+
107+
65108
class PreferencesExAPI(_BasicAppCfgPref):
66109
"""User specific preferences API."""
67110

@@ -76,6 +119,20 @@ def set_value(self, key: str, value: str) -> None:
76119
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
77120

78121

122+
class AsyncPreferencesExAPI(_AsyncBasicAppCfgPref):
123+
"""User specific preferences API."""
124+
125+
_url_suffix = "ex-app/preference"
126+
127+
async def set_value(self, key: str, value: str) -> None:
128+
"""Sets a value for a key."""
129+
if not key:
130+
raise ValueError("`key` parameter can not be empty")
131+
require_capabilities("app_api", await self._session.capabilities)
132+
params = {"configKey": key, "configValue": value}
133+
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
134+
135+
79136
class AppConfigExAPI(_BasicAppCfgPref):
80137
"""Non-user(App) specific preferences API."""
81138

@@ -95,3 +152,24 @@ def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None
95152
if sensitive is not None:
96153
params["sensitive"] = sensitive
97154
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
155+
156+
157+
class AsyncAppConfigExAPI(_AsyncBasicAppCfgPref):
158+
"""Non-user(App) specific preferences API."""
159+
160+
_url_suffix = "ex-app/config"
161+
162+
async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
163+
"""Sets a value and if specified the sensitive flag for a key.
164+
165+
.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
166+
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
167+
sensitive is *unspecified* it will not change the existing `sensitive` flag.
168+
"""
169+
if not key:
170+
raise ValueError("`key` parameter can not be empty")
171+
require_capabilities("app_api", await self._session.capabilities)
172+
params: dict = {"configKey": key, "configValue": value}
173+
if sensitive is not None:
174+
params["sensitive"] = sensitive
175+
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)

0 commit comments

Comments
 (0)