Skip to content

Commit

Permalink
set_handlers: heartbeat_handler/init_handler params rework (#226)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <[email protected]>
  • Loading branch information
bigcat88 committed Feb 14, 2024
1 parent ee4b739 commit 8a030f1
Show file tree
Hide file tree
Showing 20 changed files with 168 additions and 201 deletions.
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

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

## [0.10.0 - 2024-02-0x]
## [0.10.0 - 2024-02-14]

### Added

- set_handlers: `models_to_fetch` can now accept direct links to a files to download. #217
- DeclarativeSettings API for Nextcloud 29. #222
- NextcloudApp: `set_handlers`: `models_to_fetch` can now accept direct links to a files to download. #217
- NextcloudApp: DeclarativeSettings UI API for Nextcloud `29`. #222

### Changed

- adjusted code related to changes in AppAPI `2.0.3` #216
- NextcloudApp: adjusted code related to changes in AppAPI `2.0.3` #216
- NextcloudApp: `set_handlers` **rework of optional parameters** see PR for information. #226

## [0.9.0 - 2024-01-25]

Expand Down
75 changes: 39 additions & 36 deletions docs/NextcloudApp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,36 @@ Since this is a simple skeleton application, we only define the ``/enable`` endp
When the application receives a request at the endpoint ``/enable``,
it should register all its functionalities in the cloud and wait for requests from Nextcloud.

So, calling:
So, defining:

.. code-block:: python
set_handlers(APP, enabled_handler)
@asynccontextmanager
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
yield
will register an **enabled_handler** that will be called **both when the application is enabled and disabled**.

During the enablement process, you should register all the functionalities that your application offers
in the **enabled_handler** and remove them during the disablement process.

The API is designed so that you don't have to check whether an endpoint is already registered
The AppAPI APIs is designed so that you don't have to check whether an endpoint is already registered
(e.g., in case of a malfunction or if the administrator manually altered something in the Nextcloud database).
The API will not fail, and in such cases, it will simply re-register without error.
The AppAPI APIs will not fail, and in such cases, it will simply re-register without error.

If any error prevents your application from functioning, you should provide a brief description in the return instead
of an empty string, and log comprehensive information that will assist the administrator in addressing the issue.

.. code-block:: python
APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)
With help of ``AppAPIAuthMiddleware`` you can add **global** AppAPI authentication for all future endpoints you will define.

.. note:: ``AppAPIAuthMiddleware`` supports **disable_for** optional argument, where you can list all routes for which authentication should be skipped.

Dockerfile
----------

Expand Down Expand Up @@ -86,7 +98,7 @@ After launching your application, execute the following command in the Nextcloud
.. code-block:: shell
php occ app_api:app:register YOUR_APP_ID manual_install --json-info \
"{\"appid\":\"YOUR_APP_ID\",\"name\":\"YOUR_APP_DISPLAY_NAME\",\"daemon_config_name\":\"manual_install\",\"version\":\"YOU_APP_VERSION\",\"secret\":\"YOUR_APP_SECRET\",\"scopes\":{\"required\":[\"ALL\"],\"optional\":[]},\"port\":SELECTED_PORT,\"system_app\":0}" \
"{\"appid\":\"YOUR_APP_ID\",\"name\":\"YOUR_APP_DISPLAY_NAME\",\"daemon_config_name\":\"manual_install\",\"version\":\"YOU_APP_VERSION\",\"secret\":\"YOUR_APP_SECRET\",\"scopes\":[\"ALL\"],\"port\":SELECTED_PORT,\"system_app\":0}" \
--force-scopes --wait-finish
You can see how **nc_py_api** registers in ``scripts/dev_register.sh``.
Expand Down Expand Up @@ -178,29 +190,12 @@ 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,
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()
And this step should be discussed in more detail, since it demonstrates most of the process of working External applications.

Here we see: **nc: Annotated[NextcloudApp, Depends(nc_app)]**

For those who already know how FastAPI works, everything should be clear by now,
and for those who have not, it is very important to understand that:

It is a declaration of FastAPI `dependency <https://fastapi.tiangolo.com/tutorial/dependencies/#dependencies>`_ to be executed
before the code of **video_to_gif** starts execution.

And this required dependency handles authentication and returns an instance of the :py:class:`~nc_py_api.nextcloud.NextcloudApp`
class that allows you to make requests to Nextcloud.

.. note:: Every endpoint in your application should be protected with such method, this will ensure that only Nextcloud instance
will be able to perform requests to the application.

Finally, we are left with two much less interesting parameters, let's start with the last one, with **BackgroundTasks**:
We see two parameters ``file`` 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 All @@ -219,28 +214,36 @@ and since this is not directly related to working with NextCloud, we will skip t

