Skip to content

Commit

Permalink
AppAPI 2.6.0 - new FileActionsV2 (#252)
Browse files Browse the repository at this point in the history
Now in ExApp you can conveniently accept many files at once, and this is
very cool 🥳

Reference: nextcloud/app_api#284

---------

Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
bigcat88 authored May 8, 2024
1 parent 54082a4 commit c817dc7
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 28 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.14.0 - 2024-05-xx]

### Added

- NextcloudApp: `nc.ui.files_dropdown_menu.register_ex` to register new version of FileActions(AppAPI 2.6.0+)

## [0.13.0 - 2024-04-28]

### Added
Expand Down
12 changes: 7 additions & 5 deletions docs/NextcloudApp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ After that we extend the **enabled** handler and include there registration of t
def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
try:
if enabled:
nc.ui.files_dropdown_menu.register("to_gif", "TO GIF", "/video_to_gif", mime="video")
nc.ui.files_dropdown_menu.register_ex("to_gif", "TO GIF", "/video_to_gif", mime="video")
else:
nc.ui.files_dropdown_menu.unregister("to_gif")
except Exception as e:
Expand All @@ -225,13 +225,15 @@ After that, let's define the **"/video_to_gif"** endpoint that we had registered
@APP.post("/video_to_gif")
async def video_to_gif(
file: UiFileActionHandlerInfo,
files: ActionFileInfoEx,
nc: Annotated[NextcloudApp, Depends(nc_app)],
background_tasks: BackgroundTasks,
):
background_tasks.add_task(convert_video_to_gif, file.actionFile.to_fs_node(), nc)
return Response()
for one_file in files.files:
background_tasks.add_task(convert_video_to_gif, one_file.to_fs_node(), nc)
return responses.Response()
We see two parameters ``file`` and ``BackgroundTasks``, let's start with the last one, with **BackgroundTasks**:
We see two parameters ``files`` and ``BackgroundTasks``, let's start with the last one, with **BackgroundTasks**:

FastAPI `BackgroundTasks <https://fastapi.tiangolo.com/tutorial/background-tasks/?h=backgroundtasks#background-tasks>`_ documentation.

Expand Down
3 changes: 3 additions & 0 deletions docs/reference/Files/Files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ All File APIs are designed to work relative to the current user.

.. autoclass:: nc_py_api.files.ActionFileInfo
:members: fileId, name, directory, etag, mime, fileType, size, favorite, permissions, mtime, userId, instanceId, to_fs_node

.. autoclass:: nc_py_api.files.ActionFileInfoEx
:members: files
22 changes: 8 additions & 14 deletions examples/as_app/to_gif/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,12 @@
import cv2
import imageio
import numpy
from fastapi import BackgroundTasks, Depends, FastAPI
from fastapi import BackgroundTasks, Depends, FastAPI, responses
from pygifsicle import optimize
from requests import Response

from nc_py_api import FsNode, NextcloudApp
from nc_py_api.ex_app import (
ActionFileInfo,
AppAPIAuthMiddleware,
LogLvl,
nc_app,
run_app,
set_handlers,
)
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, nc_app, run_app, set_handlers
from nc_py_api.files import ActionFileInfoEx


@asynccontextmanager
Expand Down Expand Up @@ -77,19 +70,20 @@ def convert_video_to_gif(input_file: FsNode, nc: NextcloudApp):

@APP.post("/video_to_gif")
async def video_to_gif(
file: ActionFileInfo,
files: ActionFileInfoEx,
nc: Annotated[NextcloudApp, Depends(nc_app)],
background_tasks: BackgroundTasks,
):
background_tasks.add_task(convert_video_to_gif, file.to_fs_node(), nc)
return Response()
for one_file in files.files:
background_tasks.add_task(convert_video_to_gif, one_file.to_fs_node(), nc)
return responses.Response()


