From aeb623acef2b0b715e72ea4e62b1eff080f86b0c Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Wed, 21 Feb 2024 12:53:30 -0300 Subject: [PATCH 1/7] SETUP EventType support --- apps/examples/client-example/api/openapi.json | 5 +- apps/examples/simple-example/api/openapi.json | 5 +- .../simple-example/config/app-config.json | 4 + .../service/something_generator.py | 26 +- .../src/simple_example/setup_something.py | 24 + engine/src/hopeit/app/config.py | 4 +- engine/src/hopeit/server/web.py | 637 +++++++++++------- plugins/ops/apps-visualizer/api/openapi.json | 5 +- 8 files changed, 460 insertions(+), 250 deletions(-) create mode 100644 apps/examples/simple-example/src/simple_example/setup_something.py diff --git a/apps/examples/client-example/api/openapi.json b/apps/examples/client-example/api/openapi.json index b88ccfb9..e25fa316 100644 --- a/apps/examples/client-example/api/openapi.json +++ b/apps/examples/client-example/api/openapi.json @@ -635,7 +635,8 @@ "POST", "STREAM", "SERVICE", - "MULTIPART" + "MULTIPART", + "SETUP" ], "x-enum-name": "EventType", "x-module-name": "hopeit.app.config" @@ -704,7 +705,7 @@ } }, "x-module-name": "hopeit.app.config", - "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " + "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE, SETUP\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " }, "EventConnection": { "type": "object", diff --git a/apps/examples/simple-example/api/openapi.json b/apps/examples/simple-example/api/openapi.json index 6c0025d9..424469f5 100644 --- a/apps/examples/simple-example/api/openapi.json +++ b/apps/examples/simple-example/api/openapi.json @@ -1933,7 +1933,8 @@ "POST", "STREAM", "SERVICE", - "MULTIPART" + "MULTIPART", + "SETUP" ], "x-enum-name": "EventType", "x-module-name": "hopeit.app.config" @@ -2002,7 +2003,7 @@ } }, "x-module-name": "hopeit.app.config", - "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " + "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE, SETUP\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " }, "EventConnection": { "type": "object", diff --git a/apps/examples/simple-example/config/app-config.json b/apps/examples/simple-example/config/app-config.json index d3b65eb8..1d90dc47 100644 --- a/apps/examples/simple-example/config/app-config.json +++ b/apps/examples/simple-example/config/app-config.json @@ -66,6 +66,10 @@ } }, "events": { + "setup_something": { + "type": "SETUP", + "setting_keys": ["fs_storage"] + }, "list_somethings": { "type": "GET", "setting_keys": ["fs_storage"] diff --git a/apps/examples/simple-example/src/simple_example/service/something_generator.py b/apps/examples/simple-example/src/simple_example/service/something_generator.py index b4afe76f..ea97a7ad 100644 --- a/apps/examples/simple-example/src/simple_example/service/something_generator.py +++ b/apps/examples/simple-example/src/simple_example/service/something_generator.py @@ -3,22 +3,29 @@ -------------------------------------------------------------------- Creates and publish Something object every 10 seconds """ + import asyncio import random - +import os from hopeit.app.context import EventContext from hopeit.app.events import Spawn, service_running from hopeit.app.logger import app_extra_logger from model import Something, User, SomethingParams -__steps__ = ['create_something'] +__steps__ = ["create_something"] logger, extra = app_extra_logger() async def __service__(context: EventContext) -> Spawn[SomethingParams]: i = 1 + if not os.path.exists("/tmp/hopeit.initializead"): + raise RuntimeError( + "Missing /tmp/hopeit.initializead file. " + "Service will not start until run setup_something." + ) + os.remove("/tmp/hopeit.initializead") while service_running(context): logger.info(context, f"Generating something event {i}...") yield SomethingParams(f"id{i}", f"user{i}") @@ -27,13 +34,14 @@ async def __service__(context: EventContext) -> Spawn[SomethingParams]: logger.info(context, "Service seamlessly exit") -async def create_something(payload: SomethingParams, context: EventContext) -> Something: - logger.info(context, "Creating something...", extra=extra( - payload_id=payload.id, user=payload.user - )) - result = Something( - id=payload.id, - user=User(id=payload.user, name=payload.user) +async def create_something( + payload: SomethingParams, context: EventContext +) -> Something: + logger.info( + context, + "Creating something...", + extra=extra(payload_id=payload.id, user=payload.user), ) + result = Something(id=payload.id, user=User(id=payload.user, name=payload.user)) await asyncio.sleep(random.random() * 5.0) return result diff --git a/apps/examples/simple-example/src/simple_example/setup_something.py b/apps/examples/simple-example/src/simple_example/setup_something.py new file mode 100644 index 00000000..2334918b --- /dev/null +++ b/apps/examples/simple-example/src/simple_example/setup_something.py @@ -0,0 +1,24 @@ +""" +Simple Example: Setup Something +-------------------------------------------------------------------- +Setup run before initialize endpoints streams and services +""" + +from pathlib import Path + +from hopeit.app.context import EventContext +from hopeit.app.logger import app_extra_logger + + +__steps__ = ["run_once"] + + +logger, extra = app_extra_logger() + + +async def run_once(payload: None, context: EventContext): + """ + Load objects that match the given wildcard + """ + logger.info(context, "Setup done") + Path("/tmp/hopeit.initializead").touch() diff --git a/engine/src/hopeit/app/config.py b/engine/src/hopeit/app/config.py index 14d98d77..bb6ee51b 100644 --- a/engine/src/hopeit/app/config.py +++ b/engine/src/hopeit/app/config.py @@ -61,12 +61,14 @@ class EventType(str, Enum): STREAM: event triggered read events from stream. Can be started and stopped. SERVICE: event executed on demand or continuously. Long lived. Can be started and stopped. MULTIPART: event triggered from api postform-multipart request via endpoint. + SETUP: event that is executed once when service is starting """ GET = 'GET' POST = 'POST' STREAM = 'STREAM' SERVICE = 'SERVICE' MULTIPART = 'MULTIPART' + SETUP = 'SETUP' class StreamQueue: @@ -299,7 +301,7 @@ class EventDescriptor: """ Event Descriptor: configures event implementation - :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE + :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE, SETUP :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the current app (ON_APP) or it will be created in the original plugin (STANDALONE, default) :field: route, optional str: custom route for endpoint. If not specified route will be derived diff --git a/engine/src/hopeit/server/web.py b/engine/src/hopeit/server/web.py index d4d9c206..674b1a0e 100644 --- a/engine/src/hopeit/server/web.py +++ b/engine/src/hopeit/server/web.py @@ -1,12 +1,13 @@ """ Webserver module based on aiohttp to handle web/api requests """ + # flake8: noqa # pylint: disable=wrong-import-position, wrong-import-order from collections import namedtuple import aiohttp -setattr(aiohttp.http, 'SERVER_SOFTWARE', '') +setattr(aiohttp.http, "SERVER_SOFTWARE", "") import argparse import asyncio @@ -17,9 +18,7 @@ import uuid from datetime import datetime, timezone from functools import partial -from typing import ( - Any, Callable, Coroutine, Dict, List, Optional, Type, Tuple, Union -) +from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Tuple, Union import aiohttp_cors # type: ignore from aiohttp import web @@ -28,24 +27,33 @@ from stringcase import snakecase, titlecase # type: ignore from hopeit.app.config import ( - AppConfig, EventDescriptor, EventPlugMode, EventSettings, EventType, parse_app_config_json + AppConfig, + EventDescriptor, + EventPlugMode, + EventSettings, + EventType, + parse_app_config_json, ) from hopeit.app.context import ( - EventContext, NoopMultiparReader, PostprocessHook, PreprocessHook + EventContext, + NoopMultiparReader, + PostprocessHook, + PreprocessHook, ) from hopeit.app.errors import BadRequest, Unauthorized from hopeit.dataobjects import DataObject, EventPayload, EventPayloadType from hopeit.dataobjects.payload import Payload from hopeit.server import api, runtime from hopeit.server.api import app_route_name, OPEN_API_DEFAULTS -from hopeit.server.config import ( - AuthType, ServerConfig, parse_server_config_json -) +from hopeit.server.config import AuthType, ServerConfig, parse_server_config_json from hopeit.server.engine import AppEngine from hopeit.server.errors import ErrorInfo from hopeit.server.events import get_event_settings from hopeit.server.logger import ( - EngineLoggerWrapper, combined, engine_logger, extra_logger + EngineLoggerWrapper, + combined, + engine_logger, + extra_logger, ) from hopeit.server.metrics import metrics from hopeit.server.names import route_name @@ -53,13 +61,15 @@ from hopeit.toolkit import auth -__all__ = ['parse_args', - 'prepare_engine', - 'serve', - 'server_startup_hook', - 'app_startup_hook', - 'stream_startup_hook', - 'stop_server'] +__all__ = [ + "parse_args", + "prepare_engine", + "serve", + "server_startup_hook", + "app_startup_hook", + "stream_startup_hook", + "stop_server", +] logger: EngineLoggerWrapper = logging.getLogger(__name__) # type: ignore extra = extra_logger() @@ -70,8 +80,14 @@ auth_info_default = {} -def prepare_engine(*, config_files: List[str], api_file: Optional[str], api_auto: List[str], - enabled_groups: List[str], start_streams: bool): +def prepare_engine( + *, + config_files: List[str], + api_file: Optional[str], + api_auto: List[str], + enabled_groups: List[str], + start_streams: bool, +): """ Load configuration files and add hooks to setup engine server and apps, start streams and services. @@ -80,11 +96,9 @@ def prepare_engine(*, config_files: List[str], api_file: Optional[str], api_auto server_config: ServerConfig = _load_engine_config(config_files[0]) # Add startup hook to start engine - web_server.on_startup.append( - partial(server_startup_hook, server_config) - ) + web_server.on_startup.append(partial(server_startup_hook, server_config)) if server_config.auth.domain: - auth_info_default['domain'] = server_config.auth.domain + auth_info_default["domain"] = server_config.auth.domain if api_file is not None: api.load_api_file(api_file) @@ -92,7 +106,7 @@ def prepare_engine(*, config_files: List[str], api_file: Optional[str], api_auto if api_file is None and api_auto: if len(api_auto) < 3: - api_auto.extend(OPEN_API_DEFAULTS[len(api_auto) - 3:]) + api_auto.extend(OPEN_API_DEFAULTS[len(api_auto) - 3 :]) else: api_auto = api_auto[:3] api.init_auto_api(api_auto[0], api_auto[1], api_auto[2]) @@ -109,16 +123,12 @@ def prepare_engine(*, config_files: List[str], api_file: Optional[str], api_auto api.register_apps(apps_config) api.enable_swagger(server_config, web_server) for config in apps_config: - web_server.on_startup.append( - partial(app_startup_hook, config, enabled_groups) - ) + web_server.on_startup.append(partial(app_startup_hook, config, enabled_groups)) # Add hooks to start streams and service if start_streams: for config in apps_config: - web_server.on_startup.append( - partial(stream_startup_hook, config) - ) + web_server.on_startup.append(partial(stream_startup_hook, config)) web_server.on_shutdown.append(_shutdown_hook) logger.debug(__name__, "Performing forced garbage collection...") @@ -150,7 +160,9 @@ async def stop_server(): await web_server.cleanup() -async def app_startup_hook(config: AppConfig, enabled_groups: List[str], *args, **kwargs): +async def app_startup_hook( + config: AppConfig, enabled_groups: List[str], *args, **kwargs +): """ Start Hopeit app specified by config @@ -158,22 +170,31 @@ async def app_startup_hook(config: AppConfig, enabled_groups: List[str], *args, :param enabled_groups: list of event groups names to enable. If empty, all events will be enabled. """ - app_engine = await runtime.server.start_app(app_config=config, enabled_groups=enabled_groups) - cors_origin = aiohttp_cors.setup(web_server, defaults={ - config.engine.cors_origin: aiohttp_cors.ResourceOptions( - allow_credentials=True, - expose_headers="*", - allow_headers="*", + app_engine = await runtime.server.start_app( + app_config=config, enabled_groups=enabled_groups + ) + cors_origin = ( + aiohttp_cors.setup( + web_server, + defaults={ + config.engine.cors_origin: aiohttp_cors.ResourceOptions( + allow_credentials=True, + expose_headers="*", + allow_headers="*", + ) + }, ) - }) if config.engine.cors_origin else None + if config.engine.cors_origin + else None + ) - _setup_app_event_routes(app_engine) + await _setup_app_event_routes(app_engine) for plugin in config.plugins: plugin_engine = runtime.server.app_engine(app_key=plugin.app_key()) - _setup_app_event_routes(app_engine, plugin_engine) + await _setup_app_event_routes(app_engine, plugin_engine) if cors_origin: app = app_engine.app_config.app - _enable_cors(route_name('api', app.name, app.version), cors_origin) + _enable_cors(route_name("api", app.name, app.version), cors_origin) async def stream_startup_hook(app_config: AppConfig, *args, **kwargs): @@ -187,22 +208,25 @@ async def stream_startup_hook(app_config: AppConfig, *args, **kwargs): if event_info.type == EventType.STREAM: assert event_info.read_stream logger.info( - __name__, f"STREAM start event_name={event_name} read_stream={event_info.read_stream.name}") + __name__, + f"STREAM start event_name={event_name} read_stream={event_info.read_stream.name}", + ) asyncio.create_task(app_engine.read_stream(event_name=event_name)) elif event_info.type == EventType.SERVICE: - logger.info( - __name__, f"SERVICE start event_name={event_name}") + logger.info(__name__, f"SERVICE start event_name={event_name}") asyncio.create_task(app_engine.service_loop(event_name=event_name)) def _effective_events(app_engine: AppEngine, plugin: Optional[AppEngine] = None): if plugin is None: return { - k: v for k, v in app_engine.effective_events.items() + k: v + for k, v in app_engine.effective_events.items() if v.plug_mode == EventPlugMode.STANDALONE } return { - k: v for k, v in plugin.effective_events.items() + k: v + for k, v in plugin.effective_events.items() if v.plug_mode == EventPlugMode.ON_APP } @@ -229,8 +253,7 @@ def _enable_cors(prefix: str, cors: CorsConfig): cors.add(route) -def _setup_app_event_routes(app_engine: AppEngine, - plugin: Optional[AppEngine] = None): +async def _setup_app_event_routes(app_engine: AppEngine, plugin: Optional[AppEngine] = None): """ Setup http routes for existing events in app, in existing web_server global instance. @@ -248,23 +271,38 @@ def _setup_app_event_routes(app_engine: AppEngine, """ for event_name, event_info in _effective_events(app_engine, plugin).items(): if event_info.type == EventType.POST: - web_server.add_routes([ - _create_post_event_route( - app_engine, plugin=plugin, event_name=event_name, event_info=event_info - ) - ]) + web_server.add_routes( + [ + _create_post_event_route( + app_engine, + plugin=plugin, + event_name=event_name, + event_info=event_info, + ) + ] + ) elif event_info.type == EventType.GET: - web_server.add_routes([ - _create_get_event_route( - app_engine, plugin=plugin, event_name=event_name, event_info=event_info - ) - ]) + web_server.add_routes( + [ + _create_get_event_route( + app_engine, + plugin=plugin, + event_name=event_name, + event_info=event_info, + ) + ] + ) elif event_info.type == EventType.MULTIPART: - web_server.add_routes([ - _create_multipart_event_route( - app_engine, plugin=plugin, event_name=event_name, event_info=event_info - ) - ]) + web_server.add_routes( + [ + _create_multipart_event_route( + app_engine, + plugin=plugin, + event_name=event_name, + event_info=event_info, + ) + ] + ) elif event_info.type == EventType.STREAM and plugin is None: web_server.add_routes( _create_event_management_routes( @@ -277,8 +315,12 @@ def _setup_app_event_routes(app_engine: AppEngine, app_engine, event_name=event_name, event_info=event_info ) ) + elif event_info.type == EventType.SETUP: + await _execute_setup_event(app_engine, plugin, event_name) else: - raise ValueError(f"Invalid event_type:{event_info.type} for event:{event_name}") + raise ValueError( + f"Invalid event_type:{event_info.type} for event:{event_name}" + ) def _auth_types(app_engine: AppEngine, event_name: str): @@ -290,97 +332,143 @@ def _auth_types(app_engine: AppEngine, event_name: str): def _create_post_event_route( - app_engine: AppEngine, *, - plugin: Optional[AppEngine] = None, - event_name: str, - event_info: EventDescriptor) -> web.RouteDef: + app_engine: AppEngine, + *, + plugin: Optional[AppEngine] = None, + event_name: str, + event_info: EventDescriptor, +) -> web.RouteDef: """ Creates route for handling POST event """ - datatype = find_datatype_handler(app_config=app_engine.app_config, event_name=event_name, event_info=event_info) - route = app_route_name(app_engine.app_config.app, event_name=event_name, - plugin=None if plugin is None else plugin.app_config.app, - override_route_name=event_info.route) + datatype = find_datatype_handler( + app_config=app_engine.app_config, event_name=event_name, event_info=event_info + ) + route = app_route_name( + app_engine.app_config.app, + event_name=event_name, + plugin=None if plugin is None else plugin.app_config.app, + override_route_name=event_info.route, + ) logger.info(__name__, f"POST path={route} input={str(datatype)}") impl = plugin if plugin else app_engine - handler = partial(_handle_post_invocation, app_engine, impl, - event_name, datatype, _auth_types(impl, event_name)) - setattr(handler, '__closure__', None) - setattr(handler, '__code__', _handle_post_invocation.__code__) - api_handler = api.add_route('post', route, handler) + handler = partial( + _handle_post_invocation, + app_engine, + impl, + event_name, + datatype, + _auth_types(impl, event_name), + ) + setattr(handler, "__closure__", None) + setattr(handler, "__code__", _handle_post_invocation.__code__) + api_handler = api.add_route("post", route, handler) return web.post(route, api_handler) def _create_get_event_route( - app_engine: AppEngine, *, - plugin: Optional[AppEngine] = None, - event_name: str, - event_info: EventDescriptor) -> web.RouteDef: + app_engine: AppEngine, + *, + plugin: Optional[AppEngine] = None, + event_name: str, + event_info: EventDescriptor, +) -> web.RouteDef: """ Creates route for handling GET requests """ - route = app_route_name(app_engine.app_config.app, event_name=event_name, - plugin=None if plugin is None else plugin.app_config.app, - override_route_name=event_info.route) + route = app_route_name( + app_engine.app_config.app, + event_name=event_name, + plugin=None if plugin is None else plugin.app_config.app, + override_route_name=event_info.route, + ) logger.info(__name__, f"GET path={route}") impl = plugin if plugin else app_engine - handler = partial(_handle_get_invocation, app_engine, impl, event_name, _auth_types(impl, event_name)) - setattr(handler, '__closure__', None) - setattr(handler, '__code__', _handle_get_invocation.__code__) - api_handler = api.add_route('get', route, handler) + handler = partial( + _handle_get_invocation, + app_engine, + impl, + event_name, + _auth_types(impl, event_name), + ) + setattr(handler, "__closure__", None) + setattr(handler, "__code__", _handle_get_invocation.__code__) + api_handler = api.add_route("get", route, handler) return web.get(route, api_handler) def _create_multipart_event_route( - app_engine: AppEngine, *, - plugin: Optional[AppEngine] = None, - event_name: str, - event_info: EventDescriptor) -> web.RouteDef: + app_engine: AppEngine, + *, + plugin: Optional[AppEngine] = None, + event_name: str, + event_info: EventDescriptor, +) -> web.RouteDef: """ Creates route for handling MULTIPART event """ - datatype = find_datatype_handler(app_config=app_engine.app_config, event_name=event_name, event_info=event_info) - route = app_route_name(app_engine.app_config.app, event_name=event_name, - plugin=None if plugin is None else plugin.app_config.app, - override_route_name=event_info.route) + datatype = find_datatype_handler( + app_config=app_engine.app_config, event_name=event_name, event_info=event_info + ) + route = app_route_name( + app_engine.app_config.app, + event_name=event_name, + plugin=None if plugin is None else plugin.app_config.app, + override_route_name=event_info.route, + ) logger.info(__name__, f"MULTIPART path={route} input={str(datatype)}") impl = plugin if plugin else app_engine - handler = partial(_handle_multipart_invocation, app_engine, impl, - event_name, datatype, _auth_types(impl, event_name)) - setattr(handler, '__closure__', None) - setattr(handler, '__code__', _handle_multipart_invocation.__code__) - api_handler = api.add_route('post', route, handler) + handler = partial( + _handle_multipart_invocation, + app_engine, + impl, + event_name, + datatype, + _auth_types(impl, event_name), + ) + setattr(handler, "__closure__", None) + setattr(handler, "__code__", _handle_multipart_invocation.__code__) + api_handler = api.add_route("post", route, handler) return web.post(route, api_handler) def _create_event_management_routes( - app_engine: AppEngine, *, - event_name: str, - event_info: EventDescriptor) -> List[web.RouteDef]: + app_engine: AppEngine, *, event_name: str, event_info: EventDescriptor +) -> List[web.RouteDef]: """ Create routes to start and stop processing of STREAM events """ - evt = event_name.replace('.', '/').replace('$', '/') - base_route = app_route_name(app_engine.app_config.app, event_name=evt, - prefix='mgmt', override_route_name=event_info.route) - logger.info(__name__, f"{event_info.type.value.upper()} path={base_route}/[start|stop]") + evt = event_name.replace(".", "/").replace("$", "/") + base_route = app_route_name( + app_engine.app_config.app, + event_name=evt, + prefix="mgmt", + override_route_name=event_info.route, + ) + logger.info( + __name__, f"{event_info.type.value.upper()} path={base_route}/[start|stop]" + ) handler: Optional[partial[Coroutine[Any, Any, Response]]] = None if event_info.type == EventType.STREAM: handler = partial(_handle_stream_start_invocation, app_engine, event_name) elif event_info.type == EventType.SERVICE: handler = partial(_handle_service_start_invocation, app_engine, event_name) - assert handler is not None, f"No handler for event={event_name} type={event_info.type}" + assert ( + handler is not None + ), f"No handler for event={event_name} type={event_info.type}" return [ - web.get(base_route + '/start', handler), + web.get(base_route + "/start", handler), web.get( - base_route + '/stop', - partial(_handle_event_stop_invocation, app_engine, event_name) - ) + base_route + "/stop", + partial(_handle_event_stop_invocation, app_engine, event_name), + ), ] -def _response(*, track_ids: Dict[str, str], key: str, payload: EventPayload, hook: PostprocessHook) -> ResponseType: +def _response( + *, track_ids: Dict[str, str], key: str, payload: EventPayload, hook: PostprocessHook +) -> ResponseType: """ Creates a web response object from a given payload (body), header track ids and applies a postprocess hook @@ -388,12 +476,12 @@ def _response(*, track_ids: Dict[str, str], key: str, payload: EventPayload, hoo response: ResponseType headers = { **hook.headers, - **{f"X-{re.sub(' ', '-', titlecase(k))}": v for k, v in track_ids.items()} + **{f"X-{re.sub(' ', '-', titlecase(k))}": v for k, v in track_ids.items()}, } if hook.file_response is not None: response = web.FileResponse( path=hook.file_response, - headers={'Content-Type': hook.content_type, **headers} + headers={"Content-Type": hook.content_type, **headers}, ) elif hook.stream_response is not None: response = hook.stream_response.resp @@ -403,9 +491,7 @@ def _response(*, track_ids: Dict[str, str], key: str, payload: EventPayload, hoo ) body = serializer(payload, key=key) response = web.Response( - body=body, - headers=headers, - content_type=hook.content_type + body=body, headers=headers, content_type=hook.content_type ) for name, cookie in hook.cookies.items(): value, args, kwargs = cookie @@ -418,55 +504,78 @@ def _response(*, track_ids: Dict[str, str], key: str, payload: EventPayload, hoo def _response_info(response: ResponseType): - return extra(prefix='response.', status=str(response.status)) + return extra(prefix="response.", status=str(response.status)) def _track_ids(request: web.Request) -> Dict[str, str]: return { - 'track.operation_id': str(uuid.uuid4()), - 'track.request_id': str(uuid.uuid4()), - 'track.request_ts': datetime.now(tz=timezone.utc).isoformat(), + "track.operation_id": str(uuid.uuid4()), + "track.request_id": str(uuid.uuid4()), + "track.request_ts": datetime.now(tz=timezone.utc).isoformat(), **{ "track." + snakecase(k[8:].lower()): v - for k, v in request.headers.items() if k.lower().startswith('x-track-') - } + for k, v in request.headers.items() + if k.lower().startswith("x-track-") + }, } -def _failed_response(context: Optional[EventContext], - e: Exception) -> web.Response: +def _failed_response(context: Optional[EventContext], e: Exception) -> web.Response: if context: logger.error(context, e) logger.failed(context) else: logger.error(__name__, e) info = ErrorInfo.from_exception(e) - return web.Response( - status=500, - body=Payload.to_json(info) - ) + return web.Response(status=500, body=Payload.to_json(info)) -def _ignored_response(context: Optional[EventContext], - status: int, - e: BaseException) -> web.Response: +def _ignored_response( + context: Optional[EventContext], status: int, e: BaseException +) -> web.Response: if context: logger.error(context, e) logger.ignored(context) else: logger.error(__name__, e) info = ErrorInfo.from_exception(e) - return web.Response( - status=status, - body=Payload.to_json(info) + return web.Response(status=status, body=Payload.to_json(info)) + + +async def _execute_setup_event( + app_engine: AppEngine, + plugin: Optional[AppEngine], + event_name: str, +) -> EventContext: + """ + Executes event of SETUP type, on server start + """ + event_settings = get_event_settings(app_engine.settings, event_name) + context = EventContext( + app_config=app_engine.app_config, + plugin_config=app_engine.app_config if plugin is None else plugin.app_config, + event_name=event_name, + settings=event_settings, + track_ids={}, + auth_info=auth_info_default, ) + logger.start(context) + if plugin is None: + res = await app_engine.execute(context=context, query_args=None, payload=None) + else: + res = await plugin.execute(context=context, query_args=None, payload=None) + logger.done(context, extra=metrics(context)) + + return res + def _request_start(app_engine: AppEngine, plugin: AppEngine, event_name: str, event_settings: EventSettings, - request: web.Request) -> EventContext: + request: web.Request, +) -> EventContext: """ Extracts context and track information from a request and logs start of event """ @@ -498,22 +607,26 @@ def _ignore_auth(request: web.Request, context: EventContext) -> str: AuthType.BASIC: _extract_auth_header, AuthType.BEARER: _extract_auth_header, AuthType.REFRESH: _extract_refresh_cookie, - AuthType.UNSECURED: _ignore_auth + AuthType.UNSECURED: _ignore_auth, } -def _extract_authorization(auth_methods: List[AuthType], request: web.Request, context: EventContext): +def _extract_authorization( + auth_methods: List[AuthType], request: web.Request, context: EventContext +): for auth_type in auth_methods: auth_header = AUTH_HEADER_EXTRACTORS[auth_type](request, context) if auth_header is not None: return auth_header - return 'Unsecured -' + return "Unsecured -" -def _validate_authorization(app_config: AppConfig, - context: EventContext, - auth_types: List[AuthType], - request: web.Request): +def _validate_authorization( + app_config: AppConfig, + context: EventContext, + auth_types: List[AuthType], + request: web.Request, +): """ Validates Authorization header from request to provide valid credentials for the methods supported in event configuration. @@ -530,11 +643,11 @@ def _validate_authorization(app_config: AppConfig, except ValueError as e: raise BadRequest("Malformed Authorization") from e - context.auth_info['allowed'] = False + context.auth_info["allowed"] = False for auth_type in auth_types: if method.upper() == auth_type.name.upper(): auth.validate_auth_method(auth_type, data, context) - if context.auth_info.get('allowed'): + if context.auth_info.get("allowed"): return None raise Unauthorized(method) @@ -548,47 +661,49 @@ def _text_response(result: str, *args, **kwargs) -> str: CONTENT_TYPE_BODY_SER: Dict[str, Callable[..., str]] = { - 'application/json': _application_json_response, - 'text/html': _text_response, - 'text/plain': _text_response + "application/json": _application_json_response, + "text/html": _text_response, + "text/plain": _text_response, } async def _request_execute( - app_engine: AppEngine, - event_name: str, - context: EventContext, - query_args: Dict[str, Any], - payload: Optional[EventPayloadType], - preprocess_hook: PreprocessHook, - request: web.Request) -> ResponseType: + app_engine: AppEngine, + event_name: str, + context: EventContext, + query_args: Dict[str, Any], + payload: Optional[EventPayloadType], + preprocess_hook: PreprocessHook, + request: web.Request, +) -> ResponseType: """ Executes request using engine event handler """ response_hook = PostprocessHook(request) result = await app_engine.preprocess( - context=context, query_args=query_args, payload=payload, request=preprocess_hook) + context=context, query_args=query_args, payload=payload, request=preprocess_hook + ) if (preprocess_hook.status is None) or (preprocess_hook.status == 200): - result = await app_engine.execute(context=context, query_args=query_args, payload=result) - result = await app_engine.postprocess(context=context, payload=result, response=response_hook) + result = await app_engine.execute( + context=context, query_args=query_args, payload=result + ) + result = await app_engine.postprocess( + context=context, payload=result, response=response_hook + ) else: response_hook.set_status(preprocess_hook.status) response = _response( - track_ids=context.track_ids, - key=event_name, - payload=result, - hook=response_hook + track_ids=context.track_ids, key=event_name, payload=result, hook=response_hook ) - logger.done(context, extra=combined( - _response_info(response), metrics(context) - )) + logger.done(context, extra=combined(_response_info(response), metrics(context))) return response async def _request_process_payload( - context: EventContext, - datatype: Optional[Type[EventPayloadType]], - request: web.Request) -> Tuple[Optional[EventPayloadType], Optional[bytes]]: + context: EventContext, + datatype: Optional[Type[EventPayloadType]], + request: web.Request, +) -> Tuple[Optional[EventPayloadType], Optional[bytes]]: """ Extract payload from request. Returns payload if parsing succeeded. Raises BadRequest if payload fails to parse @@ -604,12 +719,13 @@ async def _request_process_payload( async def _handle_post_invocation( - app_engine: AppEngine, - impl: AppEngine, - event_name: str, - datatype: Optional[Type[DataObject]], - auth_types: List[AuthType], - request: web.Request) -> ResponseType: + app_engine: AppEngine, + impl: AppEngine, + event_name: str, + datatype: Optional[Type[DataObject]], + auth_types: List[AuthType], + request: web.Request, +) -> ResponseType: """ Handler to execute POST calls """ @@ -619,10 +735,20 @@ async def _handle_post_invocation( context = _request_start(app_engine, impl, event_name, event_settings, request) query_args = dict(request.query) _validate_authorization(app_engine.app_config, context, auth_types, request) - payload, payload_raw = await _request_process_payload(context, datatype, request) - hook: PreprocessHook[NoopMultiparReader] = PreprocessHook(headers=request.headers, payload_raw=payload_raw) + payload, payload_raw = await _request_process_payload( + context, datatype, request + ) + hook: PreprocessHook[NoopMultiparReader] = PreprocessHook( + headers=request.headers, payload_raw=payload_raw + ) return await _request_execute( - impl, event_name, context, query_args, payload, preprocess_hook=hook, request=request + impl, + event_name, + context, + query_args, + payload, + preprocess_hook=hook, + request=request, ) except Unauthorized as e: return _ignored_response(context, 401, e) @@ -633,11 +759,12 @@ async def _handle_post_invocation( async def _handle_get_invocation( - app_engine: AppEngine, - impl: AppEngine, - event_name: str, - auth_types: List[AuthType], - request: web.Request) -> ResponseType: + app_engine: AppEngine, + impl: AppEngine, + event_name: str, + auth_types: List[AuthType], + request: web.Request, +) -> ResponseType: """ Handler to execute GET calls """ @@ -647,15 +774,20 @@ async def _handle_get_invocation( context = _request_start(app_engine, impl, event_name, event_settings, request) _validate_authorization(app_engine.app_config, context, auth_types, request) query_args = dict(request.query) - payload = query_args.get('payload') + payload = query_args.get("payload") if payload is not None: - del query_args['payload'] - hook: PreprocessHook[NoopMultiparReader] = PreprocessHook(headers=request.headers) + del query_args["payload"] + hook: PreprocessHook[NoopMultiparReader] = PreprocessHook( + headers=request.headers + ) return await _request_execute( - impl, event_name, context, - query_args, payload=payload, + impl, + event_name, + context, + query_args, + payload=payload, preprocess_hook=hook, - request=request + request=request, ) except Unauthorized as e: return _ignored_response(context, 401, e) @@ -666,12 +798,13 @@ async def _handle_get_invocation( async def _handle_multipart_invocation( - app_engine: AppEngine, - impl: AppEngine, - event_name: str, - datatype: Optional[Type[DataObject]], - auth_types: List[AuthType], - request: web.Request) -> ResponseType: + app_engine: AppEngine, + impl: AppEngine, + event_name: str, + datatype: Optional[Type[DataObject]], + auth_types: List[AuthType], + request: web.Request, +) -> ResponseType: """ Handler to execute POST calls """ @@ -681,14 +814,17 @@ async def _handle_multipart_invocation( context = _request_start(app_engine, impl, event_name, event_settings, request) query_args = dict(request.query) _validate_authorization(app_engine.app_config, context, auth_types, request) - hook = PreprocessHook( # type: ignore + hook = PreprocessHook( # type: ignore headers=request.headers, multipart_reader=await request.multipart() # type: ignore ) return await _request_execute( - impl, event_name, context, - query_args, payload=None, + impl, + event_name, + context, + query_args, + payload=None, preprocess_hook=hook, - request=request + request=request, ) except Unauthorized as e: return _ignored_response(context, 401, e) @@ -699,9 +835,8 @@ async def _handle_multipart_invocation( async def _handle_stream_start_invocation( - app_engine: AppEngine, - event_name: str, - request: web.Request) -> web.Response: + app_engine: AppEngine, event_name: str, request: web.Request +) -> web.Response: """ Handles call to stream processing event `start` endpoint, spawning an async job that listens continuosly to event streams @@ -715,9 +850,8 @@ async def _handle_stream_start_invocation( async def _handle_service_start_invocation( - app_engine: AppEngine, - event_name: str, - request: web.Request) -> web.Response: + app_engine: AppEngine, event_name: str, request: web.Request +) -> web.Response: """ Handles call to service event `start` endpoint, spawning an async job that listens continuosly __service__ @@ -731,9 +865,8 @@ async def _handle_service_start_invocation( async def _handle_event_stop_invocation( - app_engine: AppEngine, - event_name: str, - request: web.Request) -> web.Response: + app_engine: AppEngine, event_name: str, request: web.Request +) -> web.Response: """ Signals engine for stopping an event. Used to stop reading stream processing events. @@ -747,8 +880,19 @@ async def _handle_event_stop_invocation( return web.Response(status=500, body=str(e)) -ParsedArgs = namedtuple("ParsedArgs", ["host", "port", "path", "start_streams", - "config_files", "api_file", "api_auto", "enabled_groups"]) +ParsedArgs = namedtuple( + "ParsedArgs", + [ + "host", + "port", + "path", + "start_streams", + "config_files", + "api_file", + "api_auto", + "enabled_groups", + ], +) def parse_args(args) -> ParsedArgs: @@ -760,7 +904,7 @@ def parse_args(args) -> ParsedArgs: --start-streams, optional True if to auto start all events of STREAM type --config-files, is a comma-separated list of hopeit apps config files relative or full paths --api-file, optional path to openapi.json file with at least openapi and info sections - --api-auto, optional when api_file is not defined, specify a semicolons-separated + --api-auto, optional when api_file is not defined, specify a semicolons-separated `version;title;description` to define API General Info and enable OpenAPI --enabled-groups, optional list of group label to be started Example:: @@ -773,20 +917,26 @@ def parse_args(args) -> ParsedArgs: """ parser = argparse.ArgumentParser(description="hopeit.py engine") - parser.add_argument('--host') - parser.add_argument('--path') - parser.add_argument('--port') - parser.add_argument('--start-streams', action='store_true') - parser.add_argument('--config-files') - parser.add_argument('--api-file') - parser.add_argument('--api-auto') - parser.add_argument('--enabled-groups') + parser.add_argument("--host") + parser.add_argument("--path") + parser.add_argument("--port") + parser.add_argument("--start-streams", action="store_true") + parser.add_argument("--config-files") + parser.add_argument("--api-file") + parser.add_argument("--api-auto") + parser.add_argument("--enabled-groups") parsed_args = parser.parse_args(args=args) - port = int(parsed_args.port) if parsed_args.port else 8020 if parsed_args.path is None else None - config_files = parsed_args.config_files.split(',') - enabled_groups = parsed_args.enabled_groups.split(',') if parsed_args.enabled_groups else [] - api_auto = [] if parsed_args.api_auto is None else parsed_args.api_auto.split(';') + port = ( + int(parsed_args.port) + if parsed_args.port + else 8020 if parsed_args.path is None else None + ) + config_files = parsed_args.config_files.split(",") + enabled_groups = ( + parsed_args.enabled_groups.split(",") if parsed_args.enabled_groups else [] + ) + api_auto = [] if parsed_args.api_auto is None else parsed_args.api_auto.split(";") return ParsedArgs( host=parsed_args.host, @@ -796,12 +946,17 @@ def parse_args(args) -> ParsedArgs: config_files=config_files, api_file=parsed_args.api_file, api_auto=api_auto, - enabled_groups=enabled_groups + enabled_groups=enabled_groups, ) -def init_web_server(config_files: List[str], api_file: str, api_auto: List[str], enabled_groups: List[str], - start_streams: bool) -> web.Application: +def init_web_server( + config_files: List[str], + api_file: str, + api_auto: List[str], + enabled_groups: List[str], + start_streams: bool, +) -> web.Application: """ Init Web Server """ @@ -812,28 +967,42 @@ def init_web_server(config_files: List[str], api_file: str, api_auto: List[str], api_file=api_file, api_auto=api_auto, start_streams=start_streams, - enabled_groups=enabled_groups + enabled_groups=enabled_groups, ) return web_server -def serve(host: str, path: str, port: int, config_files:List[str], api_file: str, api_auto: List[str], - start_streams:bool, enabled_groups:List[str]): +def serve( + host: str, + path: str, + port: int, + config_files: List[str], + api_file: str, + api_auto: List[str], + start_streams: bool, + enabled_groups: List[str], +): """ Serve hopeit.engine """ - web_app = init_web_server(config_files, api_file, api_auto, enabled_groups, start_streams) - logger.info(__name__, f"Starting web server host: {host} port: {port} socket: {path}...") + web_app = init_web_server( + config_files, api_file, api_auto, enabled_groups, start_streams + ) + logger.info( + __name__, f"Starting web server host: {host} port: {port} socket: {path}..." + ) web.run_app(web_app, host=host, path=path, port=port) if __name__ == "__main__": sys_args = parse_args(sys.argv[1:]) - serve(host=sys_args.host, + serve( + host=sys_args.host, path=sys_args.path, port=sys_args.port, config_files=sys_args.config_files, api_file=sys_args.api_file, api_auto=sys_args.api_auto, start_streams=sys_args.start_streams, - enabled_groups=sys_args.enabled_groups) + enabled_groups=sys_args.enabled_groups, + ) \ No newline at end of file diff --git a/plugins/ops/apps-visualizer/api/openapi.json b/plugins/ops/apps-visualizer/api/openapi.json index ee06ac0b..ebeaf5f2 100644 --- a/plugins/ops/apps-visualizer/api/openapi.json +++ b/plugins/ops/apps-visualizer/api/openapi.json @@ -634,7 +634,8 @@ "POST", "STREAM", "SERVICE", - "MULTIPART" + "MULTIPART", + "SETUP" ], "x-enum-name": "EventType", "x-module-name": "hopeit.app.config" @@ -703,7 +704,7 @@ } }, "x-module-name": "hopeit.app.config", - "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " + "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE, SETUP\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " }, "EventConnection": { "type": "object", From 0c4244aa2f90b1bde811f35eaa4533fb65c60dcf Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Wed, 21 Feb 2024 13:15:30 -0300 Subject: [PATCH 2/7] siemple-example tests --- .../service/something_generator.py | 6 +- .../src/simple_example/setup_something.py | 6 +- .../test_it_something_generator.py | 56 ++++++++++++++----- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/apps/examples/simple-example/src/simple_example/service/something_generator.py b/apps/examples/simple-example/src/simple_example/service/something_generator.py index ea97a7ad..086ae0b1 100644 --- a/apps/examples/simple-example/src/simple_example/service/something_generator.py +++ b/apps/examples/simple-example/src/simple_example/service/something_generator.py @@ -20,12 +20,12 @@ async def __service__(context: EventContext) -> Spawn[SomethingParams]: i = 1 - if not os.path.exists("/tmp/hopeit.initializead"): + if not os.path.exists("/tmp/hopeit.initialized"): raise RuntimeError( - "Missing /tmp/hopeit.initializead file. " + "Missing /tmp/hopeit.initialized file. " "Service will not start until run setup_something." ) - os.remove("/tmp/hopeit.initializead") + os.remove("/tmp/hopeit.initialized") while service_running(context): logger.info(context, f"Generating something event {i}...") yield SomethingParams(f"id{i}", f"user{i}") diff --git a/apps/examples/simple-example/src/simple_example/setup_something.py b/apps/examples/simple-example/src/simple_example/setup_something.py index 2334918b..20636c0d 100644 --- a/apps/examples/simple-example/src/simple_example/setup_something.py +++ b/apps/examples/simple-example/src/simple_example/setup_something.py @@ -1,7 +1,7 @@ """ Simple Example: Setup Something -------------------------------------------------------------------- -Setup run before initialize endpoints streams and services +SETUP EventType runs before initializing endpoints, streams, and services. """ from pathlib import Path @@ -18,7 +18,7 @@ async def run_once(payload: None, context: EventContext): """ - Load objects that match the given wildcard + This method initializes the environment. """ logger.info(context, "Setup done") - Path("/tmp/hopeit.initializead").touch() + Path("/tmp/hopeit.initialized").touch() diff --git a/apps/examples/simple-example/test/integration/test_it_something_generator.py b/apps/examples/simple-example/test/integration/test_it_something_generator.py index 24d68b03..b1e33cb4 100644 --- a/apps/examples/simple-example/test/integration/test_it_something_generator.py +++ b/apps/examples/simple-example/test/integration/test_it_something_generator.py @@ -5,20 +5,48 @@ @pytest.mark.asyncio -async def test_it_something_generator(app_config, something_params_example): # noqa: F811 - result = await execute_event(app_config=app_config, - event_name='service.something_generator', - payload=something_params_example) - assert result == Something(something_params_example.id, - User(something_params_example.user, something_params_example.user)) +async def test_it_something_generator_with_setup_service( + app_config, something_params_example +): # noqa: F811 + results = await execute_event( + app_config=app_config, event_name="setup_something", payload=None + ) + results = await execute_service( + app_config=app_config, event_name="service.something_generator", max_events=2 + ) + assert results == [ + Something( + id="id1", user=User(id="user1", name="user1"), status=None, history=[] + ), + Something( + id="id2", user=User(id="user2", name="user2"), status=None, history=[] + ), + ] @pytest.mark.asyncio -async def test_it_something_generator_service(app_config, something_params_example): # noqa: F811 - results = await execute_service(app_config=app_config, - event_name='service.something_generator', - max_events=2) - assert results == [ - Something(id='id1', user=User(id='user1', name='user1'), status=None, history=[]), - Something(id='id2', user=User(id='user2', name='user2'), status=None, history=[]), - ] +async def test_it_something_generator( + app_config, something_params_example +): # noqa: F811 + result = await execute_event( + app_config=app_config, + event_name="service.something_generator", + payload=something_params_example, + ) + assert result == Something( + something_params_example.id, + User(something_params_example.user, something_params_example.user), + ) + + +@pytest.mark.asyncio +async def test_it_something_generator_service(app_config, something_params_example): + with pytest.raises(RuntimeError) as exc_info: + await execute_service( + app_config=app_config, + event_name="service.something_generator", + max_events=2, + ) + assert ( + str(exc_info.value) == "Service will not start until run setup_something." + ) From f82095bf3258032650701d08dff3247b31d4b023 Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Wed, 21 Feb 2024 14:59:28 -0300 Subject: [PATCH 3/7] test-plugins --- .../simple-example/config/app-config.json | 3 +- engine/src/hopeit/app/config.py | 2 +- engine/src/hopeit/server/web.py | 25 ++++++---- .../events_graph_data_standard.json | 7 +++ .../integration/runtime_simple_example.json | 46 ++++++++++++++++++- 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/apps/examples/simple-example/config/app-config.json b/apps/examples/simple-example/config/app-config.json index 1d90dc47..233f0389 100644 --- a/apps/examples/simple-example/config/app-config.json +++ b/apps/examples/simple-example/config/app-config.json @@ -67,8 +67,7 @@ }, "events": { "setup_something": { - "type": "SETUP", - "setting_keys": ["fs_storage"] + "type": "SETUP" }, "list_somethings": { "type": "GET", diff --git a/engine/src/hopeit/app/config.py b/engine/src/hopeit/app/config.py index bb6ee51b..50478c35 100644 --- a/engine/src/hopeit/app/config.py +++ b/engine/src/hopeit/app/config.py @@ -61,7 +61,7 @@ class EventType(str, Enum): STREAM: event triggered read events from stream. Can be started and stopped. SERVICE: event executed on demand or continuously. Long lived. Can be started and stopped. MULTIPART: event triggered from api postform-multipart request via endpoint. - SETUP: event that is executed once when service is starting + SETUP: event that is executed once when service is starting """ GET = 'GET' POST = 'POST' diff --git a/engine/src/hopeit/server/web.py b/engine/src/hopeit/server/web.py index 674b1a0e..52cfd1eb 100644 --- a/engine/src/hopeit/server/web.py +++ b/engine/src/hopeit/server/web.py @@ -253,7 +253,9 @@ def _enable_cors(prefix: str, cors: CorsConfig): cors.add(route) -async def _setup_app_event_routes(app_engine: AppEngine, plugin: Optional[AppEngine] = None): +async def _setup_app_event_routes( + app_engine: AppEngine, plugin: Optional[AppEngine] = None +): """ Setup http routes for existing events in app, in existing web_server global instance. @@ -546,7 +548,7 @@ async def _execute_setup_event( app_engine: AppEngine, plugin: Optional[AppEngine], event_name: str, -) -> EventContext: +) -> Optional[EventPayload]: """ Executes event of SETUP type, on server start """ @@ -570,10 +572,11 @@ async def _execute_setup_event( return res -def _request_start(app_engine: AppEngine, - plugin: AppEngine, - event_name: str, - event_settings: EventSettings, +def _request_start( + app_engine: AppEngine, + plugin: AppEngine, + event_name: str, + event_settings: EventSettings, request: web.Request, ) -> EventContext: """ @@ -585,7 +588,7 @@ def _request_start(app_engine: AppEngine, event_name=event_name, settings=event_settings, track_ids=_track_ids(request), - auth_info=auth_info_default + auth_info=auth_info_default, ) logger.start(context) return context @@ -595,12 +598,14 @@ def _extract_auth_header(request: web.Request, context: EventContext) -> Optiona return request.headers.get("Authorization") -def _extract_refresh_cookie(request: web.Request, context: EventContext) -> Optional[str]: +def _extract_refresh_cookie( + request: web.Request, context: EventContext +) -> Optional[str]: return request.cookies.get(f"{context.app_key}.refresh") def _ignore_auth(request: web.Request, context: EventContext) -> str: - return 'Unsecured -' + return "Unsecured -" AUTH_HEADER_EXTRACTORS = { @@ -1005,4 +1010,4 @@ def serve( api_auto=sys_args.api_auto, start_streams=sys_args.start_streams, enabled_groups=sys_args.enabled_groups, - ) \ No newline at end of file + ) diff --git a/plugins/ops/apps-visualizer/test/integration/events_graph_data_standard.json b/plugins/ops/apps-visualizer/test/integration/events_graph_data_standard.json index 5af54786..ebabafed 100644 --- a/plugins/ops/apps-visualizer/test/integration/events_graph_data_standard.json +++ b/plugins/ops/apps-visualizer/test/integration/events_graph_data_standard.json @@ -55,6 +55,13 @@ }, "classes": "EVENT" }, + "simple_example.${APPS_ROUTE_VERSION}.setup_something": { + "data": { + "id": "simple_example.${APPS_ROUTE_VERSION}.setup_something", + "content": "simple_example.${APPS_ROUTE_VERSION}\nsetup_something" + }, + "classes": "EVENT" +}, "simple_example.${APPS_ROUTE_VERSION}.list_somethings.GET": { "data": { "id": "simple_example.${APPS_ROUTE_VERSION}.list_somethings.GET", diff --git a/plugins/ops/config-manager/test/integration/runtime_simple_example.json b/plugins/ops/config-manager/test/integration/runtime_simple_example.json index b78dfceb..854c998d 100644 --- a/plugins/ops/config-manager/test/integration/runtime_simple_example.json +++ b/plugins/ops/config-manager/test/integration/runtime_simple_example.json @@ -40,6 +40,15 @@ } }, "events": { + "setup_something": { + "type": "SETUP", + "plug_mode": "Standalone", + "connections": [], + "auth": [], + "setting_keys": [], + "dataobjects": [], + "group": "DEFAULT" + }, "list_somethings": { "type": "GET", "plug_mode": "Standalone", @@ -356,7 +365,28 @@ } }, "effective_settings": { - "list_somethings": { + "setup_something": { + "response_timeout": 60.0, + "logging": { + "extra_fields": [], + "stream_fields": [ + "stream.name", + "stream.msg_id", + "stream.consumer_group" + ] + }, + "stream": { + "timeout": 60.0, + "target_max_len": 0, + "throttle_ms": 0, + "step_delay": 0, + "batch_size": 100, + "compression": "lz4", + "serialization": "json+base64" + }, + "extras": {} + }, + "list_somethings": { "response_timeout": 60.0, "logging": { "extra_fields": [], @@ -850,6 +880,15 @@ "dataobjects": [], "group": "DEFAULT" }, + "simple_example.${APPS_ROUTE_VERSION}.setup_something": { + "type": "SETUP", + "plug_mode": "Standalone", + "connections": [], + "auth": [], + "setting_keys": [], + "dataobjects": [], + "group": "DEFAULT" + }, "simple_example.${APPS_ROUTE_VERSION}.list_somethings": { "type": "GET", "plug_mode": "Standalone", @@ -1161,7 +1200,10 @@ "streams": { "stream_manager": "hopeit.streams.NoStreamManager", "connection_str": "<>", - "delay_auto_start_seconds": 3 + "delay_auto_start_seconds": 3, + "initial_backoff_seconds": 1.0, + "max_backoff_seconds": 60.0, + "num_failures_open_circuit_breaker": 1 }, "logging": { "log_level": "DEBUG", From 5b6c748f498e365c9c73203da335dfa783fbf2d3 Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Wed, 21 Feb 2024 15:12:18 -0300 Subject: [PATCH 4/7] linting --- .../src/simple_example/service/something_generator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/examples/simple-example/src/simple_example/service/something_generator.py b/apps/examples/simple-example/src/simple_example/service/something_generator.py index 086ae0b1..3c11908f 100644 --- a/apps/examples/simple-example/src/simple_example/service/something_generator.py +++ b/apps/examples/simple-example/src/simple_example/service/something_generator.py @@ -19,6 +19,10 @@ async def __service__(context: EventContext) -> Spawn[SomethingParams]: + """ + Generate SomethingParams asynchronously in a loop until the service is stopped. + """ + i = 1 if not os.path.exists("/tmp/hopeit.initialized"): raise RuntimeError( @@ -37,6 +41,7 @@ async def __service__(context: EventContext) -> Spawn[SomethingParams]: async def create_something( payload: SomethingParams, context: EventContext ) -> Something: + """Create a Something object asynchronously.""" logger.info( context, "Creating something...", From e2817d3f0b25b18e1efbdd5345f8c0fde8db3b28 Mon Sep 17 00:00:00 2001 From: Leo Smerling <61629371+leosmerling-hopeit@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:17:00 +0100 Subject: [PATCH 5/7] Test web.py module --- engine/src/hopeit/server/web.py | 8 +++----- engine/test/integration/server/test_it_web.py | 7 ++++++- engine/test/mock_app/__init__.py | 3 +++ engine/test/mock_app/mock_event_setup.py | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 engine/test/mock_app/mock_event_setup.py diff --git a/engine/src/hopeit/server/web.py b/engine/src/hopeit/server/web.py index 52cfd1eb..a527bda0 100644 --- a/engine/src/hopeit/server/web.py +++ b/engine/src/hopeit/server/web.py @@ -548,7 +548,7 @@ async def _execute_setup_event( app_engine: AppEngine, plugin: Optional[AppEngine], event_name: str, -) -> Optional[EventPayload]: +) -> None: """ Executes event of SETUP type, on server start """ @@ -564,13 +564,11 @@ async def _execute_setup_event( logger.start(context) if plugin is None: - res = await app_engine.execute(context=context, query_args=None, payload=None) + await app_engine.execute(context=context, query_args=None, payload=None) else: - res = await plugin.execute(context=context, query_args=None, payload=None) + await plugin.execute(context=context, query_args=None, payload=None) logger.done(context, extra=metrics(context)) - return res - def _request_start( app_engine: AppEngine, diff --git a/engine/test/integration/server/test_it_web.py b/engine/test/integration/server/test_it_web.py index e2f6f11f..009d7f01 100644 --- a/engine/test/integration/server/test_it_web.py +++ b/engine/test/integration/server/test_it_web.py @@ -13,7 +13,7 @@ from hopeit.server.web import server_startup_hook, stop_server, app_startup_hook, stream_startup_hook from mock_engine import MockStreamManager, MockEventHandler -from mock_app import MockResult, mock_app_config # type: ignore # noqa: F401 +from mock_app import MockResult, mock_app_config, mock_event_setup # type: ignore # noqa: F401 from mock_plugin import mock_plugin_config # type: ignore # noqa: F401 @@ -490,6 +490,10 @@ async def call_get_plugin_event_on_app(client): assert result == '{"plugin_event": "PluginEvent.postprocess"}' +async def check_setup_event_on_start(client): + assert mock_event_setup.initialized + + async def call_start_stream(client): res = await client.get( '/mgmt/mock-app/test/mock-stream-event/start' @@ -580,6 +584,7 @@ async def test_endpoints(monkeypatch, logger.addHandler(ch) test_list = [ + check_setup_event_on_start, call_get_mock_event, call_get_mock_event_cors_allowed, call_get_mock_event_cors_unknown, diff --git a/engine/test/mock_app/__init__.py b/engine/test/mock_app/__init__.py index c2eb9197..291310a3 100644 --- a/engine/test/mock_app/__init__.py +++ b/engine/test/mock_app/__init__.py @@ -116,6 +116,9 @@ def mock_app_config(): } }, events={ + "mock_event_setup": EventDescriptor( + type=EventType.SETUP, + ), "mock_event": EventDescriptor( type=EventType.GET, route='mock-app/test/mock-event-test', diff --git a/engine/test/mock_app/mock_event_setup.py b/engine/test/mock_app/mock_event_setup.py new file mode 100644 index 00000000..3da94705 --- /dev/null +++ b/engine/test/mock_app/mock_event_setup.py @@ -0,0 +1,19 @@ +""" +test setup event +""" + +from hopeit.app.context import EventContext +from hopeit.app.logger import app_extra_logger + + +__steps__ = ["setup"] + +initialized = False + +logger, extra = app_extra_logger() + + +async def setup(payload: None, context: EventContext): + global initialized + initialized = True + logger.info(context, "Setup done") From dcbd423f84b9377e447159da0f72e067d03afa45 Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Fri, 23 Feb 2024 09:55:37 -0300 Subject: [PATCH 6/7] tests; bump version --- apps/examples/client-example/api/openapi.json | 24 +- apps/examples/simple-example/api/openapi.json | 86 +-- docs/source/release-notes.rst | 9 +- .../schemas/app-config-schema-draftv6.json | 9 +- .../schemas/server-config-schema-draftv6.json | 4 +- engine/src/hopeit/server/version.py | 2 +- engine/test/integration/server/test_it_web.py | 578 ++++++++++-------- engine/test/mock_plugin/__init__.py | 29 +- engine/test/mock_plugin/plugin_setup.py | 19 + engine/test/unit/server/test_engine.py | 8 +- plugins/ops/apps-visualizer/api/openapi.json | 22 +- 11 files changed, 432 insertions(+), 358 deletions(-) create mode 100644 engine/test/mock_plugin/plugin_setup.py diff --git a/apps/examples/client-example/api/openapi.json b/apps/examples/client-example/api/openapi.json index e25fa316..fcdab0c3 100644 --- a/apps/examples/client-example/api/openapi.json +++ b/apps/examples/client-example/api/openapi.json @@ -1,12 +1,12 @@ { "openapi": "3.0.3", "info": { - "version": "0.21", + "version": "0.22", "title": "Client Example", "description": "Client Example" }, "paths": { - "/api/config-manager/0x21/runtime-apps-config": { + "/api/config-manager/0x22/runtime-apps-config": { "get": { "summary": "Config Manager: Runtime Apps Config", "description": "Returns the runtime config for the Apps running on this server", @@ -62,11 +62,11 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, - "/api/config-manager/0x21/cluster-apps-config": { + "/api/config-manager/0x22/cluster-apps-config": { "get": { "summary": "Config Manager: Cluster Apps Config", "description": "Handle remote access to runtime configuration for a group of hosts", @@ -122,11 +122,11 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, - "/api/client-example/0x21/call-unsecured": { + "/api/client-example/0x22/call-unsecured": { "get": { "summary": "Client Example: Call Unsecured", "description": "List all available Something objects connecting to simple-example app", @@ -196,11 +196,11 @@ } }, "tags": [ - "client_example.0x21" + "client_example.0x22" ] } }, - "/api/client-example/0x21/count-and-save": { + "/api/client-example/0x22/count-and-save": { "get": { "summary": "Client Example: Count Objects and Save new one", "description": "Count all available Something objects connecting to simple-example app", @@ -267,7 +267,7 @@ } }, "tags": [ - "client_example.0x21" + "client_example.0x22" ], "security": [ { @@ -276,7 +276,7 @@ ] } }, - "/api/client-example/0x21/handle-responses": { + "/api/client-example/0x22/handle-responses": { "get": { "summary": "Client Example: Handle Responses", "description": "Non default responses and UnhandledResponse exception\n\nTo manage different types of responses from the same endpoint we can use the `responses` parameter where we list the\nhttp response status codes expected and the corresponding data type for each one. In this example `app_call` expect\nand handle, 200 and 404 responses.\n\nAlso in the code you can see how to handle an expection of type `UnhandledResponse` and log as warining.", @@ -361,7 +361,7 @@ } }, "tags": [ - "client_example.0x21" + "client_example.0x22" ], "security": [ { @@ -835,7 +835,7 @@ }, "engine_version": { "type": "string", - "default": "0.21.3" + "default": "0.22.0" } }, "x-module-name": "hopeit.server.config", diff --git a/apps/examples/simple-example/api/openapi.json b/apps/examples/simple-example/api/openapi.json index 424469f5..72e56b4c 100644 --- a/apps/examples/simple-example/api/openapi.json +++ b/apps/examples/simple-example/api/openapi.json @@ -1,12 +1,12 @@ { "openapi": "3.0.3", "info": { - "version": "0.21", + "version": "0.22", "title": "Simple Example", "description": "Simple Example" }, "paths": { - "/api/basic-auth/0x21/decode": { + "/api/basic-auth/0x22/decode": { "get": { "summary": "Basic Auth: Decode", "description": "Returns decoded auth info", @@ -44,7 +44,7 @@ } }, "tags": [ - "basic_auth.0x21" + "basic_auth.0x22" ], "security": [ { @@ -53,7 +53,7 @@ ] } }, - "/api/config-manager/0x21/runtime-apps-config": { + "/api/config-manager/0x22/runtime-apps-config": { "get": { "summary": "Config Manager: Runtime Apps Config", "description": "Returns the runtime config for the Apps running on this server", @@ -109,11 +109,11 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, - "/api/config-manager/0x21/cluster-apps-config": { + "/api/config-manager/0x22/cluster-apps-config": { "get": { "summary": "Config Manager: Cluster Apps Config", "description": "Handle remote access to runtime configuration for a group of hosts", @@ -169,11 +169,11 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, - "/api/simple-example/0x21/list-somethings": { + "/api/simple-example/0x22/list-somethings": { "get": { "summary": "Simple Example: List Objects", "description": "Lists all available Something objects", @@ -243,7 +243,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -252,7 +252,7 @@ ] } }, - "/api/simple-example/0x21/list-somethings-unsecured": { + "/api/simple-example/0x22/list-somethings-unsecured": { "get": { "summary": "Simple Example: List Objects Unsecured", "description": "Lists all available Something objects", @@ -322,11 +322,11 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ] } }, - "/api/simple-example/0x21/query-something": { + "/api/simple-example/0x22/query-something": { "get": { "summary": "Simple Example: Query Something", "description": "Loads Something from disk", @@ -412,7 +412,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -516,7 +516,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -525,7 +525,7 @@ ] } }, - "/api/simple-example/0x21/save-something": { + "/api/simple-example/0x22/save-something": { "post": { "summary": "Simple Example: Save Something", "description": "Creates and saves Something", @@ -622,7 +622,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -631,7 +631,7 @@ ] } }, - "/api/simple-example/0x21/download-something": { + "/api/simple-example/0x22/download-something": { "get": { "summary": "Simple Example: Download Something", "description": "Download image file. The PostprocessHook return the requested file as stream.", @@ -718,7 +718,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -727,7 +727,7 @@ ] } }, - "/api/simple-example/0x21/download-something-streamed": { + "/api/simple-example/0x22/download-something-streamed": { "get": { "summary": "Simple Example: Download Something Streamed", "description": "Download streamd created content as file.\nThe PostprocessHook return the requested resource as stream using `prepare_stream_response`.", @@ -795,11 +795,11 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ] } }, - "/api/simple-example/0x21/upload-something": { + "/api/simple-example/0x22/upload-something": { "post": { "summary": "Simple Example: Multipart Upload files", "description": "Upload files using Multipart form request", @@ -937,7 +937,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -946,7 +946,7 @@ ] } }, - "/api/simple-example/0x21/streams/something-event": { + "/api/simple-example/0x22/streams/something-event": { "post": { "summary": "Simple Example: Something Event", "description": "Submits a Something object to a stream to be processed asynchronously by process-events app event", @@ -1015,7 +1015,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1024,7 +1024,7 @@ ] } }, - "/api/simple-example/0x21/collector/query-concurrently": { + "/api/simple-example/0x22/collector/query-concurrently": { "post": { "summary": "Simple Example: Query Concurrently", "description": "Loads 2 Something objects concurrently from disk and combine the results\nusing `collector` steps constructor (instantiating an `AsyncCollector`)", @@ -1096,7 +1096,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1105,7 +1105,7 @@ ] } }, - "/api/simple-example/0x21/collector/collect-spawn": { + "/api/simple-example/0x22/collector/collect-spawn": { "post": { "summary": "Simple Example: Collect and Spawn", "description": "Loads 2 Something objects concurrently from disk, combine the results\nusing `collector` steps constructor (instantiating an `AsyncCollector`)\nthen spawn the items found individually into a stream", @@ -1183,7 +1183,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1192,7 +1192,7 @@ ] } }, - "/api/simple-example/0x21/shuffle/spawn-event": { + "/api/simple-example/0x22/shuffle/spawn-event": { "post": { "summary": "Simple Example: Spawn Event", "description": "This example will spawn 3 data events, those are going to be send to a stream using SHUFFLE\nand processed in asynchronously / in parallel if multiple nodes are available", @@ -1270,7 +1270,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1279,7 +1279,7 @@ ] } }, - "/api/simple-example/0x21/shuffle/parallelize-event": { + "/api/simple-example/0x22/shuffle/parallelize-event": { "post": { "summary": "Simple Example: Parallelize Event", "description": "This example will spawn 2 copies of payload data, those are going to be send to a stream using SHUFFLE\nand processed in asynchronously / in parallel if multiple nodes are available,\nthen submitted to other stream to be updated and saved", @@ -1357,7 +1357,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1366,7 +1366,7 @@ ] } }, - "/api/simple-example/0x21/basic-auth/0x21/login": { + "/api/simple-example/0x22/basic-auth/0x22/login": { "get": { "summary": "Basic Auth: Login", "description": "Handles users login using basic-auth\nand generate access tokens for external services invoking apps\nplugged in with basic-auth plugin.", @@ -1434,7 +1434,7 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { @@ -1443,7 +1443,7 @@ ] } }, - "/api/simple-example/0x21/basic-auth/0x21/refresh": { + "/api/simple-example/0x22/basic-auth/0x22/refresh": { "get": { "summary": "Basic Auth: Refresh", "description": "This event can be used for obtain new access token and update refresh token (http cookie),\nwith no need to re-login the user if there is a valid refresh token active.", @@ -1511,16 +1511,16 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { - "simple_example.0x21.refresh": [] + "simple_example.0x22.refresh": [] } ] } }, - "/api/simple-example/0x21/basic-auth/0x21/logout": { + "/api/simple-example/0x22/basic-auth/0x22/logout": { "get": { "summary": "Basic Auth: Logout", "description": "Invalidates previous refresh cookies.", @@ -1597,11 +1597,11 @@ } }, "tags": [ - "simple_example.0x21" + "simple_example.0x22" ], "security": [ { - "simple_example.0x21.refresh": [] + "simple_example.0x22.refresh": [] } ] } @@ -2133,7 +2133,7 @@ }, "engine_version": { "type": "string", - "default": "0.21.3" + "default": "0.22.0" } }, "x-module-name": "hopeit.server.config", @@ -2406,10 +2406,10 @@ "type": "http", "scheme": "bearer" }, - "simple_example.0x21.refresh": { + "simple_example.0x22.refresh": { "type": "apiKey", "in": "cookie", - "name": "simple_example.0x21.refresh" + "name": "simple_example.0x22.refresh" } } }, diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index d85f62a4..8c421150 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -1,11 +1,18 @@ Release Notes ============= +Version 0.22.0 +______________ +- Engine: + + - SETUP: new EventType, runs before initializing endpoints, streams, and services + + Version 0.21.3 ______________ - Plugin: - fs-storage: Resolved compatibility issues with long file names on Windows + - fs-storage: Resolved compatibility issues with long file names on Windows Version 0.21.2 diff --git a/engine/config/schemas/app-config-schema-draftv6.json b/engine/config/schemas/app-config-schema-draftv6.json index a591846c..9a4cf576 100644 --- a/engine/config/schemas/app-config-schema-draftv6.json +++ b/engine/config/schemas/app-config-schema-draftv6.json @@ -205,7 +205,8 @@ "POST", "STREAM", "SERVICE", - "MULTIPART" + "MULTIPART", + "SETUP" ] }, "plug_mode": { @@ -267,7 +268,7 @@ "default": "DEFAULT" } }, - "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " + "description": "\n Event Descriptor: configures event implementation\n\n :field: type, EventType: type of event i.e.: GET, POST, MULTIPART, STREAM, SERVICE, SETUP\n :field: plug_mode, EventPlugMode: defines whether an event defined in a plugin is created in the\n current app (ON_APP) or it will be created in the original plugin (STANDALONE, default)\n :field: route, optional str: custom route for endpoint. If not specified route will be derived\n from `/api/app_name/app_version/event_name`\n :field: impl, optional str: custom event implementation Python module. If not specified, module\n with same same as event will be imported.\n :field: connections, list of EventConnection: specifies dependencies on other apps/endpoints,\n that can be used by client plugins to call events on external apps\n :field: read_stream, optional ReadStreamDescriptor: specifies source stream to read from.\n Valid only for STREAM events.\n :field: write_stream, optional WriteStreamDescriptor: for any type of events, resultant dataobjects will\n be published to the specified stream.\n :field: auth, list of AuthType: supported authentication schemas for this event. If not specified\n application default will be used.\n :field: setting_keys, list of str: by default EventContext will have access to the settings section\n with the same name of the event using `settings = context.settings(datatype=MySettingsType)`.\n In case additional sections are needed to be accessed from\n EventContext, then a list of setting keys, including the name of the event if needed,\n can be specified here. Then access to a `custom` key can be done using\n `custom_settings = context.settings(key=\"customer\", datatype=MyCustomSettingsType)`\n :field: dataobjects, list of str: list of full qualified dataobject types that this event can process.\n When not specified, the engine will inspect the module implementation and find all datatypes supported\n as payload in the functions defined as `__steps__`. In case of generic functions that support\n `payload: DataObject` argument, then a list of full qualified datatypes must be specified here.\n :field: group, str: group name, if none is assigned it is automatically assigned as 'DEFAULT'.\n " }, "EventConnection": { "type": "object", @@ -390,7 +391,7 @@ }, "engine_version": { "type": "string", - "default": "0.21.0" + "default": "0.22.0" } }, "description": "\n Server configuration\n " @@ -423,7 +424,7 @@ "default": 1 } }, - "description": "\n :field connection_str: str, url to connect to streams server: i.e. redis://localhost:6379\n " + "description": "\n :field connection_str: str, url to connect to streams server: i.e. redis://localhost:6379\n if using redis stream manager plugin to connect locally\n " }, "LoggingConfig": { "type": "object", diff --git a/engine/config/schemas/server-config-schema-draftv6.json b/engine/config/schemas/server-config-schema-draftv6.json index b4307568..55b86a33 100644 --- a/engine/config/schemas/server-config-schema-draftv6.json +++ b/engine/config/schemas/server-config-schema-draftv6.json @@ -41,7 +41,7 @@ }, "engine_version": { "type": "string", - "default": "0.21.0" + "default": "0.22.0" } }, "description": "\n Server configuration\n ", @@ -75,7 +75,7 @@ "default": 1 } }, - "description": "\n :field connection_str: str, url to connect to streams server: i.e. redis://localhost:6379\n " + "description": "\n :field connection_str: str, url to connect to streams server: i.e. redis://localhost:6379\n if using redis stream manager plugin to connect locally\n " }, "LoggingConfig": { "type": "object", diff --git a/engine/src/hopeit/server/version.py b/engine/src/hopeit/server/version.py index 98c394e8..5f9e2e1d 100644 --- a/engine/src/hopeit/server/version.py +++ b/engine/src/hopeit/server/version.py @@ -8,7 +8,7 @@ import sys ENGINE_NAME = "hopeit.engine" -ENGINE_VERSION = "0.21.3" +ENGINE_VERSION = "0.22.0" # Major.Minor version to be used in App versions and Api endpoints for Apps/Plugins APPS_API_VERSION = '.'.join(ENGINE_VERSION.split('.')[0:2]) diff --git a/engine/test/integration/server/test_it_web.py b/engine/test/integration/server/test_it_web.py index 009d7f01..4e680eee 100644 --- a/engine/test/integration/server/test_it_web.py +++ b/engine/test/integration/server/test_it_web.py @@ -10,339 +10,360 @@ from aiohttp.web import Application from hopeit.server import api, runtime, engine, web -from hopeit.server.web import server_startup_hook, stop_server, app_startup_hook, stream_startup_hook +from hopeit.server.web import ( + server_startup_hook, + stop_server, + app_startup_hook, + stream_startup_hook, +) from mock_engine import MockStreamManager, MockEventHandler from mock_app import MockResult, mock_app_config, mock_event_setup # type: ignore # noqa: F401 -from mock_plugin import mock_plugin_config # type: ignore # noqa: F401 +from mock_plugin import mock_plugin_config, plugin_setup # type: ignore # noqa: F401 async def call_get_mock_event(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'ok'}, - headers={'X-Track-Session-Id': 'test_session_id'} + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "ok"}, + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get('X-Status') == 'ok' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert res.headers.get("X-Status") == "ok" result = (await res.read()).decode() assert result == '{"mock_event": "ok: ok"}' async def call_get_mock_event_cors_allowed(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "ok"}, headers={ - 'X-Track-Session-Id': 'test_session_id', - 'Origin': 'http://test', - 'Host': 'test' - } + "X-Track-Session-Id": "test_session_id", + "Origin": "http://test", + "Host": "test", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get('Access-Control-Allow-Origin') == 'http://test' - assert res.headers.get('Access-Control-Allow-Credentials') == 'true' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert res.headers.get("Access-Control-Allow-Origin") == "http://test" + assert res.headers.get("Access-Control-Allow-Credentials") == "true" result = (await res.read()).decode() assert result == '{"mock_event": "ok: ok"}' async def call_get_mock_event_cors_unknown(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "ok"}, headers={ - 'X-Track-Session-Id': 'test_session_id', - 'Origin': 'http://unknown', - 'Host': 'test' - } + "X-Track-Session-Id": "test_session_id", + "Origin": "http://unknown", + "Host": "test", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get('Access-Control-Allow-Origin') is None - assert res.headers.get('Access-Control-Allow-Credentials') is None - assert res.cookies.get('Test-Cookie').value == 'ok' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert res.headers.get("Access-Control-Allow-Origin") is None + assert res.headers.get("Access-Control-Allow-Credentials") is None + assert res.cookies.get("Test-Cookie").value == "ok" result = (await res.read()).decode() assert result == '{"mock_event": "ok: ok"}' async def call_get_mock_event_special_case(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'no-ok'}, - headers={'x-track-session-id': 'test_session_id'} + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "no-ok"}, + headers={"x-track-session-id": "test_session_id"}, ) assert res.status == 400 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.cookies.get('Test-Cookie').value == '' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert res.cookies.get("Test-Cookie").value == "" result = (await res.read()).decode() assert result == '{"mock_event": "not-ok: None"}' async def call_get_fail_request(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'fail'}, - headers={ - 'X-Track-Session-Id': 'test_session_id' - } + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "fail"}, + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 500 result = (await res.read()).decode() - assert result == '{"msg": "Test for error", "tb": ["AssertionError: Test for error\\n"]}' + assert ( + result + == '{"msg": "Test for error", "tb": ["AssertionError: Test for error\\n"]}' + ) async def call_post_mock_event(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "ok"}, data='{"value": "ok"}', headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"value": "ok: ok", "processed": true}' async def call_post_mock_event_preprocess(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-post-preprocess', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-post-preprocess", + params={"query_arg1": "ok"}, data='{"value": "ok"}', headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"value": "ok: test_request_id"}' async def call_post_mock_event_preprocess_no_datatype(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-post-preprocess-no-datatype', - params={'query_arg1': 'ok'}, - data='OK\n', + "/api/mock-app/test/mock-post-preprocess-no-datatype", + params={"query_arg1": "ok"}, + data="OK\n", headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"value": "ok: test_request_id"}' async def call_multipart_mock_event(client): - os.makedirs('/tmp/call_multipart_mock_event/', exist_ok=True) - with open('/tmp/call_multipart_mock_event/test_attachment', 'wb') as f: - f.write(b'testdata') + os.makedirs("/tmp/call_multipart_mock_event/", exist_ok=True) + with open("/tmp/call_multipart_mock_event/test_attachment", "wb") as f: + f.write(b"testdata") - attachment = open('/tmp/call_multipart_mock_event/test_attachment', 'rb') + attachment = open("/tmp/call_multipart_mock_event/test_attachment", "rb") with aiohttp.MultipartWriter("form-data", boundary=":") as mp: - mp.append("value1", headers={ - 'Content-Disposition': 'form-data; name="field1"' - }) - mp.append_json({"value": "value2"}, headers={ - 'Content-Disposition': 'form-data; name="field2"' - }) - mp.append(attachment, headers={ - 'Content-Disposition': 'attachments; name="attachment"; filename="test_attachment"' - }) + mp.append("value1", headers={"Content-Disposition": 'form-data; name="field1"'}) + mp.append_json( + {"value": "value2"}, + headers={"Content-Disposition": 'form-data; name="field2"'}, + ) + mp.append( + attachment, + headers={ + "Content-Disposition": 'attachments; name="attachment"; filename="test_attachment"' + }, + ) res: ClientResponse = await client.post( - '/api/mock-app/test/mock-multipart-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-multipart-event-test", + params={"query_arg1": "ok"}, data=mp, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id', - 'Content-Type': 'multipart/form-data; boundary=":"'} - ) + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + "Content-Type": 'multipart/form-data; boundary=":"', + }, + ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() - assert result == '{"value": "field1=value1 field2=value2 attachment=test_attachment ok"}' + assert ( + result + == '{"value": "field1=value1 field2=value2 attachment=test_attachment ok"}' + ) async def call_multipart_mock_event_plain_text_json(client): - os.makedirs('/tmp/call_multipart_mock_event/', exist_ok=True) - with open('/tmp/call_multipart_mock_event/test_attachment', 'wb') as f: - f.write(b'testdata') + os.makedirs("/tmp/call_multipart_mock_event/", exist_ok=True) + with open("/tmp/call_multipart_mock_event/test_attachment", "wb") as f: + f.write(b"testdata") - attachment = open('/tmp/call_multipart_mock_event/test_attachment', 'rb') + attachment = open("/tmp/call_multipart_mock_event/test_attachment", "rb") with aiohttp.MultipartWriter("form-data", boundary=":") as mp: - mp.append("value1", headers={ - 'Content-Disposition': 'form-data; name="field1"' - }) - mp.append('{"value": "value2"}', headers={ - 'Content-Disposition': 'form-data; name="field2"' - }) - mp.append(attachment, headers={ - 'Content-Disposition': 'attachments; name="attachment"; filename="test_attachment"' - }) + mp.append("value1", headers={"Content-Disposition": 'form-data; name="field1"'}) + mp.append( + '{"value": "value2"}', + headers={"Content-Disposition": 'form-data; name="field2"'}, + ) + mp.append( + attachment, + headers={ + "Content-Disposition": 'attachments; name="attachment"; filename="test_attachment"' + }, + ) res: ClientResponse = await client.post( - '/api/mock-app/test/mock-multipart-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-multipart-event-test", + params={"query_arg1": "ok"}, data=mp, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id', - 'Content-Type': 'multipart/form-data; boundary=":"'} - ) + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + "Content-Type": 'multipart/form-data; boundary=":"', + }, + ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() - assert result == '{"value": "field1=value1 field2=value2 attachment=test_attachment ok"}' + assert ( + result + == '{"value": "field1=value1 field2=value2 attachment=test_attachment ok"}' + ) async def call_multipart_mock_event_bad_request(client): with aiohttp.MultipartWriter("form-data", boundary=":") as mp: - mp.append("value1", headers={'Content-Disposition': 'form-data; name="field1"'}) - mp.append("value2", headers={'Content-Disposition': 'form-data; name="field2"'}) + mp.append("value1", headers={"Content-Disposition": 'form-data; name="field1"'}) + mp.append("value2", headers={"Content-Disposition": 'form-data; name="field2"'}) res: ClientResponse = await client.post( - '/api/mock-app/test/mock-multipart-event-test', + "/api/mock-app/test/mock-multipart-event-test", data=mp, - params={'query_arg1': 'bad form'}, + params={"query_arg1": "bad form"}, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id', - 'Content-Type': 'multipart/form-data; boundary=":"'} - ) + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + "Content-Type": 'multipart/form-data; boundary=":"', + }, + ) assert res.status == 400 async def call_post_nopayload(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-post-nopayload', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-post-nopayload", + params={"query_arg1": "ok"}, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"mock_post_nopayload": "ok: nopayload ok"}' async def call_post_invalid_payload(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'ok'}, + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "ok"}, data='{"value": invalid_json}', - headers={ - 'X-Track-Session-Id': 'test_session_id' - } + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 400 result = (await res.read()).decode() - assert result == \ - '{"msg": "Expecting value: line 1 column 11 (char 10)", "tb": ' \ + assert ( + result == '{"msg": "Expecting value: line 1 column 11 (char 10)", "tb": ' '["hopeit.app.errors.BadRequest: Expecting value: line 1 column 11 (char 10)\\n"]}' + ) async def call_post_fail_request(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-event-test', - params={'query_arg1': 'fail'}, + "/api/mock-app/test/mock-event-test", + params={"query_arg1": "fail"}, data='{"value": "ok"}', - headers={ - 'X-Track-Session-Id': 'test_session_id' - } + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 500 result = (await res.read()).decode() - assert result == '{"msg": "Test for error", "tb": ["AssertionError: Test for error\\n"]}' + assert ( + result + == '{"msg": "Test for error", "tb": ["AssertionError: Test for error\\n"]}' + ) async def call_post_mock_collector(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-collector', + "/api/mock-app/test/mock-collector", params={}, data='{"value": "ok"}', headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() - assert result == '{"value": ' \ - '"((ok+mock_collector+step1)&(ok+mock_collector+step2)+mock_collector+step3)", ' \ + assert ( + result == '{"value": ' + '"((ok+mock_collector+step1)&(ok+mock_collector+step2)+mock_collector+step3)", ' '"processed": true}' + ) async def call_get_mock_spawn_event(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-spawn-event', - params={'payload': 'ok'}, + "/api/mock-app/test/mock-spawn-event", + params={"payload": "ok"}, data=None, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"value": "stream: ok.2"}' async def call_get_mock_timeout_ok(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-timeout', - params={'delay': 1}, + "/api/mock-app/test/mock-timeout", + params={"delay": 1}, data=None, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') == 'test_request_id' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") == "test_request_id" result = (await res.read()).decode() assert result == '{"mock_timeout": "ok"}' async def call_get_mock_timeout_exceeded(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-timeout', - params={'delay': 5}, + "/api/mock-app/test/mock-timeout", + params={"delay": 5}, data=None, headers={ - 'X-Track-Request-Id': 'test_request_id', - 'X-Track-Session-Id': 'test_session_id' - } + "X-Track-Request-Id": "test_request_id", + "X-Track-Session-Id": "test_session_id", + }, ) assert res.status == 500 result = (await res.read()).decode() @@ -350,175 +371,179 @@ async def call_get_mock_timeout_exceeded(client): async def call_get_file_response(client): - file_name = str(uuid.uuid4()) + '.txt' + file_name = str(uuid.uuid4()) + ".txt" res: ClientResponse = await client.get( - '/api/mock-app/test/mock-file-response', - params={'file_name': file_name}, - headers={'X-Track-Session-Id': 'test_session_id'} + "/api/mock-app/test/mock-file-response", + params={"file_name": file_name}, + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' - assert res.headers.get("Content-Type") == 'text/plain' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert ( + res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' + ) + assert res.headers.get("Content-Type") == "text/plain" result = (await res.read()).decode() - assert result == 'mock_file_response test file_response' + assert result == "mock_file_response test file_response" async def call_get_stream_response(client): - file_name = str(uuid.uuid4()) + '.txt' + file_name = str(uuid.uuid4()) + ".txt" res: ClientResponse = await client.get( - '/api/mock-app/test/mock-stream-response', - params={'file_name': file_name}, - headers={'X-Track-Session-Id': 'test_session_id'} + "/api/mock-app/test/mock-stream-response", + params={"file_name": file_name}, + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' - assert res.headers.get("Content-Type") == 'application/octet-stream' - assert res.headers.get("Content-length") == '48' - result = (await res.read()) - assert result == b'TestDataTestDataTestDataTestDataTestDataTestData' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert ( + res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' + ) + assert res.headers.get("Content-Type") == "application/octet-stream" + assert res.headers.get("Content-length") == "48" + result = await res.read() + assert result == b"TestDataTestDataTestDataTestDataTestDataTestData" async def call_get_file_response_content_type(client): file_name = "binary.png" res: ClientResponse = await client.get( - '/api/mock-app/test/mock-file-response_content_type', - params={'file_name': file_name}, - headers={'X-Track-Session-Id': 'test_session_id'} + "/api/mock-app/test/mock-file-response_content_type", + params={"file_name": file_name}, + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' - assert res.headers.get("Content-Type") == 'image/png' + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert ( + res.headers.get("Content-Disposition") == f'attachment; filename="{file_name}"' + ) + assert res.headers.get("Content-Type") == "image/png" result = (await res.read()).decode() assert result == file_name async def call_get_mock_auth_event(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-auth', + "/api/mock-app/test/mock-auth", headers={ - 'X-Track-Session-Id': 'test_session_id', - 'Authorization': 'Basic 1234567890==' - } + "X-Track-Session-Id": "test_session_id", + "Authorization": "Basic 1234567890==", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") result = (await res.read()).decode() assert result == '{"mock_auth": "ok"}' async def call_get_mock_auth_event_malformed_authorization(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-auth', + "/api/mock-app/test/mock-auth", headers={ - 'X-Track-Session-Id': 'test_session_id', - 'Authorization': 'BAD_AUTHORIZATION' - } + "X-Track-Session-Id": "test_session_id", + "Authorization": "BAD_AUTHORIZATION", + }, ) assert res.status == 400 result = (await res.read()).decode() assert json.loads(result) == { "msg": "Malformed Authorization", - "tb": ["hopeit.app.errors.BadRequest: Malformed Authorization\n"] + "tb": ["hopeit.app.errors.BadRequest: Malformed Authorization\n"], } async def call_get_mock_auth_event_unauthorized(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-auth', - headers={ - 'X-Track-Session-Id': 'test_session_id' - } + "/api/mock-app/test/mock-auth", + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 401 result = (await res.read()).decode() - assert result == '{"msg": "Unsecured", "tb": ["hopeit.app.errors.Unauthorized: Unsecured\\n"]}' + assert ( + result + == '{"msg": "Unsecured", "tb": ["hopeit.app.errors.Unauthorized: Unsecured\\n"]}' + ) async def call_post_mock_auth_event(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-post-auth', + "/api/mock-app/test/mock-post-auth", data='{"value": "test_data"}', headers={ - 'X-Track-Session-Id': 'test_session_id', - 'Authorization': 'Basic 1234567890==' - } + "X-Track-Session-Id": "test_session_id", + "Authorization": "Basic 1234567890==", + }, ) assert res.status == 200 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") result = (await res.read()).decode() assert result == '{"mock_post_auth": "ok: test_data"}' async def call_post_mock_auth_event_unauthorized(client): res: ClientResponse = await client.post( - '/api/mock-app/test/mock-post-auth', + "/api/mock-app/test/mock-post-auth", data='{"value": "test_data"}', - headers={ - 'X-Track-Session-Id': 'test_session_id' - } + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 401 result = (await res.read()).decode() - assert result == '{"msg": "Unsecured", "tb": ["hopeit.app.errors.Unauthorized: Unsecured\\n"]}' + assert ( + result + == '{"msg": "Unsecured", "tb": ["hopeit.app.errors.Unauthorized: Unsecured\\n"]}' + ) async def call_get_plugin_event_on_app(client): res: ClientResponse = await client.get( - '/api/mock-app/test/mock-plugin/test/plugin-event', - headers={'X-Track-Session-Id': 'test_session_id'} + "/api/mock-app/test/mock-plugin/test/plugin-event", + headers={"X-Track-Session-Id": "test_session_id"}, ) assert res.status == 999 - assert res.headers.get('X-Track-Session-Id') == 'test_session_id' - assert res.headers.get('X-Track-Request-Id') - assert res.headers.get('X-Track-Request-Ts') - assert res.headers.get('PluginHeader') == 'PluginHeaderValue' - assert res.cookies.get('PluginCookie').value == "PluginCookieValue" + assert res.headers.get("X-Track-Session-Id") == "test_session_id" + assert res.headers.get("X-Track-Request-Id") + assert res.headers.get("X-Track-Request-Ts") + assert res.headers.get("PluginHeader") == "PluginHeaderValue" + assert res.cookies.get("PluginCookie").value == "PluginCookieValue" result = (await res.read()).decode() assert result == '{"plugin_event": "PluginEvent.postprocess"}' async def check_setup_event_on_start(client): assert mock_event_setup.initialized - + + +async def check_plugin_setup_event_on_start(client): + assert plugin_setup.initialized + async def call_start_stream(client): - res = await client.get( - '/mgmt/mock-app/test/mock-stream-event/start' - ) + res = await client.get("/mgmt/mock-app/test/mock-stream-event/start") assert res.status == 200 async def call_start_service(client): - res = await client.get( - '/mgmt/mock-app/test/mock-service-event/start' - ) + res = await client.get("/mgmt/mock-app/test/mock-service-event/start") assert res.status == 200 async def call_stop_stream(client): - res = await client.get( - '/mgmt/mock-app/test/mock-stream-event/stop' - ) + res = await client.get("/mgmt/mock-app/test/mock-stream-event/stop") assert res.status == 200 async def call_stop_service(client): - res = await client.get( - '/mgmt/mock-app/test/mock-service-event/stop' - ) + res = await client.get("/mgmt/mock-app/test/mock-service-event/stop") assert res.status == 200 @@ -528,31 +553,39 @@ async def stop_test_server(): web.web_server = Application() -async def start_test_server(mock_app_config, mock_plugin_config, # noqa: F811 - streams: bool, enabled_groups: List[str]): +async def start_test_server( + mock_app_config, # noqa: F811 + mock_plugin_config, # noqa: F811 + streams: bool, + enabled_groups: List[str], +): await server_startup_hook(mock_app_config.server) await app_startup_hook(mock_plugin_config, enabled_groups) await app_startup_hook(mock_app_config, enabled_groups) if streams: await stream_startup_hook(mock_app_config, enabled_groups) - print('Test engine started.', web.web_server) + print("Test engine started.", web.web_server) await asyncio.sleep(5) -async def _setup(monkeypatch, - mock_app_config, # noqa: F811 - mock_plugin_config, # noqa: F811 - aiohttp_client, # noqa: F811 - streams: bool, - enabled_groups: List[str]): +async def _setup( + monkeypatch, + mock_app_config, # noqa: F811 + mock_plugin_config, # noqa: F811 + aiohttp_client, # noqa: F811 + streams: bool, + enabled_groups: List[str], +): stream_event = MockResult("ok: ok") - monkeypatch.setattr(MockStreamManager, 'test_payload', stream_event) - monkeypatch.setattr(MockEventHandler, 'test_track_ids', None) + monkeypatch.setattr(MockStreamManager, "test_payload", stream_event) + monkeypatch.setattr(MockEventHandler, "test_track_ids", None) api.clear() if enabled_groups is None: enabled_groups = [] - await start_test_server(mock_app_config, mock_plugin_config, streams, enabled_groups) + await start_test_server( + mock_app_config, mock_plugin_config, streams, enabled_groups + ) return await aiohttp_client(web.web_server) @@ -571,12 +604,15 @@ def loop(): @pytest.mark.order(1) @pytest.mark.asyncio -async def test_endpoints(monkeypatch, - mock_app_config, # noqa: F811 - mock_plugin_config, # noqa: F811 - aiohttp_client): # noqa: F811 - test_client = await _setup(monkeypatch, mock_app_config, mock_plugin_config, - aiohttp_client, False, []) +async def test_endpoints( + monkeypatch, + mock_app_config, # noqa: F811 + mock_plugin_config, # noqa: F811 + aiohttp_client, +): # noqa: F811 + test_client = await _setup( + monkeypatch, mock_app_config, mock_plugin_config, aiohttp_client, False, [] + ) logger = logging.getLogger() logger.setLevel(logging.DEBUG) @@ -585,6 +621,7 @@ async def test_endpoints(monkeypatch, test_list = [ check_setup_event_on_start, + check_plugin_setup_event_on_start, call_get_mock_event, call_get_mock_event_cors_allowed, call_get_mock_event_cors_unknown, @@ -627,12 +664,15 @@ async def test_endpoints(monkeypatch, @pytest.mark.order(2) @pytest.mark.asyncio -async def test_start_streams_on_startup(monkeypatch, - mock_app_config, # noqa: F811 - mock_plugin_config, # noqa: F811 - aiohttp_client): # noqa: F811 - test_client = await _setup(monkeypatch, mock_app_config, mock_plugin_config, - aiohttp_client, True, []) +async def test_start_streams_on_startup( + monkeypatch, + mock_app_config, # noqa: F811 + mock_plugin_config, # noqa: F811 + aiohttp_client, +): # noqa: F811 + test_client = await _setup( + monkeypatch, mock_app_config, mock_plugin_config, aiohttp_client, True, [] + ) await call_stop_stream(test_client) await call_stop_service(test_client) await stop_test_server() diff --git a/engine/test/mock_plugin/__init__.py b/engine/test/mock_plugin/__init__.py index cffd8c20..4b41218c 100644 --- a/engine/test/mock_plugin/__init__.py +++ b/engine/test/mock_plugin/__init__.py @@ -1,27 +1,34 @@ import pytest # type: ignore -from hopeit.app.config import AppConfig, AppDescriptor, \ - EventDescriptor, EventType, EventPlugMode +from hopeit.app.config import ( + AppConfig, + AppDescriptor, + EventDescriptor, + EventType, + EventPlugMode, +) from hopeit.server.config import ServerConfig, LoggingConfig @pytest.fixture def mock_plugin_config(): return AppConfig( - app=AppDescriptor(name='mock_plugin', version='test'), + app=AppDescriptor(name="mock_plugin", version="test"), env={ - 'plugin': { - 'plugin_value': 'test_plugin_value', - 'custom_value': 'test_custom_value' + "plugin": { + "plugin_value": "test_plugin_value", + "custom_value": "test_custom_value", } }, events={ - 'plugin_event': EventDescriptor( - type=EventType.GET, - plug_mode=EventPlugMode.ON_APP - ) + "plugin_setup": EventDescriptor( + type=EventType.SETUP, plug_mode=EventPlugMode.ON_APP + ), + "plugin_event": EventDescriptor( + type=EventType.GET, plug_mode=EventPlugMode.ON_APP + ), }, server=ServerConfig( logging=LoggingConfig(log_level="DEBUG", log_path="work/logs/test/") - ) + ), ).setup() diff --git a/engine/test/mock_plugin/plugin_setup.py b/engine/test/mock_plugin/plugin_setup.py new file mode 100644 index 00000000..af0b367e --- /dev/null +++ b/engine/test/mock_plugin/plugin_setup.py @@ -0,0 +1,19 @@ +""" +test setup event plugin mode +""" + +from hopeit.app.context import EventContext +from hopeit.app.logger import app_extra_logger + + +__steps__ = ["setup"] + +initialized = False + +logger, extra = app_extra_logger() + + +async def setup(payload: None, context: EventContext): + global initialized + initialized = True + logger.info(context, "Setup done") diff --git a/engine/test/unit/server/test_engine.py b/engine/test/unit/server/test_engine.py index 29d61c73..6aec0406 100644 --- a/engine/test/unit/server/test_engine.py +++ b/engine/test/unit/server/test_engine.py @@ -757,7 +757,7 @@ async def test_start_single_group(monkeypatch, mock_app_config, mock_plugin_conf plugin=mock_plugin_config, enabled_groups=['GROUP_A'] ) - assert len(engine.effective_events) == 26 + assert len(engine.effective_events) == 27 assert all( event_name in engine.effective_events for event_name in ['mock_event', 'mock_post_event', 'mock_event_logging', 'mock_stream_event'] @@ -777,7 +777,7 @@ async def test_start_multiple_groups(monkeypatch, mock_app_config, mock_plugin_c plugin=mock_plugin_config, enabled_groups=['GROUP_A', 'GROUP_B'] ) - assert len(engine.effective_events) == 28 + assert len(engine.effective_events) == 29 assert all( event_name in engine.effective_events for event_name in [ @@ -800,8 +800,8 @@ async def test_start_default_group(monkeypatch, mock_app_config, mock_plugin_con plugin=mock_plugin_config, enabled_groups=['DEFAULT'] ) - # Checking count it should be 19 events + 2 split events == 21 - assert len(engine.effective_events) == 22 + # Checking count it should be 21 events + 2 split events == 23 + assert len(engine.effective_events) == 23 assert all( event_name not in engine.effective_events for event_name in ['mock_event', 'mock_post_event', 'mock_event_logging', 'mock_stream_event'] diff --git a/plugins/ops/apps-visualizer/api/openapi.json b/plugins/ops/apps-visualizer/api/openapi.json index ebeaf5f2..97b7dde8 100644 --- a/plugins/ops/apps-visualizer/api/openapi.json +++ b/plugins/ops/apps-visualizer/api/openapi.json @@ -1,12 +1,12 @@ { "openapi": "3.0.3", "info": { - "version": "0.21", + "version": "0.22", "title": "Simple Example", "description": "Simple Example" }, "paths": { - "/api/config-manager/0x21/runtime-apps-config": { + "/api/config-manager/0x22/runtime-apps-config": { "get": { "summary": "Config Manager: Runtime Apps Config", "description": "Returns the runtime config for the Apps running on this server", @@ -62,11 +62,11 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, - "/api/config-manager/0x21/cluster-apps-config": { + "/api/config-manager/0x22/cluster-apps-config": { "get": { "summary": "Config Manager: Cluster Apps Config", "description": "Handle remote access to runtime configuration for a group of hosts", @@ -122,7 +122,7 @@ } }, "tags": [ - "config_manager.0x21" + "config_manager.0x22" ] } }, @@ -209,11 +209,11 @@ } }, "tags": [ - "apps_visualizer.0x21" + "apps_visualizer.0x22" ] } }, - "/api/apps-visualizer/0x21/apps/events-graph": { + "/api/apps-visualizer/0x22/apps/events-graph": { "get": { "summary": "App Visualizer: Events Graph Data", "description": "App Visualizer: Events Graph Data", @@ -287,11 +287,11 @@ } }, "tags": [ - "apps_visualizer.0x21" + "apps_visualizer.0x22" ] } }, - "/api/apps-visualizer/0x21/event-stats/live": { + "/api/apps-visualizer/0x22/event-stats/live": { "get": { "summary": "App Visualizer: Live Stats", "description": "App Visualizer: Live Stats", @@ -365,7 +365,7 @@ } }, "tags": [ - "apps_visualizer.0x21" + "apps_visualizer.0x22" ] } } @@ -834,7 +834,7 @@ }, "engine_version": { "type": "string", - "default": "0.21.3" + "default": "0.22.0" } }, "x-module-name": "hopeit.server.config", From 3c3b2ad8c443a21721f2bc3c75dc3618873e1d55 Mon Sep 17 00:00:00 2001 From: Pablo Canto Date: Fri, 23 Feb 2024 09:57:53 -0300 Subject: [PATCH 7/7] fix example --- .../simple-example/src/simple_example/setup_something.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/examples/simple-example/src/simple_example/setup_something.py b/apps/examples/simple-example/src/simple_example/setup_something.py index 20636c0d..345b53a2 100644 --- a/apps/examples/simple-example/src/simple_example/setup_something.py +++ b/apps/examples/simple-example/src/simple_example/setup_something.py @@ -20,5 +20,5 @@ async def run_once(payload: None, context: EventContext): """ This method initializes the environment. """ - logger.info(context, "Setup done") Path("/tmp/hopeit.initialized").touch() + logger.info(context, "Setup done")