**ToGif** example `full source <https://github.com/cloud-py-api/nc_py_api/blob/main/examples/as_app/to_gif/lib/main.py>`_ code.

Using AppAPIAuthMiddleware
--------------------------
Life wo AppAPIAuthMiddleware
----------------------------

If in your application in most cases you don't really need the ``NextcloudApp`` class returned after standard authentication using `Depends`:
If for some reason you do not want to use global AppAPI authentication **nc_py_api** provides a FastAPI Dependency for authentication your endpoints.

.. code-block:: python
This is a modified endpoint from ``to_gif`` example:

nc: Annotated[NextcloudApp, Depends(nc_app)]
.. code-block:: python
In this case, you can use global authentication. It's quite simple, just add this line of code:
@APP.post("/video_to_gif")
async def video_to_gif(
file: UiFileActionHandlerInfo,
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()
.. code-block:: python
from nc_py_api.ex_app import AppAPIAuthMiddleware
Here we see: **nc: Annotated[NextcloudApp, Depends(nc_app)]**

APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)
For those who already know how FastAPI works, everything should be clear by now,
and for those who have not, it is very important to understand that:

and it will be called for all your endpoints and check the validity of the connection itself.
It is a declaration of FastAPI `dependency <https://fastapi.tiangolo.com/tutorial/dependencies/#dependencies>`_ to be executed
before the code of **video_to_gif** starts execution.

``AppAPIAuthMiddleware`` supports **disable_for** optional argument, where you can list all routes for which authentication should be skipped.
And this required dependency handles authentication and returns an instance of the :py:class:`~nc_py_api.nextcloud.NextcloudApp`
class that allows you to make requests to Nextcloud.

You can still use at the same time the *AppAPIAuthMiddleware* and *Depends(nc_app)*, it is clever enough and they won't interfere with each other.
.. note:: NcPyAPI is clever enough to detect whether global authentication handler is enabled, and not perform authentication twice for performance reasons.

This chapter ends here, but the next topics are even more intriguing.
1 change: 0 additions & 1 deletion docs/NextcloudTalkBot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ Afterward, using FastAPI, you can define endpoints that will be invoked by Talk:
@APP.post("/currency_talk_bot")
async def currency_talk_bot(
_nc: Annotated[NextcloudApp, Depends(nc_app)],
message: Annotated[talk_bot.TalkBotMessage, Depends(atalk_bot_msg)],
background_tasks: BackgroundTasks,
):
Expand Down
7 changes: 4 additions & 3 deletions examples/as_app/skeleton/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
from fastapi import FastAPI

from nc_py_api import NextcloudApp
from nc_py_api.ex_app import LogLvl, run_app, set_handlers
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, run_app, set_handlers


@asynccontextmanager
async def lifespan(_app: FastAPI):
set_handlers(APP, enabled_handler)
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
yield


APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware) # set global AppAPI authentication middleware


def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
Expand Down
10 changes: 6 additions & 4 deletions examples/as_app/talk_bot/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
from fastapi import BackgroundTasks, Depends, FastAPI, Response

from nc_py_api import NextcloudApp, talk_bot
from nc_py_api.ex_app import atalk_bot_msg, nc_app, run_app, set_handlers
from nc_py_api.ex_app import AppAPIAuthMiddleware, atalk_bot_msg, run_app, set_handlers


# The same stuff as for usual External Applications
@asynccontextmanager
async def lifespan(_app: FastAPI):
set_handlers(APP, enabled_handler)
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
yield


APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)


# We define bot globally, so if no `multiprocessing` module is used, it can be reused by calls.
# All stuff in it works only with local variables, so in the case of multithreading, there should not be problems.
CURRENCY_BOT = talk_bot.TalkBot("/currency_talk_bot", "Currency convertor", "Usage: `@currency convert 100 EUR to USD`")
Expand Down Expand Up @@ -66,7 +69,6 @@ def currency_talk_bot_process_request(message: talk_bot.TalkBotMessage):

@APP.post("/currency_talk_bot")
async def currency_talk_bot(
_nc: Annotated[NextcloudApp, Depends(nc_app)],
message: Annotated[talk_bot.TalkBotMessage, Depends(atalk_bot_msg)],
background_tasks: BackgroundTasks,
):
Expand Down
2 changes: 1 addition & 1 deletion examples/as_app/talk_bot/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nc_py_api[app]>=0.5.0
nc_py_api[app]>=0.10.0
8 changes: 4 additions & 4 deletions examples/as_app/talk_bot_ai/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@

from nc_py_api import NextcloudApp, talk_bot
from nc_py_api.ex_app import (
AppAPIAuthMiddleware,
atalk_bot_msg,
get_model_path,
nc_app,
run_app,
set_handlers,
)


