Skip to content

Commit

Permalink
Async Support (#181)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
bigcat88 authored Dec 15, 2023
1 parent 52b48f1 commit 16f44f8
Show file tree
Hide file tree
Showing 59 changed files with 6,226 additions and 958 deletions.
46 changes: 40 additions & 6 deletions .github/workflows/analysis-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -598,15 +598,32 @@ jobs:
- name: Enable Talk
run: php occ app:enable spreed

- name: Generate coverage report
- name: Generate coverage report (1)
working-directory: nc_py_api
run: |
coverage run --data-file=.coverage.talk_bot tests/_talk_bot.py &
coverage run --data-file=.coverage.talk_bot tests/_talk_bot_async.py &
echo $! > /tmp/_talk_bot.pid
coverage run --data-file=.coverage.ci -m pytest
kill -15 $(cat /tmp/_talk_bot.pid)
timeout 3m tail --pid=$(cat /tmp/_talk_bot.pid) -f /dev/null
coverage run --data-file=.coverage.at_the_end -m pytest tests/_tests_at_the_end.py
- name: Uninstall NcPyApi
run: |
php occ app_api:app:unregister "$APP_ID" --silent
php occ app_api:daemon:unregister manual_install
- name: Generate coverage report (2)
working-directory: nc_py_api
run: |
coverage run --data-file=.coverage.ci_install_models tests/_install_init_handler_models.py &
echo $! > /tmp/_install_models.pid
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
cd ..
sh nc_py_api/scripts/ci_register.sh "$APP_ID" "$APP_VERSION" "$APP_SECRET" "localhost" "$APP_PORT"
kill -15 $(cat /tmp/_install_models.pid)
timeout 3m tail --pid=$(cat /tmp/_install_models.pid) -f /dev/null
cd nc_py_api
coverage combine && coverage xml && coverage html
- name: HTML coverage to artifacts
Expand Down Expand Up @@ -757,21 +774,38 @@ jobs:
- name: Enable Talk
run: php occ app:enable spreed

- name: Generate coverage report
- name: Generate coverage report (1)
working-directory: nc_py_api
run: |
coverage run --data-file=.coverage.talk_bot tests/_talk_bot.py &
coverage run --data-file=.coverage.talk_bot tests/_talk_bot_async.py &
echo $! > /tmp/_talk_bot.pid
coverage run --data-file=.coverage.ci -m pytest
kill -15 $(cat /tmp/_talk_bot.pid)
timeout 3m tail --pid=$(cat /tmp/_talk_bot.pid) -f /dev/null
coverage run --data-file=.coverage.at_the_end -m pytest tests/_tests_at_the_end.py
coverage combine && coverage xml && coverage html
env:
NPA_TIMEOUT: None
NPA_TIMEOUT_DAV: None
NPA_NC_CERT: False

- name: Uninstall NcPyApi
run: |
php occ app_api:app:unregister "$APP_ID" --silent
php occ app_api:daemon:unregister manual_install
- name: Generate coverage report (2)
working-directory: nc_py_api
run: |
coverage run --data-file=.coverage.ci_install_models tests/_install_init_handler_models.py &
echo $! > /tmp/_install_models.pid
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
cd ..
sh nc_py_api/scripts/ci_register.sh "$APP_ID" "$APP_VERSION" "$APP_SECRET" "localhost" "$APP_PORT"
kill -15 $(cat /tmp/_install_models.pid)
timeout 3m tail --pid=$(cat /tmp/_install_models.pid) -f /dev/null
cd nc_py_api
coverage combine && coverage xml && coverage html
- name: HTML coverage to artifacts
uses: actions/upload-artifact@v3
with:
Expand Down Expand Up @@ -865,7 +899,7 @@ jobs:

- name: Install NcPyApi
working-directory: nc_py_api
run: python3 -m pip -v install . pytest coverage pillow
run: python3 -m pip -v install . pytest pytest-asyncio coverage pillow

- name: Talk Branch Main
if: ${{ startsWith(matrix.nextcloud, 'master') }}
Expand Down
31 changes: 31 additions & 0 deletions .run/aregister_nc_py_api (27).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="aregister_nc_py_api (27)" type="PythonConfigurationType" factoryName="Python">
<module name="nc_py_api" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="APP_ID" value="nc_py_api" />
<env name="APP_PORT" value="9009" />
<env name="APP_SECRET" value="12345" />
<env name="APP_VERSION" value="1.0.0" />
<env name="NEXTCLOUD_URL" value="http://stable27.local/index.php" />
<env name="APP_HOST" value="0.0.0.0" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
31 changes: 31 additions & 0 deletions .run/aregister_nc_py_api (28).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="aregister_nc_py_api (28)" type="PythonConfigurationType" factoryName="Python">
<module name="nc_py_api" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="APP_ID" value="nc_py_api" />
<env name="APP_PORT" value="9009" />
<env name="APP_SECRET" value="12345" />
<env name="APP_VERSION" value="1.0.0" />
<env name="NEXTCLOUD_URL" value="http://stable28.local/index.php" />
<env name="APP_HOST" value="0.0.0.0" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
31 changes: 31 additions & 0 deletions .run/aregister_nc_py_api (last).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="aregister_nc_py_api (last)" type="PythonConfigurationType" factoryName="Python">
<module name="nc_py_api" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="APP_ID" value="nc_py_api" />
<env name="APP_PORT" value="9009" />
<env name="APP_SECRET" value="12345" />
<env name="APP_VERSION" value="1.0.0" />
<env name="NEXTCLOUD_URL" value="http://nextcloud.local" />
<env name="APP_HOST" value="0.0.0.0" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/_install_async.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ All notable changes to this project will be documented in this file.

### Added

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

### Changed

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

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

### Capabilities
| **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 |
Expand Down
2 changes: 1 addition & 1 deletion nc_py_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
from ._version import __version__
from .files import FilePermissions, FsNode
from .files.sharing import ShareType
from .nextcloud import Nextcloud, NextcloudApp
from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp
26 changes: 25 additions & 1 deletion nc_py_api/_preferences.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Nextcloud API for working with classics app's storage with user's context (table oc_preferences)."""

from ._misc import check_capabilities, require_capabilities
from ._session import NcSessionBasic
from ._session import AsyncNcSessionBasic, NcSessionBasic


class PreferencesAPI:
Expand All @@ -26,3 +26,27 @@ def delete(self, app_name: str, key: str) -> None:
"""Removes a key and its value for a specific application."""
require_capabilities("provisioning_api", self._session.capabilities)
self._session.ocs("DELETE", f"{self._ep_base}/{app_name}/{key}")


class AsyncPreferencesAPI:
"""Async API for setting/removing configuration values of applications that support it."""

_ep_base: str = "/ocs/v1.php/apps/provisioning_api/api/v1/config/users"

def __init__(self, session: AsyncNcSessionBasic):
self._session = session

@property
async def available(self) -> bool:
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
return not check_capabilities("provisioning_api", await self._session.capabilities)

async def set_value(self, app_name: str, key: str, value: str) -> None:
"""Sets the value for the key for the specific application."""
require_capabilities("provisioning_api", await self._session.capabilities)
await self._session.ocs("POST", f"{self._ep_base}/{app_name}/{key}", params={"configValue": value})

async def delete(self, app_name: str, key: str) -> None:
"""Removes a key and its value for a specific application."""
require_capabilities("provisioning_api", await self._session.capabilities)
await self._session.ocs("DELETE", f"{self._ep_base}/{app_name}/{key}")
80 changes: 79 additions & 1 deletion nc_py_api/_preferences_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ._exceptions import NextcloudExceptionNotFound
from ._misc import require_capabilities
from ._session import NcSessionBasic
from ._session import AsyncNcSessionBasic, NcSessionBasic


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


class _AsyncBasicAppCfgPref:
_url_suffix: str

def __init__(self, session: AsyncNcSessionBasic):
self._session = session

async def get_value(self, key: str, default=None) -> str | None:
"""Returns the value of the key, if found, or the specified default value."""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
r = await self.get_values([key])
if r:
return r[0].value
return default

async def get_values(self, keys: list[str]) -> list[CfgRecord]:
"""Returns the :py:class:`CfgRecord` for each founded key."""
if not keys:
return []
if not all(keys):
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
data = {"configKeys": keys}
results = await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}/get-values", json=data)
return [CfgRecord(i) for i in results]

async def delete(self, keys: str | list[str], not_fail=True) -> None:
"""Deletes config/preference entries by the provided keys."""
if isinstance(keys, str):
keys = [keys]
if not keys:
return
if not all(keys):
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
try:
await self._session.ocs("DELETE", f"{self._session.ae_url}/{self._url_suffix}", json={"configKeys": keys})
except NextcloudExceptionNotFound as e:
if not not_fail:
raise e from None


class PreferencesExAPI(_BasicAppCfgPref):
"""User specific preferences API."""

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


class AsyncPreferencesExAPI(_AsyncBasicAppCfgPref):
"""User specific preferences API."""

_url_suffix = "ex-app/preference"

async def set_value(self, key: str, value: str) -> None:
"""Sets a value for a key."""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
params = {"configKey": key, "configValue": value}
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class AppConfigExAPI(_BasicAppCfgPref):
"""Non-user(App) specific preferences API."""

Expand All @@ -95,3 +152,24 @@ def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None
if sensitive is not None:
params["sensitive"] = sensitive
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class AsyncAppConfigExAPI(_AsyncBasicAppCfgPref):
"""Non-user(App) specific preferences API."""

_url_suffix = "ex-app/config"

async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
"""Sets a value and if specified the sensitive flag for a key.
.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
sensitive is *unspecified* it will not change the existing `sensitive` flag.
"""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
params: dict = {"configKey": key, "configValue": value}
if sensitive is not None:
params["sensitive"] = sensitive
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
Loading

0 comments on commit 16f44f8

Please sign in to comment.