Skip to content

AppAPI 2.6.0 - new FileActionsV2 #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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