def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
print(f"enabled={enabled}")
try:
if enabled:
nc.ui.files_dropdown_menu.register(
nc.ui.files_dropdown_menu.register_ex(
"to_gif",
"TO GIF",
"/video_to_gif",
Expand Down
5 changes: 5 additions & 0 deletions nc_py_api/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ def ae_url(self) -> str:
"""Return base url for the App Ecosystem endpoints."""
return "/ocs/v1.php/apps/app_api/api/v1"

@property
def ae_url_v2(self) -> str:
"""Return base url for the App Ecosystem endpoints(version 2)."""
return "/ocs/v1.php/apps/app_api/api/v2"


class NcSessionBasic(NcSessionBase, ABC):
adapter: Client
Expand Down
2 changes: 1 addition & 1 deletion nc_py_api/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version of nc_py_api."""

__version__ = "0.13.0"
__version__ = "0.14.0.dev0"
2 changes: 1 addition & 1 deletion nc_py_api/ex_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@


class UiActionFileInfo(ActionFileInfo):
"""``Deprecated``: use :py:class:`~nc_py_api.ex_app.ActionFileInfo` instead."""
"""``Deprecated``: use :py:class:`~nc_py_api.files.ActionFileInfo` instead."""
46 changes: 45 additions & 1 deletion nc_py_api/ex_app/ui/files_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Nextcloud API for working with drop-down file's menu."""

import dataclasses
import warnings

from ..._exceptions import NextcloudExceptionNotFound
from ..._misc import require_capabilities
Expand Down Expand Up @@ -54,6 +55,11 @@ def action_handler(self) -> str:
"""Relative ExApp url which will be called if user click on the entry."""
return self._raw_data["action_handler"]

@property
def version(self) -> str:
"""AppAPI `2.6.0` supports new version of UiActions(https://github.com/cloud-py-api/app_api/pull/284)."""
return self._raw_data.get("version", "1.0")

def __repr__(self):
return f"<{self.__class__.__name__} name={self.name}, mime={self.mime}, handler={self.action_handler}>"

Expand All @@ -67,7 +73,12 @@ def __init__(self, session: NcSessionApp):
self._session = session

def register(self, name: str, display_name: str, callback_url: str, **kwargs) -> None:
"""Registers the files a dropdown menu element."""
"""Registers the files dropdown menu element."""
warnings.warn(
"register() is deprecated and will be removed in a future version. Use register_ex() instead.",
DeprecationWarning,
stacklevel=2,
)
require_capabilities("app_api", self._session.capabilities)
params = {
"name": name,
Expand All @@ -80,6 +91,20 @@ def register(self, name: str, display_name: str, callback_url: str, **kwargs) ->
}
self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)

def register_ex(self, name: str, display_name: str, callback_url: str, **kwargs) -> None:
"""Registers the files dropdown menu element(extended version that receives ``ActionFileInfoEx``)."""
require_capabilities("app_api", self._session.capabilities)
params = {
"name": name,
"displayName": display_name,
"actionHandler": callback_url,
"icon": kwargs.get("icon", ""),
"mime": kwargs.get("mime", "file"),
"permissions": kwargs.get("permissions", 31),
"order": kwargs.get("order", 0),
}
self._session.ocs("POST", f"{self._session.ae_url_v2}/{self._ep_suffix}", json=params)

def unregister(self, name: str, not_fail=True) -> None:
"""Removes files dropdown menu element."""
require_capabilities("app_api", self._session.capabilities)
Expand Down Expand Up @@ -110,6 +135,11 @@ def __init__(self, session: AsyncNcSessionApp):

async def register(self, name: str, display_name: str, callback_url: str, **kwargs) -> None:
"""Registers the files a dropdown menu element."""
warnings.warn(
"register() is deprecated and will be removed in a future version. Use register_ex() instead.",
DeprecationWarning,
stacklevel=2,
)
require_capabilities("app_api", await self._session.capabilities)
params = {
"name": name,
Expand All @@ -122,6 +152,20 @@ async def register(self, name: str, display_name: str, callback_url: str, **kwar
}
await self._session.ocs("POST", f"{self._session.ae_url}/{self._ep_suffix}", json=params)

async def register_ex(self, name: str, display_name: str, callback_url: str, **kwargs) -> None:
"""Registers the files dropdown menu element(extended version that receives ``ActionFileInfoEx``)."""
require_capabilities("app_api", await self._session.capabilities)
params = {
"name": name,
"displayName": display_name,
"actionHandler": callback_url,
"icon": kwargs.get("icon", ""),
"mime": kwargs.get("mime", "file"),
"permissions": kwargs.get("permissions", 31),
"order": kwargs.get("order", 0),
}
await self._session.ocs("POST", f"{self._session.ae_url_v2}/{self._ep_suffix}", json=params)

async def unregister(self, name: str, not_fail=True) -> None:
"""Removes files dropdown menu element."""
require_capabilities("app_api", await self._session.capabilities)
Expand Down
7 changes: 7 additions & 0 deletions nc_py_api/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,10 @@ def to_fs_node(self) -> FsNode:
last_modified=datetime.datetime.utcfromtimestamp(self.mtime).replace(tzinfo=datetime.timezone.utc),
mimetype=self.mime,
)


class ActionFileInfoEx(BaseModel):
"""New ``register_ex`` uses new data format which allowing receiving multiple NC Nodes in one request."""

files: list[ActionFileInfo]
"""Always list of ``ActionFileInfo`` with one element minimum."""
20 changes: 14 additions & 6 deletions tests/actual_tests/ui_files_actions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def test_register_ui_file_actions(nc_app):
nc_app.ui.files_dropdown_menu.register("test_ui_action_im", "UI TEST Image", "/ui_action_test", mime="image")
nc_app.ui.files_dropdown_menu.register_ex("test_ui_action_im", "UI TEST Image", "/ui_action_test", mime="image")
result = nc_app.ui.files_dropdown_menu.get_entry("test_ui_action_im")
assert result.name == "test_ui_action_im"
assert result.display_name == "UI TEST Image"
Expand All @@ -14,6 +14,7 @@ def test_register_ui_file_actions(nc_app):
assert result.order == 0
assert result.icon == ""
assert result.appid == "nc_py_api"
assert result.version == "2.0"
nc_app.ui.files_dropdown_menu.unregister(result.name)
nc_app.ui.files_dropdown_menu.register("test_ui_action_any", "UI TEST", "ui_action", permissions=1, order=1)
result = nc_app.ui.files_dropdown_menu.get_entry("test_ui_action_any")
Expand All @@ -24,7 +25,8 @@ def test_register_ui_file_actions(nc_app):
assert result.permissions == 1
assert result.order == 1
assert result.icon == ""
nc_app.ui.files_dropdown_menu.register("test_ui_action_any", "UI", "/ui_action2", icon="/img/icon.svg")
assert result.version == "1.0"
nc_app.ui.files_dropdown_menu.register_ex("test_ui_action_any", "UI", "/ui_action2", icon="/img/icon.svg")
result = nc_app.ui.files_dropdown_menu.get_entry("test_ui_action_any")
assert result.name == "test_ui_action_any"
assert result.display_name == "UI"
Expand All @@ -33,13 +35,16 @@ def test_register_ui_file_actions(nc_app):
assert result.permissions == 31
assert result.order == 0
assert result.icon == "img/icon.svg"
assert result.version == "2.0"
nc_app.ui.files_dropdown_menu.unregister(result.name)
assert str(result).find("name=test_ui_action")


@pytest.mark.asyncio(scope="session")
async def test_register_ui_file_actions_async(anc_app):
await anc_app.ui.files_dropdown_menu.register("test_ui_action_im", "UI TEST Image", "/ui_action_test", mime="image")
await anc_app.ui.files_dropdown_menu.register_ex(
"test_ui_action_im", "UI TEST Image", "/ui_action_test", mime="image"
)
result = await anc_app.ui.files_dropdown_menu.get_entry("test_ui_action_im")
assert result.name == "test_ui_action_im"
assert result.display_name == "UI TEST Image"
Expand All @@ -49,6 +54,7 @@ async def test_register_ui_file_actions_async(anc_app):
assert result.order == 0
assert result.icon == ""
assert result.appid == "nc_py_api"
assert result.version == "2.0"
await anc_app.ui.files_dropdown_menu.unregister(result.name)
await anc_app.ui.files_dropdown_menu.register("test_ui_action_any", "UI TEST", "ui_action", permissions=1, order=1)
result = await anc_app.ui.files_dropdown_menu.get_entry("test_ui_action_any")
Expand All @@ -59,7 +65,8 @@ async def test_register_ui_file_actions_async(anc_app):
assert result.permissions == 1
assert result.order == 1
assert result.icon == ""
await anc_app.ui.files_dropdown_menu.register("test_ui_action_any", "UI", "/ui_action2", icon="/img/icon.svg")
assert result.version == "1.0"
await anc_app.ui.files_dropdown_menu.register_ex("test_ui_action_any", "UI", "/ui_action2", icon="/img/icon.svg")
result = await anc_app.ui.files_dropdown_menu.get_entry("test_ui_action_any")
assert result.name == "test_ui_action_any"
assert result.display_name == "UI"
Expand All @@ -68,12 +75,13 @@ async def test_register_ui_file_actions_async(anc_app):
assert result.permissions == 31
assert result.order == 0
assert result.icon == "img/icon.svg"
assert result.version == "2.0"
await anc_app.ui.files_dropdown_menu.unregister(result.name)
assert str(result).find("name=test_ui_action")


def test_unregister_ui_file_actions(nc_app):
nc_app.ui.files_dropdown_menu.register("test_ui_action", "NcPyApi UI TEST", "/any_rel_url")
nc_app.ui.files_dropdown_menu.register_ex("test_ui_action", "NcPyApi UI TEST", "/any_rel_url")
nc_app.ui.files_dropdown_menu.unregister("test_ui_action")
assert nc_app.ui.files_dropdown_menu.get_entry("test_ui_action") is None
nc_app.ui.files_dropdown_menu.unregister("test_ui_action")
Expand All @@ -83,7 +91,7 @@ def test_unregister_ui_file_actions(nc_app):

@pytest.mark.asyncio(scope="session")
async def test_unregister_ui_file_actions_async(anc_app):
await anc_app.ui.files_dropdown_menu.register("test_ui_action", "NcPyApi UI TEST", "/any_rel_url")
await anc_app.ui.files_dropdown_menu.register_ex("test_ui_action", "NcPyApi UI TEST", "/any_rel_url")
await anc_app.ui.files_dropdown_menu.unregister("test_ui_action")
assert await anc_app.ui.files_dropdown_menu.get_entry("test_ui_action") is None
await anc_app.ui.files_dropdown_menu.unregister("test_ui_action")
Expand Down

0 comments on commit c817dc7

Please sign in to comment.