From 755a30323913ef40396a7482517849f9a104ef8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 11:36:05 +0200 Subject: [PATCH 01/24] creates api definition, using that in sio_setup --- src/pydase/server/web_server/api.py | 88 +++++++++++++++++++++++ src/pydase/server/web_server/sio_setup.py | 32 +++++---- 2 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 src/pydase/server/web_server/api.py diff --git a/src/pydase/server/web_server/api.py b/src/pydase/server/web_server/api.py new file mode 100644 index 0000000..d0d6805 --- /dev/null +++ b/src/pydase/server/web_server/api.py @@ -0,0 +1,88 @@ +import logging +from typing import Any + +import aiohttp.web +import aiohttp_middlewares.error + +from pydase.data_service.state_manager import StateManager +from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict +from pydase.utils.helpers import get_object_attr_from_path +from pydase.utils.serialization.deserializer import loads +from pydase.utils.serialization.serializer import dump +from pydase.utils.serialization.types import SerializedObject + +logger = logging.getLogger(__name__) + +API_VERSION = "v1" + + +def update_value(state_manager: StateManager, data: UpdateDict) -> None: + path = data["access_path"] + + state_manager.set_service_attribute_value_by_path( + path=path, serialized_value=data["value"] + ) + + +def get_value(state_manager: StateManager, access_path: str) -> SerializedObject: + return state_manager._data_service_cache.get_value_dict_from_cache(access_path) + + +def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any: + method = get_object_attr_from_path(state_manager.service, data["access_path"]) + + serialized_args = data.get("args", None) + args = loads(serialized_args) if serialized_args else [] + + serialized_kwargs = data.get("kwargs", None) + kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {} + + return dump(method(*args, **kwargs)) + + +def create_api_application(state_manager: StateManager) -> aiohttp.web.Application: + api_application = aiohttp.web.Application( + middlewares=(aiohttp_middlewares.error.error_middleware(),) + ) + + async def _get_value(request: aiohttp.web.Request) -> aiohttp.web.Response: + logger.info("Handle api request: %s", request) + api_version = request.match_info["version"] + logger.info("Version number: %s", api_version) + + access_path = request.rel_url.query["access_path"] + + try: + result = get_value(state_manager, access_path) + except Exception as e: + logger.exception(e) + result = dump(e) + return aiohttp.web.json_response(result) + + async def _update_value(request: aiohttp.web.Request) -> aiohttp.web.Response: + data: UpdateDict = await request.json() + + try: + update_value(state_manager, data) + + return aiohttp.web.Response() + except Exception as e: + logger.exception(e) + return aiohttp.web.json_response(dump(e)) + + async def _trigger_method(request: aiohttp.web.Request) -> aiohttp.web.Response: + data: TriggerMethodDict = await request.json() + + try: + trigger_method(state_manager, data) + + return aiohttp.web.Response() + except Exception as e: + logger.exception(e) + return aiohttp.web.json_response(dump(e)) + + api_application.router.add_get("/{version}/get_value", _get_value) + api_application.router.add_post("/{version}/update_value", _update_value) + api_application.router.add_post("/{version}/trigger_method", _trigger_method) + + return api_application diff --git a/src/pydase/server/web_server/sio_setup.py b/src/pydase/server/web_server/sio_setup.py index aa160f4..e91c7a4 100644 --- a/src/pydase/server/web_server/sio_setup.py +++ b/src/pydase/server/web_server/sio_setup.py @@ -1,15 +1,21 @@ import asyncio import logging +import sys from typing import Any, TypedDict +if sys.version_info < (3, 11): + from typing_extensions import NotRequired +else: + from typing import NotRequired + import click import socketio # type: ignore[import-untyped] +import pydase.server.web_server.api import pydase.utils.serialization.deserializer import pydase.utils.serialization.serializer from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.state_manager import StateManager -from pydase.utils.helpers import get_object_attr_from_path from pydase.utils.logging import SocketIOHandler from pydase.utils.serialization.serializer import SerializedObject @@ -39,8 +45,8 @@ class UpdateDict(TypedDict): class TriggerMethodDict(TypedDict): access_path: str - args: SerializedObject - kwargs: SerializedObject + args: NotRequired[SerializedObject] + kwargs: NotRequired[SerializedObject] class RunMethodDict(TypedDict): @@ -137,21 +143,22 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> return state_manager.cache_manager.cache @sio.event - async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None: # type: ignore - path = data["access_path"] - + async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None: try: - state_manager.set_service_attribute_value_by_path( - path=path, serialized_value=data["value"] + pydase.server.web_server.api.update_value( + state_manager=state_manager, data=data ) except Exception as e: logger.exception(e) return dump(e) + return None @sio.event async def get_value(sid: str, access_path: str) -> SerializedObject: try: - return state_manager.cache_manager.get_value_dict_from_cache(access_path) + return pydase.server.web_server.api.get_value( + state_manager=state_manager, access_path=access_path + ) except Exception as e: logger.exception(e) return dump(e) @@ -159,12 +166,9 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> @sio.event async def trigger_method(sid: str, data: TriggerMethodDict) -> Any: try: - method = get_object_attr_from_path( - state_manager.service, data["access_path"] + return pydase.server.web_server.api.trigger_method( + state_manager=state_manager, data=data ) - args = loads(data["args"]) - kwargs: dict[str, Any] = loads(data["kwargs"]) - return dump(method(*args, **kwargs)) except Exception as e: logger.error(e) return dump(e) From aa55ac772e4f8d6b5d169aa97628d3bf94d183c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 11:36:20 +0200 Subject: [PATCH 02/24] using api application as web server api endpoint --- src/pydase/server/web_server/web_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pydase/server/web_server/web_server.py b/src/pydase/server/web_server/web_server.py index 1dd6c96..d6d994c 100644 --- a/src/pydase/server/web_server/web_server.py +++ b/src/pydase/server/web_server/web_server.py @@ -9,6 +9,7 @@ import aiohttp_middlewares.cors from pydase.config import ServiceConfig, WebServerConfig from pydase.data_service.data_service_observer import DataServiceObserver +from pydase.server.web_server.api import create_api_application from pydase.server.web_server.sio_setup import ( setup_sio_server, ) @@ -106,6 +107,7 @@ class WebServer: app.router.add_get("/service-properties", self._service_properties_route) app.router.add_get("/web-settings", self._web_settings_route) app.router.add_get("/custom.css", self._styles_route) + app.add_subapp("/api/", create_api_application(self.state_manager)) app.router.add_get(r"/", index) app.router.add_get(r"/{tail:.*}", index) From eaf76a7211b81edf1cfde296e033b300df554d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 12:08:26 +0200 Subject: [PATCH 03/24] fixing logging for aiohttp and SocketIOHandler --- src/pydase/server/web_server/sio_setup.py | 4 ++-- src/pydase/utils/logging.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pydase/server/web_server/sio_setup.py b/src/pydase/server/web_server/sio_setup.py index e91c7a4..1b2c3fc 100644 --- a/src/pydase/server/web_server/sio_setup.py +++ b/src/pydase/server/web_server/sio_setup.py @@ -175,5 +175,5 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> def setup_logging_handler(sio: socketio.AsyncServer) -> None: - logger = logging.getLogger() - logger.addHandler(SocketIOHandler(sio)) + logging.getLogger().addHandler(SocketIOHandler(sio)) + logging.getLogger("pydase").addHandler(SocketIOHandler(sio)) diff --git a/src/pydase/utils/logging.py b/src/pydase/utils/logging.py index edb027b..ddd6f11 100644 --- a/src/pydase/utils/logging.py +++ b/src/pydase/utils/logging.py @@ -38,6 +38,16 @@ LOGGING_CONFIG = { }, "loggers": { "pydase": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False}, + "aiohttp_middlewares": { + "handlers": ["default"], + "level": logging.WARNING, + "propagate": False, + }, + "aiohttp": { + "handlers": ["default"], + "level": logging.INFO, + "propagate": False, + }, }, } From e659ca9d1ca31c7c646437027153b762d47ec42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 12:08:38 +0200 Subject: [PATCH 04/24] adds requests to dev group --- poetry.lock | 150 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index dfe2ded..c329080 100644 --- a/poetry.lock +++ b/poetry.lock @@ -189,6 +189,116 @@ files = [ {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, ] +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -1793,6 +1903,27 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.2.2" @@ -1891,6 +2022,23 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "watchdog" version = "4.0.1" @@ -2071,4 +2219,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e5a96d8cc033c893a14aeb90e902923b10884e464b39ccf7a0a08e122a08b21b" +content-hash = "704ee6c0507dbbd884f3c9e3d8648d087732092e115d3adbf58e89405b31e5f2" diff --git a/pyproject.toml b/pyproject.toml index 51ffdcb..53833c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ pyright = "^1.1.323" pytest-mock = "^3.11.1" ruff = "^0.2.0" pytest-asyncio = "^0.23.2" +requests = "^2.32.3" [tool.poetry.group.docs] optional = true From 0e73239d0866416ca950aa11fc7c8980c6fdf5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 13:24:34 +0200 Subject: [PATCH 05/24] adds API versioning --- src/pydase/server/web_server/api/__init__.py | 24 ++++++++++ .../server/web_server/api/v1/__init__.py | 0 .../{api.py => api/v1/application.py} | 46 +++++-------------- .../server/web_server/api/v1/endpoints.py | 32 +++++++++++++ src/pydase/server/web_server/sio_setup.py | 8 ++-- 5 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 src/pydase/server/web_server/api/__init__.py create mode 100644 src/pydase/server/web_server/api/v1/__init__.py rename src/pydase/server/web_server/{api.py => api/v1/application.py} (52%) create mode 100644 src/pydase/server/web_server/api/v1/endpoints.py diff --git a/src/pydase/server/web_server/api/__init__.py b/src/pydase/server/web_server/api/__init__.py new file mode 100644 index 0000000..c63dafb --- /dev/null +++ b/src/pydase/server/web_server/api/__init__.py @@ -0,0 +1,24 @@ +import logging + +import aiohttp.web +import aiohttp_middlewares.error + +import pydase.server.web_server.api.v1.application +from pydase.data_service.state_manager import StateManager + +logger = logging.getLogger(__name__) + + +def create_api_application(state_manager: StateManager) -> aiohttp.web.Application: + api_application = aiohttp.web.Application( + middlewares=(aiohttp_middlewares.error.error_middleware(),) + ) + + api_application.add_subapp( + "/v1/", + pydase.server.web_server.api.v1.application.create_api_application( + state_manager + ), + ) + + return api_application diff --git a/src/pydase/server/web_server/api/v1/__init__.py b/src/pydase/server/web_server/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pydase/server/web_server/api.py b/src/pydase/server/web_server/api/v1/application.py similarity index 52% rename from src/pydase/server/web_server/api.py rename to src/pydase/server/web_server/api/v1/application.py index d0d6805..41868a7 100644 --- a/src/pydase/server/web_server/api.py +++ b/src/pydase/server/web_server/api/v1/application.py @@ -1,45 +1,25 @@ import logging -from typing import Any +from typing import TYPE_CHECKING import aiohttp.web import aiohttp_middlewares.error from pydase.data_service.state_manager import StateManager -from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict -from pydase.utils.helpers import get_object_attr_from_path -from pydase.utils.serialization.deserializer import loads +from pydase.server.web_server.api.v1.endpoints import ( + get_value, + trigger_method, + update_value, +) from pydase.utils.serialization.serializer import dump -from pydase.utils.serialization.types import SerializedObject + +if TYPE_CHECKING: + from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict logger = logging.getLogger(__name__) API_VERSION = "v1" -def update_value(state_manager: StateManager, data: UpdateDict) -> None: - path = data["access_path"] - - state_manager.set_service_attribute_value_by_path( - path=path, serialized_value=data["value"] - ) - - -def get_value(state_manager: StateManager, access_path: str) -> SerializedObject: - return state_manager._data_service_cache.get_value_dict_from_cache(access_path) - - -def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any: - method = get_object_attr_from_path(state_manager.service, data["access_path"]) - - serialized_args = data.get("args", None) - args = loads(serialized_args) if serialized_args else [] - - serialized_kwargs = data.get("kwargs", None) - kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {} - - return dump(method(*args, **kwargs)) - - def create_api_application(state_manager: StateManager) -> aiohttp.web.Application: api_application = aiohttp.web.Application( middlewares=(aiohttp_middlewares.error.error_middleware(),) @@ -47,8 +27,6 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati async def _get_value(request: aiohttp.web.Request) -> aiohttp.web.Response: logger.info("Handle api request: %s", request) - api_version = request.match_info["version"] - logger.info("Version number: %s", api_version) access_path = request.rel_url.query["access_path"] @@ -81,8 +59,8 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati logger.exception(e) return aiohttp.web.json_response(dump(e)) - api_application.router.add_get("/{version}/get_value", _get_value) - api_application.router.add_post("/{version}/update_value", _update_value) - api_application.router.add_post("/{version}/trigger_method", _trigger_method) + api_application.router.add_get("/get_value", _get_value) + api_application.router.add_post("/update_value", _update_value) + api_application.router.add_post("/trigger_method", _trigger_method) return api_application diff --git a/src/pydase/server/web_server/api/v1/endpoints.py b/src/pydase/server/web_server/api/v1/endpoints.py new file mode 100644 index 0000000..bad0dcf --- /dev/null +++ b/src/pydase/server/web_server/api/v1/endpoints.py @@ -0,0 +1,32 @@ +from typing import Any + +from pydase.data_service.state_manager import StateManager +from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict +from pydase.utils.helpers import get_object_attr_from_path +from pydase.utils.serialization.deserializer import loads +from pydase.utils.serialization.serializer import dump +from pydase.utils.serialization.types import SerializedObject + + +def update_value(state_manager: StateManager, data: UpdateDict) -> None: + path = data["access_path"] + + state_manager.set_service_attribute_value_by_path( + path=path, serialized_value=data["value"] + ) + + +def get_value(state_manager: StateManager, access_path: str) -> SerializedObject: + return state_manager._data_service_cache.get_value_dict_from_cache(access_path) + + +def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any: + method = get_object_attr_from_path(state_manager.service, data["access_path"]) + + serialized_args = data.get("args", None) + args = loads(serialized_args) if serialized_args else [] + + serialized_kwargs = data.get("kwargs", None) + kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {} + + return dump(method(*args, **kwargs)) diff --git a/src/pydase/server/web_server/sio_setup.py b/src/pydase/server/web_server/sio_setup.py index 1b2c3fc..5187f85 100644 --- a/src/pydase/server/web_server/sio_setup.py +++ b/src/pydase/server/web_server/sio_setup.py @@ -11,7 +11,7 @@ else: import click import socketio # type: ignore[import-untyped] -import pydase.server.web_server.api +import pydase.server.web_server.api.v1.endpoints import pydase.utils.serialization.deserializer import pydase.utils.serialization.serializer from pydase.data_service.data_service_observer import DataServiceObserver @@ -145,7 +145,7 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> @sio.event async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None: try: - pydase.server.web_server.api.update_value( + pydase.server.web_server.api.v1.endpoints.update_value( state_manager=state_manager, data=data ) except Exception as e: @@ -156,7 +156,7 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> @sio.event async def get_value(sid: str, access_path: str) -> SerializedObject: try: - return pydase.server.web_server.api.get_value( + return pydase.server.web_server.api.v1.endpoints.get_value( state_manager=state_manager, access_path=access_path ) except Exception as e: @@ -166,7 +166,7 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) -> @sio.event async def trigger_method(sid: str, data: TriggerMethodDict) -> Any: try: - return pydase.server.web_server.api.trigger_method( + return pydase.server.web_server.api.v1.endpoints.trigger_method( state_manager=state_manager, data=data ) except Exception as e: From 6f4fcf52dd3416aa21bb07225fc7ad3211093b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 16:32:21 +0200 Subject: [PATCH 06/24] adds user guide for restful api --- docs/user-guide/Interaction.md | 9 ++ docs/user-guide/RESTful API.md | 15 ++ docs/user-guide/openapi.yaml | 256 +++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 docs/user-guide/Interaction.md create mode 100644 docs/user-guide/RESTful API.md create mode 100644 docs/user-guide/openapi.yaml diff --git a/docs/user-guide/Interaction.md b/docs/user-guide/Interaction.md new file mode 100644 index 0000000..fcd9114 --- /dev/null +++ b/docs/user-guide/Interaction.md @@ -0,0 +1,9 @@ +# Interacting with `pydase` Services + +`pydase` offers multiple ways for users to interact with the services they create, providing flexibility and convenience for different use cases. This section outlines the primary interaction methods available, including RESTful APIs, Python client based on Socket.IO, and the auto-generated frontend. + +{% + include-markdown "./RESTful API.md" + heading-offset=1 +%} + diff --git a/docs/user-guide/RESTful API.md b/docs/user-guide/RESTful API.md new file mode 100644 index 0000000..47939b7 --- /dev/null +++ b/docs/user-guide/RESTful API.md @@ -0,0 +1,15 @@ +# RESTful API + +The `pydase` RESTful API provides access to various functionalities through specific routes. Below are the available endpoints for version 1 (`v1`) of the API, including details on request methods, parameters, and example usage. + +## Base URL + +``` +http://:/api/v1/ +``` + + + +## Change Log + +- v0.9.0: Initial release with `get_value`, `update_value`, and `trigger_method` endpoints. diff --git a/docs/user-guide/openapi.yaml b/docs/user-guide/openapi.yaml new file mode 100644 index 0000000..5073456 --- /dev/null +++ b/docs/user-guide/openapi.yaml @@ -0,0 +1,256 @@ +openapi: 3.1.0 +info: + title: Pydase RESTful API +tags: + - name: /api/v1 + description: Version 1 +paths: + /api/v1/get_value: + get: + tags: + - /api/v1 + summary: Get the value of an existing attribute. + description: Get the value of an existing attribute by full access path. + operationId: getValue + parameters: + - in: query + name: access_path + schema: + type: string + example: device.channel[0].voltage + required: true + description: Full access path of the service attribute. + responses: + '200': + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/SerializedAttribute' + examples: + Exists: + summary: Attribute exists + value: + docs: My documentation string. + full_access_path: device.channel[0].voltage + readonly: false + type: float + value: 12.1 + DoesNotExist: + summary: Attribute or does not exist + value: + docs: null + full_access_path: device.channel[0].voltage + readonly: false + type: "None" + value: null + /api/v1/update_value: + put: + tags: + - /api/v1 + summary: Update an existing attribute. + description: Update an existing attribute by full access path. + operationId: updateValue + requestBody: + description: Update an existent attribute in the service + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateValue' + required: true + responses: + '200': + description: Successful Operation + '400': + description: Could not Update Attribute + content: + application/json: + schema: + $ref: '#/components/schemas/SerializedException' + examples: + List: + summary: List out of index + value: + docs: null + full_access_path: "" + name: SerializationPathError + readonly: false + type: Exception + value: "Index '2': list index out of range" + Attribute: + summary: Attribute or does not exist + value: + docs: null + full_access_path: "" + name: SerializationPathError + readonly: false + type: Exception + value: "Key 'invalid_attribute': 'invalid_attribute'." + /api/v1/trigger_method: + put: + tags: + - /api/v1 + summary: Trigger method. + description: Trigger method with by full access path with provided args and kwargs. + operationId: triggerMethod + requestBody: + description: Update an existent attribute in the service + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerMethod' + required: true + responses: + '200': + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/SerializedAttribute' + examples: + NoneReturn: + summary: Function returns None + value: + docs: null + full_access_path: "" + readonly: false + type: "NoneType" + value: null + FloatReturn: + summary: Function returns float + value: + docs: null + full_access_path: "" + readonly: false + type: "float" + value: 23.2 +components: + schemas: + UpdateValue: + required: + - access_path + - value + type: object + properties: + access_path: + type: string + example: device.channel[0].voltage + value: + $ref: '#/components/schemas/SerializedValue' + TriggerMethod: + required: + - access_path + type: object + properties: + access_path: + type: string + example: device.channel[0].voltage + args: + type: object + required: + - type + - value + - full_access_path + properties: + full_access_path: + type: string + example: "" + type: + type: string + enum: + - list + value: + type: array + items: + $ref: '#/components/schemas/SerializedValue' + kwargs: + type: object + required: + - type + - value + - full_access_path + properties: + full_access_path: + type: string + example: "" + type: + type: string + enum: + - dict + value: + type: object + additionalProperties: + $ref: '#/components/schemas/SerializedValue' + SerializedValue: + required: + - full_access_path + - type + - value + type: object + properties: + docs: + type: string | null + example: null + full_access_path: + type: string + example: "" + readonly: + type: boolean + example: false + type: + type: string + example: float + value: + type: any + example: 22.0 + SerializedAttribute: + required: + - full_access_path + - type + - value + type: object + properties: + docs: + type: string | null + example: My documentation string. + full_access_path: + type: string + example: device.channel[0].voltage + readonly: + type: boolean + example: false + type: + type: string + example: float + value: + type: any + example: 22.0 + SerializedException: + required: + - full_access_path + - type + - value + type: object + properties: + docs: + type: string | null + example: Raised when the access path does not correspond to a valid attribute. + full_access_path: + type: string + example: "" + name: + type: string + example: SerializationPathError + readonly: + type: boolean + example: false + type: + type: string + example: Exception + value: + type: string + examples: + value: + "Index '2': list index out of range" + some: + "Index '2': list index out of range" From 95d29ee4e822e306b4b9c6835d5ea1475b473179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 16:34:07 +0200 Subject: [PATCH 07/24] return method results over http --- src/pydase/server/web_server/api/v1/application.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pydase/server/web_server/api/v1/application.py b/src/pydase/server/web_server/api/v1/application.py index 41868a7..aa3cbab 100644 --- a/src/pydase/server/web_server/api/v1/application.py +++ b/src/pydase/server/web_server/api/v1/application.py @@ -43,18 +43,17 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati try: update_value(state_manager, data) - return aiohttp.web.Response() + return aiohttp.web.json_response() except Exception as e: logger.exception(e) - return aiohttp.web.json_response(dump(e)) + return aiohttp.web.json_response(dump(e), status=400) async def _trigger_method(request: aiohttp.web.Request) -> aiohttp.web.Response: data: TriggerMethodDict = await request.json() try: - trigger_method(state_manager, data) + return aiohttp.web.json_response(trigger_method(state_manager, data)) - return aiohttp.web.Response() except Exception as e: logger.exception(e) return aiohttp.web.json_response(dump(e)) From 9ce0c93954a6f03563e8c2c822f6fccaec580c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 25 Jul 2024 16:37:41 +0200 Subject: [PATCH 08/24] adds swagger-ui-tag python dep to render swagger ui, updates mkdocs to include new page --- mkdocs.yml | 4 +++- poetry.lock | 48 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 4b1d504..53450e3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,8 +4,9 @@ edit_uri: blob/docs/docs/ nav: - Home: index.md - Getting Started: getting-started.md - - User Guide: + - User Guide: - Components Guide: user-guide/Components.md + - Interacting with Services: user-guide/Interaction.md - Developer Guide: - Developer Guide: dev-guide/README.md - API Reference: dev-guide/api.md @@ -37,6 +38,7 @@ plugins: - include-markdown - search - mkdocstrings + - swagger-ui-tag watch: - src/pydase diff --git a/poetry.lock b/poetry.lock index c329080..d1aea50 100644 --- a/poetry.lock +++ b/poetry.lock @@ -178,6 +178,27 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "bidict" version = "0.23.1" @@ -1089,6 +1110,20 @@ files = [ dev = ["bump2version (==1.0.1)", "mkdocs (==1.4.0)", "pre-commit", "pytest (==7.1.3)", "pytest-cov (==3.0.0)", "tox"] test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"] +[[package]] +name = "mkdocs-swagger-ui-tag" +version = "0.6.10" +description = "A MkDocs plugin supports for add Swagger UI in page." +optional = false +python-versions = "*" +files = [ + {file = "mkdocs-swagger-ui-tag-0.6.10.tar.gz", hash = "sha256:811d55e0905bfecc5f2e743b5b1e4d03b663707d5472984cc7b21f45117dcb29"}, + {file = "mkdocs_swagger_ui_tag-0.6.10-py3-none-any.whl", hash = "sha256:839373498a42202b3824068064919ad6ddf2e98d5a8deed7d4132719221c0528"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.11.1" + [[package]] name = "mkdocstrings" version = "0.22.0" @@ -1978,6 +2013,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + [[package]] name = "toml" version = "0.10.2" @@ -2219,4 +2265,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "704ee6c0507dbbd884f3c9e3d8648d087732092e115d3adbf58e89405b31e5f2" +content-hash = "3f397396c238541d7a07327c9b289843124d8d4dd6b1d96a3393f3be2be38fa4" diff --git a/pyproject.toml b/pyproject.toml index 53833c9..ec52d97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ mkdocs = "^1.5.2" mkdocs-include-markdown-plugin = "^3.9.1" mkdocstrings = "^0.22.0" pymdown-extensions = "^10.1" +mkdocs-swagger-ui-tag = "^0.6.10" [build-system] requires = ["poetry-core"] From baad1268e8b4d0566c440a42444922c9b9dffa89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 11:08:27 +0200 Subject: [PATCH 09/24] updates documentation - using material theme instead of readthedocs - introducing "Interacting with pydase Services" guide - restful api docs - auto-generated frontend - pydase.Client --- README.md | 160 ++++++++-------- docs/dev-guide/Adding_Components.md | 17 +- .../Tailoring frontend component layout.png | Bin 0 -> 22986 bytes .../Tailoring_frontend_component_layout.png | Bin 18368 -> 0 bytes docs/user-guide/Interaction.md | 9 - docs/user-guide/RESTful API.md | 15 -- .../interaction/Auto-generated Frontend.md | 167 +++++++++++++++++ docs/user-guide/interaction/Python Client.md | 45 +++++ docs/user-guide/interaction/RESTful API.md | 14 ++ docs/user-guide/interaction/main.md | 81 ++++++++ .../user-guide/{ => interaction}/openapi.yaml | 3 +- mkdocs.yml | 13 +- poetry.lock | 174 +++++++++++++++++- pyproject.toml | 2 +- 14 files changed, 577 insertions(+), 123 deletions(-) create mode 100644 docs/images/Tailoring frontend component layout.png delete mode 100644 docs/images/Tailoring_frontend_component_layout.png delete mode 100644 docs/user-guide/Interaction.md delete mode 100644 docs/user-guide/RESTful API.md create mode 100644 docs/user-guide/interaction/Auto-generated Frontend.md create mode 100644 docs/user-guide/interaction/Python Client.md create mode 100644 docs/user-guide/interaction/RESTful API.md create mode 100644 docs/user-guide/interaction/main.md rename docs/user-guide/{ => interaction}/openapi.yaml (99%) diff --git a/README.md b/README.md index 6b194b8..8111121 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# pydase (Python Data Service) +# pydase [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=latest) -`pydase` is a Python library for creating data service servers with integrated web and RPC servers. It's designed to handle the management of data structures, automated tasks, and callbacks, and provides built-in functionality for serving data over different protocols. +`pydase` is a Python library designed to streamline the creation of services that interface with devices and data. It offers a unified API, simplifying the process of data querying and device interaction. Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates rapid service development and deployment. - [Features](#features) - [Installation](#installation) @@ -45,11 +45,11 @@ ## Features -- [Simple data service definition through class-based interface](#defining-a-dataService) -- [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface) +- [Simple service definition through class-based interface](#defining-a-dataService) +- [Integrated web interface for interactive access and control of your service](#accessing-the-web-interface) - [Support for programmatic control and interaction with your service](#connecting-to-the-service-via-python-client) - [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system) -- [Customizable styling for the web interface through user-defined CSS](#customizing-web-interface-style) +- [Customizable styling for the web interface](#customizing-web-interface-style) - [Saving and restoring the service state for service persistence](#understanding-service-persistence) - [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase) - [Support for units](#understanding-units-in-pydase) @@ -510,96 +510,96 @@ In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` - Accessing parent class resources in `NumberSlider` - In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events. + In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events. - Here's an illustrative example: + Here's an illustrative example: - ```python - from collections.abc import Callable + ```python + from collections.abc import Callable - import pydase - import pydase.components + import pydase + import pydase.components - class MySlider(pydase.components.NumberSlider): - def __init__( - self, - value: float, - on_change: Callable[[float], None], - ) -> None: - super().__init__(value=value) - self._on_change = on_change + class MySlider(pydase.components.NumberSlider): + def __init__( + self, + value: float, + on_change: Callable[[float], None], + ) -> None: + super().__init__(value=value) + self._on_change = on_change - # ... other properties ... + # ... other properties ... - @property - def value(self) -> float: - return self._value + @property + def value(self) -> float: + return self._value - @value.setter - def value(self, new_value: float) -> None: - if new_value < self._min or new_value > self._max: - raise ValueError("Value is either below allowed min or above max value.") - self._value = new_value - self._on_change(new_value) + @value.setter + def value(self, new_value: float) -> None: + if new_value < self._min or new_value > self._max: + raise ValueError("Value is either below allowed min or above max value.") + self._value = new_value + self._on_change(new_value) - class MyService(pydase.DataService): - def __init__(self) -> None: - self.voltage = MySlider( - 5, - on_change=self.handle_voltage_change, - ) + class MyService(pydase.DataService): + def __init__(self) -> None: + self.voltage = MySlider( + 5, + on_change=self.handle_voltage_change, + ) - def handle_voltage_change(self, new_voltage: float) -> None: - print(f"Voltage changed to: {new_voltage}") - # Additional logic here + def handle_voltage_change(self, new_voltage: float) -> None: + print(f"Voltage changed to: {new_voltage}") + # Additional logic here - if __name__ == "__main__": - service_instance = MyService() - my_service.voltage.value = 7 # Output: "Voltage changed to: 7" - pydase.Server(service_instance).run() - ``` + if __name__ == "__main__": + service_instance = MyService() + my_service.voltage.value = 7 # Output: "Voltage changed to: 7" + pydase.Server(service_instance).run() + ``` - Incorporating units in `NumberSlider` - The `NumberSlider` is capable of [displaying units](#understanding-units-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend. + The `NumberSlider` is capable of [displaying units](#understanding-units-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend. - Here's how to implement a `NumberSlider` with unit display: + Here's how to implement a `NumberSlider` with unit display: - ```python - import pydase - import pydase.components - import pydase.units as u + ```python + import pydase + import pydase.components + import pydase.units as u - class MySlider(pydase.components.NumberSlider): - def __init__( - self, - value: u.Quantity = 0.0 * u.units.V, - ) -> None: - super().__init__(value) + class MySlider(pydase.components.NumberSlider): + def __init__( + self, + value: u.Quantity = 0.0 * u.units.V, + ) -> None: + super().__init__(value) - @property - def value(self) -> u.Quantity: - return self._value + @property + def value(self) -> u.Quantity: + return self._value - @value.setter - def value(self, value: u.Quantity) -> None: - if value.m < self._min or value.m > self._max: - raise ValueError("Value is either below allowed min or above max value.") - self._value = value + @value.setter + def value(self, value: u.Quantity) -> None: + if value.m < self._min or value.m > self._max: + raise ValueError("Value is either below allowed min or above max value.") + self._value = value - class MyService(pydase.DataService): - def __init__(self) -> None: - super().__init__() - self.voltage = MySlider() + class MyService(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self.voltage = MySlider() - if __name__ == "__main__": - service_instance = MyService() - service_instance.voltage.value = 5 * u.units.V - print(service_instance.voltage.value) # Output: 5 V - pydase.Server(service_instance).run() - ``` + if __name__ == "__main__": + service_instance = MyService() + service_instance.voltage.value = 5 * u.units.V + print(service_instance.voltage.value) # Output: 5 V + pydase.Server(service_instance).run() + ``` #### `ColouredEnum` @@ -922,8 +922,8 @@ import pydase class Device(pydase.DataService): name = "My Device" - some_float = 1.0 - some_int = 1 + temperature = 1.0 + power = 1 class Service(pydase.DataService): @@ -946,11 +946,13 @@ with the following `web_settings.json` "device.name": { "display": false }, - "device.some_float": { + "device.power": { + "displayName": "Power", "displayOrder": 1 }, - "device.some_int": { - "displayOrder": 0 + "device.temperature": { + "displayName": "Temperature", + "displayOrder": 0 }, "state": { "displayOrder": 0 @@ -960,7 +962,7 @@ with the following `web_settings.json` looks like this: -![Tailoring frontend component layout](./docs/images/Tailoring_frontend_component_layout.png) +![Tailoring frontend component layout](./docs/images/Tailoring frontend component layout.png) ### Specifying a Custom Frontend Source diff --git a/docs/dev-guide/Adding_Components.md b/docs/dev-guide/Adding_Components.md index 54ec087..ab5b137 100644 --- a/docs/dev-guide/Adding_Components.md +++ b/docs/dev-guide/Adding_Components.md @@ -111,7 +111,7 @@ Write the React component code, following the structure and patterns used in exi For example, for the `Image` component, a template could look like this: -```tsx +```ts import React, { useEffect, useRef, useState } from 'react'; import { Card, Collapse, Image } from 'react-bootstrap'; import { DocStringComponent } from './DocStringComponent'; @@ -203,8 +203,7 @@ There are two different events a component might want to trigger: updating an at For illustration, take the `ButtonComponent`. When the button state changes, we want to send this update to the backend: - ```tsx - // file: frontend/src/components/ButtonComponent.tsx + ```ts title="frontend/src/components/ButtonComponent.tsx" // ... (import statements) type ButtonComponentProps = { @@ -249,7 +248,7 @@ There are two different events a component might want to trigger: updating an at To see how to use the `MethodComponent` in your component, have a look at the `DeviceConnection.tsx` file. Here is an example that demonstrates the usage of the `runMethod` function (also, have a look at the `MethodComponent.tsx` file): - ```tsx + ```ts title="frontend/src/components/_YourComponent_.tsx" import { runMethod } from '../socket'; // ... (other imports) @@ -287,9 +286,7 @@ The `GenericComponent` is responsible for rendering different types of component At the beginning of the `GenericComponent` file, import the newly created `ImageComponent`: -```tsx -// file: frontend/src/components/GenericComponent.tsx - +```ts title="frontend/src/components/GenericComponent.tsx" import { ImageComponent } from './ImageComponent'; ``` @@ -299,7 +296,7 @@ Update the `AttributeType` type definition to include the new type for the `Imag For example, if the new attribute type is `'Image'` (which should correspond to the name of the backend component class), you can add it to the union: -```tsx +```ts type AttributeType = | 'str' | 'bool' @@ -318,7 +315,7 @@ type AttributeType = Inside the `GenericComponent` function, add a new conditional branch to render the `ImageComponent` when the attribute type is `'Image'`: -```tsx +```ts } else if (attribute.type === 'Image') { return ( element).join('.'); useEffect(() => { diff --git a/docs/images/Tailoring frontend component layout.png b/docs/images/Tailoring frontend component layout.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4cf22b40d10e19803d26de58c78d3af405ce2f GIT binary patch literal 22986 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 BDHQ+! literal 0 HcmV?d00001 diff --git a/docs/images/Tailoring_frontend_component_layout.png b/docs/images/Tailoring_frontend_component_layout.png deleted file mode 100644 index 02c34c1d8164c3e0fdef025ff250a1f05f3094a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18368 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 diff --git a/docs/user-guide/Interaction.md b/docs/user-guide/Interaction.md deleted file mode 100644 index fcd9114..0000000 --- a/docs/user-guide/Interaction.md +++ /dev/null @@ -1,9 +0,0 @@ -# Interacting with `pydase` Services - -`pydase` offers multiple ways for users to interact with the services they create, providing flexibility and convenience for different use cases. This section outlines the primary interaction methods available, including RESTful APIs, Python client based on Socket.IO, and the auto-generated frontend. - -{% - include-markdown "./RESTful API.md" - heading-offset=1 -%} - diff --git a/docs/user-guide/RESTful API.md b/docs/user-guide/RESTful API.md deleted file mode 100644 index 47939b7..0000000 --- a/docs/user-guide/RESTful API.md +++ /dev/null @@ -1,15 +0,0 @@ -# RESTful API - -The `pydase` RESTful API provides access to various functionalities through specific routes. Below are the available endpoints for version 1 (`v1`) of the API, including details on request methods, parameters, and example usage. - -## Base URL - -``` -http://:/api/v1/ -``` - - - -## Change Log - -- v0.9.0: Initial release with `get_value`, `update_value`, and `trigger_method` endpoints. diff --git a/docs/user-guide/interaction/Auto-generated Frontend.md b/docs/user-guide/interaction/Auto-generated Frontend.md new file mode 100644 index 0000000..a8b6c29 --- /dev/null +++ b/docs/user-guide/interaction/Auto-generated Frontend.md @@ -0,0 +1,167 @@ +# Auto-generated Frontend + +`pydase` automatically generates a frontend interface based on your service definition, representing the current state and controls of the service. +It simplifies the process of visualization and control of the data and devices managed by your `pydase` service, making it accessible to both developers and end-users. + +Through the integration of Socket.IO, the frontend provides real-time updates, reflecting changes as they occur and allowing for immediate interaction with the backend. + + +## Accessing the Frontend + +You can access the auto-generated frontend by navigating to the hostname of the device the service is hosted on, followed by the exposed port: + +``` +http://:/ +``` + +The frontend uses a component-based approach, representing various data types and control mechanisms as distinct UI components. For more information about this, please refer to [Components Guide](../Components.md). + +## Customization Options + +`pydase` allows you to enhance the user experience by customizing the web interface's appearance through + +1. a custom CSS file, and +2. tailoring the frontend component layout and display style. + +For more advanced customization, you can provide a completely custom frontend source. + +### Custom CSS Styling + +You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization. +Here's how you can use this feature: + +1. Prepare your custom CSS file with the desired styles. + +2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file. + +```python +from pydase import Server, DataService + + +class MyService(DataService): + # ... your service definition ... + + +if __name__ == "__main__": + service = MyService() + server = Server(service, css="path/to/your/custom.css").run() +``` + +This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility. + +Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup. + +### Tailoring Frontend Component Layout + +You can customize the display names, visibility, and order of components via the `web_settings.json` file. +Each key in the file corresponds to the full access path of public attributes, properties, and methods of the exposed service, using dot-notation. + +- **Custom Display Names**: Modify the `"displayName"` value in the file to change how each component appears in the frontend. +- **Control Component Visibility**: Utilize the `"display"` key-value pair to control whether a component is rendered in the frontend. Set the value to `true` to make the component visible or `false` to hide it. +- **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. The value defaults to [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). + +The `web_settings.json` file will be stored in the directory specified by `SERVICE_CONFIG_DIR`. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](#configuring-pydase-via-environment-variables). + +For example, styling the following service + +```python +import pydase + + +class Device(pydase.DataService): + name = "My Device" + temperature = 1.0 + power = 1 + + +class Service(pydase.DataService): + device = Device() + state = "RUNNING" + + +if __name__ == "__main__": + pydase.Server(Service()).run() +``` + +with the following `web_settings.json` + +```json +{ + "device": { + "displayName": "My Device", + "displayOrder": 1 + }, + "device.name": { + "display": false + }, + "device.power": { + "displayName": "Power", + "displayOrder": 1 + }, + "device.temperature": { + "displayName": "Temperature", + "displayOrder": 0 + }, + "state": { + "displayOrder": 0 + } +} +``` + +looks like this: + +![Tailoring frontend component layout](../../images/Tailoring frontend component layout.png) + +### Specifying a Custom Frontend Source + +To further customize your web interface, you can provide a custom frontend source. +By specifying the `frontend_src` parameter when initializing the server, you can host a tailored frontend application: + +```python +from pathlib import Path + +import pydase + + +class MyService(pydase.DataService): + # Service definition + + +if __name__ == "__main__": + service = MyService() + pydase.Server( + service, + frontend_src=Path("path/to/your/frontend/directory"), + ).run() +``` + +`pydase` expects a directory structured as follows: + +```bash title="Frontend directory structure" + +├── assets +│   └── ... +└── index.html +``` + +Any CSS, js, image or other files need to be put into the assets folder for the web server to be able to provide access to it. + +#### Example: Custom React Frontend + +You can use vite to generate a react app template: + +```bash +npm create vite@latest my-react-app -- --template react +``` + +*TODO: Add some useful information here...* + +To deploy the custom react frontend, build it with + +```bash +npm run build +``` + +and pass the relative path of the output directory to the `frontend_src` parameter of the `pydase.Server`. + +**Note** that you have to make sure that all the generated files (except the `index.html`) are in the `assets` folder. In the react app, you can achieve this by not using the `public` folder, but instead using e.g. `src/assets`. diff --git a/docs/user-guide/interaction/Python Client.md b/docs/user-guide/interaction/Python Client.md new file mode 100644 index 0000000..ea844f2 --- /dev/null +++ b/docs/user-guide/interaction/Python Client.md @@ -0,0 +1,45 @@ +# Python Client + +You can connect to the service using the `pydase.Client`. Below is an example of how to establish a connection to a service and interact with it: + +```python +import pydase + +# Replace the hostname and port with the IP address and the port of the machine +# where the service is running, respectively +client_proxy = pydase.Client(hostname="", port=8001).proxy + +# Interact with the service attributes as if they were local +client_proxy.voltage = 5.0 +print(client_proxy.voltage) # Expected output: 5.0 +``` + +This example demonstrates setting and retrieving the `voltage` attribute through the client proxy. +The proxy acts as a local representative of the remote service, enabling straightforward interaction. + +The proxy class dynamically synchronizes with the server's exposed attributes. This synchronization allows the proxy to be automatically updated with any attributes or methods that the server exposes, essentially mirroring the server's API. This dynamic updating enables users to interact with the remote service as if they were working with a local object. + +## Tab Completion Support + +In interactive environments such as Python interpreters and Jupyter notebooks, the proxy class supports tab completion, which allows users to explore available methods and attributes. + +## Integration within Other Services + +You can also integrate a client proxy within another service. Here's how you can set it up: + +```python +import pydase + +class MyService(pydase.DataService): + # Initialize the client without blocking the constructor + proxy = pydase.Client(hostname="", port=8001, block_until_connected=False).proxy + +if __name__ == "__main__": + service = MyService() + # Create a server that exposes this service; adjust the web_port as needed + server = pydase.Server(service, web_port=8002). run() +``` + +In this setup, the `MyService` class has a `proxy` attribute that connects to a `pydase` service located at `:8001`. +The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails. +This configuration is particularly useful in distributed systems where services may start in any order. diff --git a/docs/user-guide/interaction/RESTful API.md b/docs/user-guide/interaction/RESTful API.md new file mode 100644 index 0000000..ecd76de --- /dev/null +++ b/docs/user-guide/interaction/RESTful API.md @@ -0,0 +1,14 @@ +# RESTful API + +The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. This is particularly useful for integrating `pydase` services with other applications or for scripting and automation. + +To help developers understand and utilize the API, we provide an OpenAPI specification. This specification describes the available endpoints and corresponding request/response formats. + +## OpenAPI Specification +**Base URL**: + +``` +http://:/api/ +``` + + diff --git a/docs/user-guide/interaction/main.md b/docs/user-guide/interaction/main.md new file mode 100644 index 0000000..8c6b19b --- /dev/null +++ b/docs/user-guide/interaction/main.md @@ -0,0 +1,81 @@ +# Interacting with `pydase` Services + +`pydase` offers multiple ways for users to interact with the services they create, providing flexibility and convenience for different use cases. This section outlines the primary interaction methods available, including an auto-generated frontend, a RESTful API, and a Python client based on Socket.IO. + +{% + include-markdown "./Auto-generated Frontend.md" + heading-offset=1 +%} + +{% + include-markdown "./RESTful API.md" + heading-offset=1 +%} + +{% + include-markdown "./Python Client.md" + heading-offset=1 +%} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/user-guide/openapi.yaml b/docs/user-guide/interaction/openapi.yaml similarity index 99% rename from docs/user-guide/openapi.yaml rename to docs/user-guide/interaction/openapi.yaml index 5073456..b2c2def 100644 --- a/docs/user-guide/openapi.yaml +++ b/docs/user-guide/interaction/openapi.yaml @@ -1,6 +1,7 @@ openapi: 3.1.0 info: - title: Pydase RESTful API + version: 1.0.0 + title: pydase API tags: - name: /api/v1 description: Version 1 diff --git a/mkdocs.yml b/mkdocs.yml index 53450e3..3b257d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ nav: - Getting Started: getting-started.md - User Guide: - Components Guide: user-guide/Components.md - - Interacting with Services: user-guide/Interaction.md + - Interacting with pydase Services: user-guide/interaction/main.md - Developer Guide: - Developer Guide: dev-guide/README.md - API Reference: dev-guide/api.md @@ -17,7 +17,10 @@ nav: - Contributing: about/contributing.md - License: about/license.md -theme: readthedocs +theme: + name: material + features: + - content.code.copy extra_css: - css/extra.css @@ -27,11 +30,13 @@ markdown_extensions: - toc: permalink: true - pymdownx.highlight: + use_pygments: true anchor_linenums: true + line_spans: __span + pygments_lang_class: true - pymdownx.snippets - pymdownx.superfences - # - pymdownx.highlight: - # - pymdownx.inlinehilite + - pymdownx.inlinehilite plugins: diff --git a/poetry.lock b/poetry.lock index d1aea50..657dc47 100644 --- a/poetry.lock +++ b/poetry.lock @@ -178,6 +178,20 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -1110,6 +1124,46 @@ files = [ dev = ["bump2version (==1.0.1)", "mkdocs (==1.4.0)", "pre-commit", "pytest (==7.1.3)", "pytest-cov (==3.0.0)", "tox"] test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"] +[[package]] +name = "mkdocs-material" +version = "9.5.30" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.30-py3-none-any.whl", hash = "sha256:fc070689c5250a180e9b9d79d8491ef9a3a7acb240db0728728d6c31eeb131d4"}, + {file = "mkdocs_material-9.5.30.tar.gz", hash = "sha256:3fd417dd42d679e3ba08b9e2d72cd8b8af142cc4a3969676ad6b00993dd182ec"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + [[package]] name = "mkdocs-swagger-ui-tag" version = "0.6.10" @@ -1381,6 +1435,16 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1672,15 +1736,29 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pymdown-extensions" -version = "10.8.1" +version = "10.9" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, - {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, + {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, + {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, ] [package.dependencies] @@ -1938,6 +2016,94 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "regex" +version = "2024.7.24" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, +] + [[package]] name = "requests" version = "2.32.3" @@ -2265,4 +2431,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3f397396c238541d7a07327c9b289843124d8d4dd6b1d96a3393f3be2be38fa4" +content-hash = "80dda5533cf7111fd83407e0cce9228a99af644a6ffab4fa1abe085f4272cabf" diff --git a/pyproject.toml b/pyproject.toml index ec52d97..0a66524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ requests = "^2.32.3" optional = true [tool.poetry.group.docs.dependencies] -mkdocs = "^1.5.2" +mkdocs-material = "^9.5.30" mkdocs-include-markdown-plugin = "^3.9.1" mkdocstrings = "^0.22.0" pymdown-extensions = "^10.1" From aeaf57331e7c2ffa52761a7ea886eac81a1d9115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 11:17:38 +0200 Subject: [PATCH 10/24] updates docs python requirements --- docs/requirements.txt | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ce8573d..b274627 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,20 +1,35 @@ +babel==2.15.0 ; python_version >= "3.10" and python_version < "4.0" +beautifulsoup4==4.12.3 ; python_version >= "3.10" and python_version < "4.0" +certifi==2024.7.4 ; python_version >= "3.10" and python_version < "4.0" +charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0" click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" -colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" ghp-import==2.1.0 ; python_version >= "3.10" and python_version < "4.0" -jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" -markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0" -markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" +idna==3.7 ; python_version >= "3.10" and python_version < "4.0" +jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0" +markdown==3.6 ; python_version >= "3.10" and python_version < "4.0" +markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0" mergedeep==1.3.4 ; python_version >= "3.10" and python_version < "4.0" -mkdocs-autorefs==0.5.0 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-autorefs==1.0.1 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-get-deps==0.2.0 ; python_version >= "3.10" and python_version < "4.0" mkdocs-include-markdown-plugin==3.9.1 ; python_version >= "3.10" and python_version < "4.0" -mkdocs==1.5.3 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-material-extensions==1.3.1 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-material==9.5.30 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-swagger-ui-tag==0.6.10 ; python_version >= "3.10" and python_version < "4.0" +mkdocs==1.6.0 ; python_version >= "3.10" and python_version < "4.0" mkdocstrings==0.22.0 ; python_version >= "3.10" and python_version < "4.0" -packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" -pathspec==0.11.2 ; python_version >= "3.10" and python_version < "4.0" -platformdirs==3.10.0 ; python_version >= "3.10" and python_version < "4.0" -pymdown-extensions==10.3 ; python_version >= "3.10" and python_version < "4.0" -python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" +packaging==24.1 ; python_version >= "3.10" and python_version < "4.0" +paginate==0.5.6 ; python_version >= "3.10" and python_version < "4.0" +pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0" +platformdirs==4.2.2 ; python_version >= "3.10" and python_version < "4.0" +pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0" +pymdown-extensions==10.9 ; python_version >= "3.10" and python_version < "4.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" pyyaml-env-tag==0.1 ; python_version >= "3.10" and python_version < "4.0" pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" +regex==2024.7.24 ; python_version >= "3.10" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0" six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" -watchdog==3.0.0 ; python_version >= "3.10" and python_version < "4.0" +soupsieve==2.5 ; python_version >= "3.10" and python_version < "4.0" +urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0" +watchdog==4.0.1 ; python_version >= "3.10" and python_version < "4.0" From 80243487cb13fe8bab638749aedbbadddeb2be63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 11:25:57 +0200 Subject: [PATCH 11/24] fixing image link --- README.md | 2 +- ....png => Tailoring_frontend_component_layout.png} | Bin .../interaction/Auto-generated Frontend.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/images/{Tailoring frontend component layout.png => Tailoring_frontend_component_layout.png} (100%) diff --git a/README.md b/README.md index 8111121..460dec7 100644 --- a/README.md +++ b/README.md @@ -962,7 +962,7 @@ with the following `web_settings.json` looks like this: -![Tailoring frontend component layout](./docs/images/Tailoring frontend component layout.png) +![Tailoring frontend component layout](./docs/images/Tailoring_frontend_component_layout.png) ### Specifying a Custom Frontend Source diff --git a/docs/images/Tailoring frontend component layout.png b/docs/images/Tailoring_frontend_component_layout.png similarity index 100% rename from docs/images/Tailoring frontend component layout.png rename to docs/images/Tailoring_frontend_component_layout.png diff --git a/docs/user-guide/interaction/Auto-generated Frontend.md b/docs/user-guide/interaction/Auto-generated Frontend.md index a8b6c29..ea10f7c 100644 --- a/docs/user-guide/interaction/Auto-generated Frontend.md +++ b/docs/user-guide/interaction/Auto-generated Frontend.md @@ -110,7 +110,7 @@ with the following `web_settings.json` looks like this: -![Tailoring frontend component layout](../../images/Tailoring frontend component layout.png) +![Tailoring frontend component layout](../../images/Tailoring_frontend_component_layout.png) ### Specifying a Custom Frontend Source From 554d6f7daa608944285be5279541ab32c60a808c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 13:30:57 +0200 Subject: [PATCH 12/24] changes http API (reflected in openapi specification) --- docs/user-guide/interaction/openapi.yaml | 29 +++++++++++++++---- .../server/web_server/api/v1/application.py | 11 +++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/user-guide/interaction/openapi.yaml b/docs/user-guide/interaction/openapi.yaml index b2c2def..d3bee4b 100644 --- a/docs/user-guide/interaction/openapi.yaml +++ b/docs/user-guide/interaction/openapi.yaml @@ -37,14 +37,31 @@ paths: readonly: false type: float value: 12.1 - DoesNotExist: - summary: Attribute or does not exist + '400': + description: Could not get attribute + content: + application/json: + schema: + $ref: '#/components/schemas/SerializedException' + examples: + List: + summary: List out of index value: docs: null - full_access_path: device.channel[0].voltage + full_access_path: "" + name: SerializationPathError readonly: false - type: "None" - value: null + type: Exception + value: "Index '2': list index out of range" + Attribute: + summary: Attribute or dict key does not exist + value: + docs: null + full_access_path: "" + name: SerializationPathError + readonly: false + type: Exception + value: "Key 'invalid_attribute': 'invalid_attribute'." /api/v1/update_value: put: tags: @@ -79,7 +96,7 @@ paths: type: Exception value: "Index '2': list index out of range" Attribute: - summary: Attribute or does not exist + summary: Attribute does not exist value: docs: null full_access_path: "" diff --git a/src/pydase/server/web_server/api/v1/application.py b/src/pydase/server/web_server/api/v1/application.py index aa3cbab..78652f2 100644 --- a/src/pydase/server/web_server/api/v1/application.py +++ b/src/pydase/server/web_server/api/v1/application.py @@ -19,6 +19,9 @@ logger = logging.getLogger(__name__) API_VERSION = "v1" +STATUS_OK = 200 +STATUS_FAILED = 400 + def create_api_application(state_manager: StateManager) -> aiohttp.web.Application: api_application = aiohttp.web.Application( @@ -30,12 +33,14 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati access_path = request.rel_url.query["access_path"] + status = STATUS_OK try: result = get_value(state_manager, access_path) except Exception as e: logger.exception(e) result = dump(e) - return aiohttp.web.json_response(result) + status = STATUS_FAILED + return aiohttp.web.json_response(result, status=status) async def _update_value(request: aiohttp.web.Request) -> aiohttp.web.Response: data: UpdateDict = await request.json() @@ -46,7 +51,7 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati return aiohttp.web.json_response() except Exception as e: logger.exception(e) - return aiohttp.web.json_response(dump(e), status=400) + return aiohttp.web.json_response(dump(e), status=STATUS_FAILED) async def _trigger_method(request: aiohttp.web.Request) -> aiohttp.web.Response: data: TriggerMethodDict = await request.json() @@ -56,7 +61,7 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati except Exception as e: logger.exception(e) - return aiohttp.web.json_response(dump(e)) + return aiohttp.web.json_response(dump(e), status=STATUS_FAILED) api_application.router.add_get("/get_value", _get_value) api_application.router.add_post("/update_value", _update_value) From 74ebbc6223b0c6d3afbfe5130a7f1229d754f4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 13:31:35 +0200 Subject: [PATCH 13/24] http api: replaces post endpoints with put endpoints --- src/pydase/server/web_server/api/v1/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pydase/server/web_server/api/v1/application.py b/src/pydase/server/web_server/api/v1/application.py index 78652f2..efe00e7 100644 --- a/src/pydase/server/web_server/api/v1/application.py +++ b/src/pydase/server/web_server/api/v1/application.py @@ -64,7 +64,7 @@ def create_api_application(state_manager: StateManager) -> aiohttp.web.Applicati return aiohttp.web.json_response(dump(e), status=STATUS_FAILED) api_application.router.add_get("/get_value", _get_value) - api_application.router.add_post("/update_value", _update_value) - api_application.router.add_post("/trigger_method", _trigger_method) + api_application.router.add_put("/update_value", _update_value) + api_application.router.add_put("/trigger_method", _trigger_method) return api_application From b148d6919a134fc23d238c32b82a77074ad7ba14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 13:49:12 +0200 Subject: [PATCH 14/24] StateManager: replaces _data_service_cache with cache_manager - _data_service_cache -> cache_manager --- src/pydase/server/web_server/api/v1/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pydase/server/web_server/api/v1/endpoints.py b/src/pydase/server/web_server/api/v1/endpoints.py index bad0dcf..279c7b3 100644 --- a/src/pydase/server/web_server/api/v1/endpoints.py +++ b/src/pydase/server/web_server/api/v1/endpoints.py @@ -17,7 +17,7 @@ def update_value(state_manager: StateManager, data: UpdateDict) -> None: def get_value(state_manager: StateManager, access_path: str) -> SerializedObject: - return state_manager._data_service_cache.get_value_dict_from_cache(access_path) + return state_manager.cache_manager.get_value_dict_from_cache(access_path) def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any: From f91be30ad004f2dd4b13f96586541b3478bbde25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 29 Jul 2024 16:37:39 +0200 Subject: [PATCH 15/24] adds tests for http api endpoints --- .../web_server/api/v1/test_endpoints.py | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 tests/server/web_server/api/v1/test_endpoints.py diff --git a/tests/server/web_server/api/v1/test_endpoints.py b/tests/server/web_server/api/v1/test_endpoints.py new file mode 100644 index 0000000..dfedff1 --- /dev/null +++ b/tests/server/web_server/api/v1/test_endpoints.py @@ -0,0 +1,186 @@ +import json +import threading +from collections.abc import Generator +from typing import Any + +import aiohttp +import pydase +import pytest + + +@pytest.fixture(scope="session") +def pydase_server() -> Generator[None, None, None]: + class SubService(pydase.DataService): + name = "SubService" + + subservice_instance = SubService() + + class MyService(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self._name = "MyService" + self._my_property = 12.1 + self.sub_service = SubService() + self.list_attr = [1, 2] + self.dict_attr = { + "foo": subservice_instance, + "dotted.key": subservice_instance, + } + + @property + def my_property(self) -> float: + return self._my_property + + @my_property.setter + def my_property(self, value: float) -> None: + self._my_property = value + + @property + def name(self) -> str: + return self._name + + def my_method(self, input_str: str) -> str: + return input_str + + server = pydase.Server(MyService(), web_port=9998) + thread = threading.Thread(target=server.run, daemon=True) + thread.start() + + yield + + +@pytest.mark.parametrize( + "access_path, expected", + [ + ( + "name", + { + "full_access_path": "name", + "doc": None, + "readonly": True, + "type": "str", + "value": "MyService", + }, + ), + ( + "sub_service.name", + { + "full_access_path": "sub_service.name", + "doc": None, + "readonly": False, + "type": "str", + "value": "SubService", + }, + ), + ( + "list_attr[0]", + { + "full_access_path": "list_attr[0]", + "doc": None, + "readonly": False, + "type": "int", + "value": 1, + }, + ), + ( + 'dict_attr["foo"]', + { + "full_access_path": 'dict_attr["foo"]', + "doc": None, + "name": "SubService", + "readonly": False, + "type": "DataService", + "value": { + "name": { + "doc": None, + "full_access_path": 'dict_attr["foo"].name', + "readonly": False, + "type": "str", + "value": "SubService", + } + }, + }, + ), + ], +) +@pytest.mark.asyncio +async def test_get_value( + access_path: str, + expected: dict[str, Any], + caplog: pytest.LogCaptureFixture, + pydase_server: None, +) -> None: + async with aiohttp.ClientSession("http://localhost:9998") as session: + resp = await session.get(f"/api/v1/get_value?access_path={access_path}") + content = json.loads(await resp.text()) + assert content == expected + + +@pytest.mark.parametrize( + "access_path, new_value", + [ + ( + "name", + { + "full_access_path": "name", + "doc": None, + "readonly": True, + "type": "str", + "value": "Other Name", + }, + ), + ( + "sub_service.name", + { + "full_access_path": "sub_service.name", + "doc": None, + "readonly": False, + "type": "str", + "value": "New Name", + }, + ), + ( + "list_attr[0]", + { + "full_access_path": "list_attr[0]", + "doc": None, + "readonly": False, + "type": "int", + "value": 11, + }, + ), + ( + 'dict_attr["foo"].name', + { + "full_access_path": 'dict_attr["foo"].name', + "doc": None, + "readonly": False, + "type": "str", + "value": "foo name", + }, + ), + ( + "my_property", + { + "full_access_path": "my_property", + "doc": None, + "readonly": False, + "type": "float", + "value": 12.0, + }, + ), + ], +) +@pytest.mark.asyncio +async def test_update_value( + access_path: str, + new_value: dict[str, Any], + caplog: pytest.LogCaptureFixture, + pydase_server: None, +) -> None: + async with aiohttp.ClientSession("http://localhost:9998") as session: + resp = await session.put( + "/api/v1/update_value", + json={"access_path": access_path, "value": new_value}, + ) + assert resp.ok From 75e355faf9e6558ddb53c374a5680e8223718646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 08:36:59 +0200 Subject: [PATCH 16/24] pytest: changes fixture scopes --- tests/client/test_client.py | 2 +- tests/components/test_device_connection.py | 2 +- tests/data_service/test_data_service_cache.py | 2 +- tests/data_service/test_task_manager.py | 8 ++++---- tests/server/web_server/api/v1/test_endpoints.py | 6 +++--- tests/utils/serialization/test_serializer.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index fa4ef98..7a66bb2 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -7,7 +7,7 @@ import pytest from pydase.client.proxy_loader import ProxyAttributeError -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def pydase_client() -> Generator[pydase.Client, None, Any]: class SubService(pydase.DataService): name = "SubService" diff --git a/tests/components/test_device_connection.py b/tests/components/test_device_connection.py index 8c5c9d5..3f5e92c 100644 --- a/tests/components/test_device_connection.py +++ b/tests/components/test_device_connection.py @@ -6,7 +6,7 @@ import pytest from pytest import LogCaptureFixture -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_reconnection(caplog: LogCaptureFixture) -> None: class MyService(pydase.components.device_connection.DeviceConnection): def __init__( diff --git a/tests/data_service/test_data_service_cache.py b/tests/data_service/test_data_service_cache.py index e21e64e..8e85403 100644 --- a/tests/data_service/test_data_service_cache.py +++ b/tests/data_service/test_data_service_cache.py @@ -35,7 +35,7 @@ def test_nested_attributes_cache_callback() -> None: ) -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_task_status_update() -> None: class ServiceClass(pydase.DataService): name = "World" diff --git a/tests/data_service/test_task_manager.py b/tests/data_service/test_task_manager.py index bb1619b..7a1a682 100644 --- a/tests/data_service/test_task_manager.py +++ b/tests/data_service/test_task_manager.py @@ -10,7 +10,7 @@ from pytest import LogCaptureFixture logger = logging.getLogger("pydase") -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_autostart_task_callback(caplog: LogCaptureFixture) -> None: class MyService(pydase.DataService): def __init__(self) -> None: @@ -36,7 +36,7 @@ async def test_autostart_task_callback(caplog: LogCaptureFixture) -> None: assert "'my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_DataService_subclass_autostart_task_callback( caplog: LogCaptureFixture, ) -> None: @@ -66,7 +66,7 @@ async def test_DataService_subclass_autostart_task_callback( assert "'sub_service.my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_DataService_subclass_list_autostart_task_callback( caplog: LogCaptureFixture, ) -> None: @@ -108,7 +108,7 @@ async def test_DataService_subclass_list_autostart_task_callback( ) -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="function") async def test_start_and_stop_task_methods(caplog: LogCaptureFixture) -> None: class MyService(pydase.DataService): def __init__(self) -> None: diff --git a/tests/server/web_server/api/v1/test_endpoints.py b/tests/server/web_server/api/v1/test_endpoints.py index dfedff1..ed6f2ef 100644 --- a/tests/server/web_server/api/v1/test_endpoints.py +++ b/tests/server/web_server/api/v1/test_endpoints.py @@ -8,7 +8,7 @@ import pydase import pytest -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def pydase_server() -> Generator[None, None, None]: class SubService(pydase.DataService): name = "SubService" @@ -103,7 +103,7 @@ def pydase_server() -> Generator[None, None, None]: ), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="module") async def test_get_value( access_path: str, expected: dict[str, Any], @@ -171,7 +171,7 @@ async def test_get_value( ), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="module") async def test_update_value( access_path: str, new_value: dict[str, Any], diff --git a/tests/utils/serialization/test_serializer.py b/tests/utils/serialization/test_serializer.py index 0dc872d..90612af 100644 --- a/tests/utils/serialization/test_serializer.py +++ b/tests/utils/serialization/test_serializer.py @@ -207,7 +207,7 @@ def test_ColouredEnum_serialize() -> None: } -@pytest.mark.asyncio(scope="session") +@pytest.mark.asyncio(scope="module") async def test_method_serialization() -> None: class ClassWithMethod(pydase.DataService): def some_method(self) -> str: From 5d8471fd47f90225919a84925614bd34fd26b467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 09:18:22 +0200 Subject: [PATCH 17/24] disallows clients to add class attributes (through the state manager) Note that adding dictionary keys still works. You can also append to lists. --- src/pydase/data_service/state_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pydase/data_service/state_manager.py b/src/pydase/data_service/state_manager.py index d8e22d3..8504a3e 100644 --- a/src/pydase/data_service/state_manager.py +++ b/src/pydase/data_service/state_manager.py @@ -281,6 +281,15 @@ class StateManager: processed_key = parse_serialized_key(path_parts[-1]) target_obj[processed_key] = value # type: ignore else: + # Don't allow adding attributes to objects through state manager + if self.__attr_exists_on_target_obj( + target_obj=target_obj, name=path_parts[-1] + ): + raise AttributeError( + f"{target_obj.__class__.__name__!r} object has no attribute " + f"{path_parts[-1]!r}" + ) + setattr(target_obj, path_parts[-1], value) def __is_loadable_state_attribute(self, full_access_path: str) -> bool: @@ -322,3 +331,8 @@ class StateManager: path_parts[-1], ) return False + + def __attr_exists_on_target_obj(self, target_obj: Any, name: str) -> bool: + return not is_property_attribute(target_obj, name) and not hasattr( + target_obj, name + ) From bfe2d82c0bc70a120a642b31b1bd7abd48a2dfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 09:28:51 +0200 Subject: [PATCH 18/24] api: getting value from service instead of cache --- src/pydase/server/web_server/api/v1/endpoints.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pydase/server/web_server/api/v1/endpoints.py b/src/pydase/server/web_server/api/v1/endpoints.py index 279c7b3..070b489 100644 --- a/src/pydase/server/web_server/api/v1/endpoints.py +++ b/src/pydase/server/web_server/api/v1/endpoints.py @@ -4,7 +4,7 @@ from pydase.data_service.state_manager import StateManager from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict from pydase.utils.helpers import get_object_attr_from_path from pydase.utils.serialization.deserializer import loads -from pydase.utils.serialization.serializer import dump +from pydase.utils.serialization.serializer import Serializer, dump from pydase.utils.serialization.types import SerializedObject @@ -17,7 +17,10 @@ def update_value(state_manager: StateManager, data: UpdateDict) -> None: def get_value(state_manager: StateManager, access_path: str) -> SerializedObject: - return state_manager.cache_manager.get_value_dict_from_cache(access_path) + return Serializer.serialize_object( + get_object_attr_from_path(state_manager.service, access_path), + access_path=access_path, + ) def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any: From 1fb296c3c1351db3a23a496cf7d152d12ac3cff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 10:10:57 +0200 Subject: [PATCH 19/24] removes read-only check from state manager's set_service_attribute_value_by_path --- src/pydase/data_service/state_manager.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pydase/data_service/state_manager.py b/src/pydase/data_service/state_manager.py index 8504a3e..8a9be12 100644 --- a/src/pydase/data_service/state_manager.py +++ b/src/pydase/data_service/state_manager.py @@ -216,11 +216,6 @@ class StateManager: "readonly": False, } - # This will also filter out methods as they are 'read-only' - if current_value_dict["readonly"]: - logger.debug("Attribute '%s' is read-only. Ignoring new value...", path) - return - if "full_access_path" not in serialized_value: # Backwards compatibility for JSON files not containing the # full_access_path From d45d2dba7d575ecd004f27909a73e4b8cdf8b18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 09:30:22 +0200 Subject: [PATCH 20/24] updates api tests --- .../web_server/api/v1/test_endpoints.py | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/tests/server/web_server/api/v1/test_endpoints.py b/tests/server/web_server/api/v1/test_endpoints.py index ed6f2ef..3fd416c 100644 --- a/tests/server/web_server/api/v1/test_endpoints.py +++ b/tests/server/web_server/api/v1/test_endpoints.py @@ -8,7 +8,7 @@ import pydase import pytest -@pytest.fixture(scope="module") +@pytest.fixture() def pydase_server() -> Generator[None, None, None]: class SubService(pydase.DataService): name = "SubService" @@ -18,7 +18,7 @@ def pydase_server() -> Generator[None, None, None]: class MyService(pydase.DataService): def __init__(self) -> None: super().__init__() - self._name = "MyService" + self._readonly_attr = "MyService" self._my_property = 12.1 self.sub_service = SubService() self.list_attr = [1, 2] @@ -36,8 +36,8 @@ def pydase_server() -> Generator[None, None, None]: self._my_property = value @property - def name(self) -> str: - return self._name + def readonly_attr(self) -> str: + return self._readonly_attr def my_method(self, input_str: str) -> str: return input_str @@ -53,11 +53,11 @@ def pydase_server() -> Generator[None, None, None]: "access_path, expected", [ ( - "name", + "readonly_attr", { - "full_access_path": "name", + "full_access_path": "readonly_attr", "doc": None, - "readonly": True, + "readonly": False, "type": "str", "value": "MyService", }, @@ -103,11 +103,10 @@ def pydase_server() -> Generator[None, None, None]: ), ], ) -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio() async def test_get_value( access_path: str, expected: dict[str, Any], - caplog: pytest.LogCaptureFixture, pydase_server: None, ) -> None: async with aiohttp.ClientSession("http://localhost:9998") as session: @@ -117,18 +116,8 @@ async def test_get_value( @pytest.mark.parametrize( - "access_path, new_value", + "access_path, new_value, ok", [ - ( - "name", - { - "full_access_path": "name", - "doc": None, - "readonly": True, - "type": "str", - "value": "Other Name", - }, - ), ( "sub_service.name", { @@ -138,6 +127,7 @@ async def test_get_value( "type": "str", "value": "New Name", }, + True, ), ( "list_attr[0]", @@ -148,6 +138,7 @@ async def test_get_value( "type": "int", "value": 11, }, + True, ), ( 'dict_attr["foo"].name', @@ -158,29 +149,46 @@ async def test_get_value( "type": "str", "value": "foo name", }, + True, ), ( - "my_property", + "readonly_attr", { - "full_access_path": "my_property", + "full_access_path": "readonly_attr", + "doc": None, + "readonly": True, + "type": "str", + "value": "Other Name", + }, + False, + ), + ( + "invalid_attribute", + { + "full_access_path": "invalid_attribute", "doc": None, "readonly": False, "type": "float", "value": 12.0, }, + False, ), ], ) -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio() async def test_update_value( access_path: str, new_value: dict[str, Any], - caplog: pytest.LogCaptureFixture, - pydase_server: None, + ok: bool, + pydase_server: pydase.DataService, ) -> None: async with aiohttp.ClientSession("http://localhost:9998") as session: resp = await session.put( "/api/v1/update_value", json={"access_path": access_path, "value": new_value}, ) - assert resp.ok + assert resp.ok == ok + if resp.ok: + resp = await session.get(f"/api/v1/get_value?access_path={access_path}") + content = json.loads(await resp.text()) + assert content == new_value From 940f7039d3b383bbf3c958f91cd9285752ab4ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 09:18:32 +0200 Subject: [PATCH 21/24] reflecting changes in openapi.yaml --- docs/user-guide/interaction/openapi.yaml | 104 +++++++++++++++++------ 1 file changed, 78 insertions(+), 26 deletions(-) diff --git a/docs/user-guide/interaction/openapi.yaml b/docs/user-guide/interaction/openapi.yaml index d3bee4b..18e1bb2 100644 --- a/docs/user-guide/interaction/openapi.yaml +++ b/docs/user-guide/interaction/openapi.yaml @@ -44,24 +44,24 @@ paths: schema: $ref: '#/components/schemas/SerializedException' examples: - List: - summary: List out of index - value: - docs: null - full_access_path: "" - name: SerializationPathError - readonly: false - type: Exception - value: "Index '2': list index out of range" Attribute: - summary: Attribute or dict key does not exist + summary: Attribute does not exist value: docs: null full_access_path: "" - name: SerializationPathError - readonly: false + name: AttributeError + readonly: true type: Exception - value: "Key 'invalid_attribute': 'invalid_attribute'." + value: "'MyService' object has no attribute 'invalid_attribute'" + List: + summary: List index out of range + value: + docs: null + full_access_path: "" + name: IndexError + readonly: true + type: Exception + value: "list index out of range" /api/v1/update_value: put: tags: @@ -86,24 +86,33 @@ paths: schema: $ref: '#/components/schemas/SerializedException' examples: - List: - summary: List out of index - value: - docs: null - full_access_path: "" - name: SerializationPathError - readonly: false - type: Exception - value: "Index '2': list index out of range" Attribute: summary: Attribute does not exist value: docs: null full_access_path: "" - name: SerializationPathError - readonly: false + name: AttributeError + readonly: true type: Exception - value: "Key 'invalid_attribute': 'invalid_attribute'." + value: "'MyService' object has no attribute 'invalid_attribute'" + ReadOnly: + summary: Attribute is read-only + value: + docs: null + full_access_path: "" + name: AttributeError + readonly: true + type: Exception + value: "property 'readonly_property' of 'MyService' object has no setter" + List: + summary: List index out of range + value: + docs: null + full_access_path: "" + name: IndexError + readonly: true + type: Exception + value: "list index out of range" /api/v1/trigger_method: put: tags: @@ -142,6 +151,49 @@ paths: readonly: false type: "float" value: 23.2 + '400': + description: Method does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/SerializedException' + examples: + Args: + summary: Wrong number of arguments + value: + docs: null + full_access_path: "" + name: TypeError + readonly: true + type: Exception + value: "MyService.some_function() takes 1 positional argument but 2 were given" + Attribute: + summary: Attribute does not exist + value: + docs: null + full_access_path: "" + name: AttributeError + readonly: true + type: Exception + value: "'MyService' object has no attribute 'invalid_method'" + List: + summary: List index out of range + value: + docs: null + full_access_path: "" + name: IndexError + readonly: true + type: Exception + value: "list index out of range" + Dict: + summary: Dictionary key does not exist + value: + docs: null + full_access_path: "" + name: KeyError + readonly: true + type: Exception + value: "invalid_key" components: schemas: UpdateValue: @@ -261,7 +313,7 @@ components: example: SerializationPathError readonly: type: boolean - example: false + example: true type: type: string example: Exception From bd6220cb9ea3e5dcfb7a7c5b7a3928e86b2b0938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 10:17:43 +0200 Subject: [PATCH 22/24] chore: refactoring state_manager --- src/pydase/data_service/state_manager.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/pydase/data_service/state_manager.py b/src/pydase/data_service/state_manager.py index 8a9be12..b5f7a89 100644 --- a/src/pydase/data_service/state_manager.py +++ b/src/pydase/data_service/state_manager.py @@ -248,17 +248,7 @@ class StateManager: path_parts = parse_full_access_path(path) target_obj = get_object_by_path_parts(self.service, path_parts[:-1]) - def cached_value_is_enum(path: str) -> bool: - try: - attr_cache_type = self.cache_manager.get_value_dict_from_cache(path)[ - "type" - ] - - return attr_cache_type in ("ColouredEnum", "Enum") - except Exception: - return False - - if cached_value_is_enum(path): + if self.__cached_value_is_enum(path): enum_attr = get_object_by_path_parts(target_obj, [path_parts[-1]]) # take the value of the existing enum class if serialized_value["type"] in ("ColouredEnum", "Enum"): @@ -327,6 +317,14 @@ class StateManager: ) return False + def __cached_value_is_enum(self, path: str) -> bool: + try: + attr_cache_type = self.cache_manager.get_value_dict_from_cache(path)["type"] + + return attr_cache_type in ("ColouredEnum", "Enum") + except Exception: + return False + def __attr_exists_on_target_obj(self, target_obj: Any, name: str) -> bool: return not is_property_attribute(target_obj, name) and not hasattr( target_obj, name From 9e852c17ac05fb6f680738c00b46f124e40b2ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 10:36:40 +0200 Subject: [PATCH 23/24] docs: updates documentation --- README.md | 19 +++++++++++++++++++ docs/user-guide/interaction/RESTful API.md | 18 +++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 460dec7..118e598 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [Connecting to the Service via Python Client](#connecting-to-the-service-via-python-client) - [Tab Completion Support](#tab-completion-support) - [Integration within Another Service](#integration-within-another-service) + - [RESTful API](#restful-api) - [Understanding the Component System](#understanding-the-component-system) - [Built-in Type and Enum Components](#built-in-type-and-enum-components) - [Method Components](#method-components) @@ -209,6 +210,24 @@ In this setup, the `MyService` class has a `proxy` attribute that connects to a The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails. This configuration is particularly useful in distributed systems where services may start in any order. +### RESTful API +The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. + +For example, you can get a value like this: + +```python +import json + +import requests + +response = requests.get( + "http://:/api/v1/get_value?access_path=" +) +serialized_value = json.loads(response.text) +``` + +For more information, see [here](https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api). + ## Understanding the Component System diff --git a/docs/user-guide/interaction/RESTful API.md b/docs/user-guide/interaction/RESTful API.md index ecd76de..f8d73fd 100644 --- a/docs/user-guide/interaction/RESTful API.md +++ b/docs/user-guide/interaction/RESTful API.md @@ -2,13 +2,21 @@ The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. This is particularly useful for integrating `pydase` services with other applications or for scripting and automation. +For example, you can get a value like this: + +```python +import json + +import requests + +response = requests.get( + "http://:/api/v1/get_value?access_path=" +) +serialized_value = json.loads(response.text) +``` + To help developers understand and utilize the API, we provide an OpenAPI specification. This specification describes the available endpoints and corresponding request/response formats. ## OpenAPI Specification -**Base URL**: - -``` -http://:/api/ -``` From 22d836587e7b1e3348e964786884585229b05d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Jul 2024 10:40:11 +0200 Subject: [PATCH 24/24] udpates to version v0.8.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a66524..e1037aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydase" -version = "0.8.4" +version = "0.8.5" description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases." authors = ["Mose Mueller "] readme = "README.md"