@asynccontextmanager
async def lifespan(_app: FastAPI):
set_handlers(APP, enabled_handler, models_to_fetch={MODEL_NAME: {}})
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler, models_to_fetch={MODEL_NAME: {}})
yield


APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)
AI_BOT = talk_bot.TalkBot("/ai_talk_bot", "AI talk bot", "Usage: `@assistant What sounds do cats make?`")
MODEL_NAME = "MBZUAI/LaMini-Flan-T5-783M"

Expand All @@ -40,7 +41,6 @@ def ai_talk_bot_process_request(message: talk_bot.TalkBotMessage):

@APP.post("/ai_talk_bot")
async def ai_talk_bot(
_nc: Annotated[NextcloudApp, Depends(nc_app)],
message: Annotated[talk_bot.TalkBotMessage, Depends(atalk_bot_msg)],
background_tasks: BackgroundTasks,
):
Expand Down
2 changes: 1 addition & 1 deletion examples/as_app/talk_bot_ai/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
nc_py_api[app]>=0.7.0
nc_py_api[app]>=0.10.0
transformers>=4.33
torch
torchvision
Expand Down
22 changes: 14 additions & 8 deletions examples/as_app/to_gif/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@
import tempfile
from contextlib import asynccontextmanager
from os import path
from typing import Annotated

import cv2
import imageio
import numpy
from fastapi import BackgroundTasks, Depends, FastAPI
from fastapi import BackgroundTasks, FastAPI
from pygifsicle import optimize
from requests import Response

from nc_py_api import FsNode, NextcloudApp
from nc_py_api.ex_app import LogLvl, UiActionFileInfo, nc_app, run_app, set_handlers
from nc_py_api.ex_app import (
AppAPIAuthMiddleware,
LogLvl,
UiActionFileInfo,
run_app,
set_handlers,
)


@asynccontextmanager
async def lifespan(_app: FastAPI):
set_handlers(APP, enabled_handler)
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
yield


APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)


def convert_video_to_gif(input_file: FsNode, nc: NextcloudApp):
def convert_video_to_gif(input_file: FsNode):
nc = NextcloudApp()
save_path = path.splitext(input_file.user_path)[0] + ".gif"
nc.log(LogLvl.WARNING, f"Processing:{input_file.user_path} -> {save_path}")
try:
Expand Down Expand Up @@ -70,10 +77,9 @@ def convert_video_to_gif(input_file: FsNode, nc: NextcloudApp):
@APP.post("/video_to_gif")
async def video_to_gif(
file: UiActionFileInfo,
nc: Annotated[NextcloudApp, Depends(nc_app)],
background_tasks: BackgroundTasks,
):
background_tasks.add_task(convert_video_to_gif, file.to_fs_node(), nc)
background_tasks.add_task(convert_video_to_gif, file.to_fs_node())
return Response()


Expand Down
2 changes: 1 addition & 1 deletion examples/as_app/to_gif/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
nc_py_api[app]>=0.7.0
nc_py_api[app]>=0.10.0
pygifsicle
imageio
opencv-python
Expand Down
12 changes: 5 additions & 7 deletions examples/as_app/ui_example/lib/main.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
"""Example with which we test UI elements."""

import random
import typing
from contextlib import asynccontextmanager

from fastapi import Depends, FastAPI, responses
from fastapi import FastAPI, responses
from pydantic import BaseModel

from nc_py_api import NextcloudApp
from nc_py_api.ex_app import (
AppAPIAuthMiddleware,
SettingsField,
SettingsFieldType,
SettingsForm,
nc_app,
run_app,
set_handlers,
)


@asynccontextmanager
async def lifespan(_app: FastAPI):
set_handlers(APP, enabled_handler)
async def lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
yield


APP = FastAPI(lifespan=lifespan)
APP.add_middleware(AppAPIAuthMiddleware)

SETTINGS_EXAMPLE = SettingsForm(
id="settings_example",
Expand Down Expand Up @@ -170,7 +170,6 @@ class Button1Format(BaseModel):

@APP.post("/verify_initial_value")
async def verify_initial_value(
_nc: typing.Annotated[NextcloudApp, Depends(nc_app)],
input1: Button1Format,
):
print("Old value: ", input1.initial_value)
Expand All @@ -190,7 +189,6 @@ class FileInfo(BaseModel):

@APP.post("/nextcloud_file")
async def nextcloud_file(
_nc: typing.Annotated[NextcloudApp, Depends(nc_app)],
args: dict,
):
print(args["file_info"])
Expand Down
2 changes: 1 addition & 1 deletion examples/as_app/ui_example/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nc_py_api[app]>=0.5.1
nc_py_api[app]>=0.10.0
Loading

0 comments on commit 8a030f1

Please sign in to comment.