diff --git a/README.md b/README.md index 753e0fd..1381a1b 100644 --- a/README.md +++ b/README.md @@ -403,3 +403,57 @@ Sample configuration in Gundi: """ ``` Notice: This can also be combined with Dynamic Schema and JSON Transformations. In that case the hex string will be parsed first, adn then the JQ filter can be applied to the extracted data. + +### Custom UI for configurations (ui schema) +It's possible to customize how the forms for configurations are displayed in the Gundi portal. +To do that, use `FieldWithUIOptions` in your models. The `UIOptions` and `GlobalUISchemaOptions` will allow you to customize the appearance of the fields in the portal by setting any of the ["ui schema"](https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema) supported options. + +```python +# Example +import pydantic +from app.services.utils import FieldWithUIOptions, GlobalUISchemaOptions, UIOptions +from .core import AuthActionConfiguration, PullActionConfiguration + + +class AuthenticateConfig(AuthActionConfiguration): + email: str # This will be rendered with default widget and settings + password: pydantic.SecretStr = FieldWithUIOptions( + ..., + format="password", + title="Password", + description="Password for the Global Forest Watch account.", + ui_options=UIOptions( + widget="password", # This will be rendered as a password input hiding the input + ) + ) + ui_global_options = GlobalUISchemaOptions( + order=["email", "password"], # This will set the order of the fields in the form + ) + + +class MyPullActionConfiguration(PullActionConfiguration): + lookback_days: int = FieldWithUIOptions( + 10, + le=30, + ge=1, + title="Data lookback days", + description="Number of days to look back for data.", + ui_options=UIOptions( + widget="range", # This will be rendered ad a range slider + ) + ) + force_fetch: bool = FieldWithUIOptions( + False, + title="Force fetch", + description="Force fetch even if in a quiet period.", + ui_options=UIOptions( + widget="radio", # This will be rendered as a radio button + ) + ) + ui_global_options = GlobalUISchemaOptions( + order=[ + "lookback_days", + "force_fetch", + ], + ) +``` diff --git a/app/actions/core.py b/app/actions/core.py index f646b4d..a7dbe09 100644 --- a/app/actions/core.py +++ b/app/actions/core.py @@ -1,9 +1,12 @@ import importlib import inspect +from typing import Optional + from pydantic import BaseModel +from app.services.utils import UISchemaModelMixin -class ActionConfiguration(BaseModel): +class ActionConfiguration(UISchemaModelMixin, BaseModel): pass diff --git a/app/conftest.py b/app/conftest.py index 8f62237..ae20973 100644 --- a/app/conftest.py +++ b/app/conftest.py @@ -1,15 +1,13 @@ import asyncio import datetime import json - import pydantic import pytest from unittest.mock import MagicMock from app import settings from gcloud.aio import pubsub -from gundi_core.schemas.v2 import Integration, IntegrationActionConfiguration, IntegrationActionSummary +from gundi_core.schemas.v2 import Integration from gundi_core.events import ( - SystemEventBaseModel, IntegrationActionCustomLog, CustomActivityLog, IntegrationActionStarted, @@ -28,9 +26,9 @@ CustomWebhookLog, LogLevel ) - from app.actions import PullActionConfiguration -from app.webhooks import GenericJsonTransformConfig, GenericJsonPayload, WebhookPayload +from app.services.utils import GlobalUISchemaOptions, FieldWithUIOptions, UIOptions +from app.webhooks import GenericJsonTransformConfig, GenericJsonPayload, WebhookPayload, WebhookConfiguration class AsyncMock(MagicMock): @@ -107,9 +105,9 @@ def integration_v2(): 'value': 'auth'}, 'data': {'token': 'testtoken2a97022f21732461ee103a08fac8a35'}}], 'additional': {}, 'default_route': {'id': '5abf3845-7c9f-478a-bc0f-b24d87038c4b', 'name': 'Gundi X Provider - Default Route'}, - 'status': {'id': 'mockid-b16a-4dbd-ad32-197c58aeef59', 'is_healthy': True, - 'details': 'Last observation has been delivered with success.', - 'observation_delivered_24hrs': 50231, 'last_observation_delivered_at': '2023-03-31T11:20:00+0200'}} + 'status': 'healthy', + 'status_details': '', + } ) @@ -139,6 +137,14 @@ def integration_v2_with_webhook(): "allowed_devices_list": {"title": "Allowed Devices List", "type": "array", "items": {}}, "deduplication_enabled": {"title": "Deduplication Enabled", "type": "boolean"}}, "required": ["allowed_devices_list", "deduplication_enabled"] + }, + "ui_schema": { + "allowed_devices_list": { + "ui:widget": "select" + }, + "deduplication_enabled": { + "ui:widget": "radio" + } } } }, @@ -163,13 +169,8 @@ def integration_v2_with_webhook(): }, "additional": {}, "default_route": None, - "status": { - "id": "mockid-b16a-4dbd-ad32-197c58aeef59", - "is_healthy": True, - "details": "Last observation has been delivered with success.", - "observation_delivered_24hrs": 50231, - "last_observation_delivered_at": "2023-03-31T11:20:00+0200" - } + "status": "healthy", + "status_details": "", } ) @@ -218,6 +219,17 @@ def integration_v2_with_webhook_generic(): "description": "Output type for the transformed data: 'obv' or 'event'" } } + }, + "ui_schema": { + "jq_filter": { + "ui:widget": "textarea" + }, + "json_schema": { + "ui:widget": "textarea" + }, + "output_type": { + "ui:widget": "text" + } } } }, @@ -455,13 +467,8 @@ def integration_v2_with_webhook_generic(): }, "additional": {}, "default_route": None, - "status": { - "id": "mockid-b16a-4dbd-ad32-197c58aeef59", - "is_healthy": True, - "details": "Last observation has been delivered with success.", - "observation_delivered_24hrs": 50231, - "last_observation_delivered_at": "2023-03-31T11:20:00+0200" - } + "status": "healthy", + "status_details": "", } ) @@ -898,7 +905,30 @@ def mock_publish_event(gcp_pubsub_publish_response): class MockPullActionConfiguration(PullActionConfiguration): - lookback_days: int = 10 + lookback_days: int = FieldWithUIOptions( + 30, + le=30, + ge=1, + title="Data lookback days", + description="Number of days to look back for data.", + ui_options=UIOptions( + widget="range", + ) + ) + force_fetch: bool = FieldWithUIOptions( + False, + title="Force fetch", + description="Force fetch even if in a quiet period.", + ui_options=UIOptions( + widget="select", + ) + ) + ui_global_options = GlobalUISchemaOptions( + order=[ + "lookback_days", + "force_fetch", + ], + ) @pytest.fixture @@ -1172,9 +1202,21 @@ class MockWebhookPayloadModel(WebhookPayload): lon: float -class MockWebhookConfigModel(pydantic.BaseModel): - allowed_devices_list: list - deduplication_enabled: bool +class MockWebhookConfigModel(WebhookConfiguration): + allowed_devices_list: list = FieldWithUIOptions( + ..., + title="Allowed Devices List", + ui_options=UIOptions( + widget="list", + ) + ) + deduplication_enabled: bool = FieldWithUIOptions( + ..., + title="Deduplication Enabled", + ui_options=UIOptions( + widget="radio", + ) + ) @pytest.fixture diff --git a/app/services/self_registration.py b/app/services/self_registration.py index 2ddce42..88e68a9 100644 --- a/app/services/self_registration.py +++ b/app/services/self_registration.py @@ -14,8 +14,6 @@ async def register_integration_in_gundi(gundi_client, type_slug=None, service_url=None): - #from ..webhooks.configurations import LiquidTechPayload - #print(GenericJsonTransformConfig.schema_json()) # Prepare the integration name and value integration_type_slug = type_slug or INTEGRATION_TYPE_SLUG if not integration_type_slug: @@ -38,6 +36,7 @@ async def register_integration_in_gundi(gundi_client, type_slug=None, service_ur _, config_model = handler action_name = action_id.replace("_", " ").title() action_schema = json.loads(config_model.schema_json()) + action_ui_schema = config_model.ui_schema() if issubclass(config_model, AuthActionConfiguration): action_type = ActionTypeEnum.AUTHENTICATION.value elif issubclass(config_model, PullActionConfiguration): @@ -53,6 +52,7 @@ async def register_integration_in_gundi(gundi_client, type_slug=None, service_ur "value": action_id, "description": f"{integration_type_name} {action_name} action", "schema": action_schema, + "ui_schema": action_ui_schema, "is_periodic_action": True if issubclass(config_model, PullActionConfiguration) else False, } ) @@ -70,6 +70,7 @@ async def register_integration_in_gundi(gundi_client, type_slug=None, service_ur "value": f"{integration_type_slug}_webhook", "description": f"Webhook Integration with {integration_type_name}", "schema": json.loads(config_model.schema_json()), + "ui_schema": config_model.ui_schema(), } logger.info(f"Registering '{integration_type_slug}' with actions: '{actions}'") diff --git a/app/services/tests/test_self_registration.py b/app/services/tests/test_self_registration.py index 2e18579..302d2a9 100644 --- a/app/services/tests/test_self_registration.py +++ b/app/services/tests/test_self_registration.py @@ -22,22 +22,40 @@ async def test_register_integration_with_slug_setting( "description": f"Default type for integrations with X Tracker", "actions": [ { - 'description': 'X Tracker Pull Observations action', - 'is_periodic_action': True, - 'name': 'Pull Observations', - 'schema': { - 'properties': { - 'lookback_days': { - 'default': 10, - 'title': 'Lookback Days', - 'type': 'integer' + "type": "pull", + "name": "Pull Observations", + "value": "pull_observations", + "description": "X Tracker Pull Observations action", + "schema": { + "title": "MockPullActionConfiguration", + "type": "object", + "properties": { + "lookback_days": { + "title": "Data lookback days", + "description": "Number of days to look back for data.", + "default": 30, + "minimum": 1, "maximum": 30, + "type": "integer" + }, + "force_fetch": { + "title": "Force fetch", + "description": "Force fetch even if in a quiet period.", + "default": False, + "type": "boolean" } }, - 'title': 'MockPullActionConfiguration', - 'type': 'object' + "definitions": {} }, - 'type': 'pull', - 'value': 'pull_observations' + "ui_schema": { + "lookback_days": { + "ui:widget": "range" + }, + "force_fetch": { + "ui:widget": "select" + }, + "ui:order": ["lookback_days", "force_fetch"], + }, + "is_periodic_action": True } ], "webhook": { @@ -58,11 +76,16 @@ async def test_register_integration_with_slug_setting( "type": "boolean" } }, + "definitions": {}, "required": [ "allowed_devices_list", "deduplication_enabled" ] - } + }, + "ui_schema": { + "allowed_devices_list": {"ui:widget": "list"}, + "deduplication_enabled": {"ui:widget": "radio"} + }, } } ) @@ -84,22 +107,41 @@ async def test_register_integration_with_slug_arg( "description": f"Default type for integrations with X Tracker", "actions": [ { - 'description': 'X Tracker Pull Observations action', - 'is_periodic_action': True, - 'name': 'Pull Observations', - 'schema': { - 'properties': { - 'lookback_days': { - 'default': 10, - 'title': 'Lookback Days', - 'type': 'integer' + "type": "pull", + "name": "Pull Observations", + "value": "pull_observations", + "description": "X Tracker Pull Observations action", + "schema": { + "title": "MockPullActionConfiguration", + "type": "object", + "properties": { + "lookback_days": { + "title": "Data lookback days", + "description": "Number of days to look back for data.", + "default": 30, + "minimum": 1, "maximum": 30, + "type": "integer" + }, + "force_fetch": { + "title": "Force fetch", + "description": "Force fetch even if in a quiet period.", + "default": False, + "type": "boolean" } }, - 'title': 'MockPullActionConfiguration', - 'type': 'object' + "definitions": {} }, - 'type': 'pull', - 'value': 'pull_observations' + "ui_schema": { + "lookback_days": { + "ui:widget": "range" + }, + "force_fetch": { + + "ui:widget": "select" + }, + "ui:order": ["lookback_days", "force_fetch"], + }, + "is_periodic_action": True } ], "webhook": { @@ -120,11 +162,16 @@ async def test_register_integration_with_slug_arg( "type": "boolean" } }, + "definitions": {}, "required": [ "allowed_devices_list", "deduplication_enabled" ] - } + }, + "ui_schema": { + "allowed_devices_list": {"ui:widget": "list"}, + "deduplication_enabled": {"ui:widget": "radio"} + }, } } ) @@ -151,22 +198,40 @@ async def test_register_integration_with_service_url_arg( 'service_url': service_url, "actions": [ { - 'description': 'X Tracker Pull Observations action', - 'is_periodic_action': True, - 'name': 'Pull Observations', - 'schema': { - 'properties': { - 'lookback_days': { - 'default': 10, - 'title': 'Lookback Days', - 'type': 'integer' + "type": "pull", + "name": "Pull Observations", + "value": "pull_observations", + "description": "X Tracker Pull Observations action", + "schema": { + "title": "MockPullActionConfiguration", + "type": "object", + "properties": { + "lookback_days": { + "title": "Data lookback days", + "description": "Number of days to look back for data.", + "default": 30, + "minimum": 1, "maximum": 30, + "type": "integer" + }, + "force_fetch": { + "title": "Force fetch", + "description": "Force fetch even if in a quiet period.", + "default": False, + "type": "boolean" } }, - 'title': 'MockPullActionConfiguration', - 'type': 'object' + "definitions": {} }, - 'type': 'pull', - 'value': 'pull_observations' + "ui_schema": { + "lookback_days": { + "ui:widget": "range" + }, + "force_fetch": { + "ui:widget": "select" + }, + "ui:order": ["lookback_days", "force_fetch"], + }, + "is_periodic_action": True } ], "webhook": { @@ -187,11 +252,16 @@ async def test_register_integration_with_service_url_arg( "type": "boolean" } }, + "definitions": {}, "required": [ "allowed_devices_list", "deduplication_enabled" ] - } + }, + "ui_schema": { + "allowed_devices_list": {"ui:widget": "list"}, + "deduplication_enabled": {"ui:widget": "radio"} + }, } } ) @@ -207,7 +277,7 @@ async def test_register_integration_with_service_url_setting( mocker.patch("app.services.self_registration.action_handlers", mock_action_handlers) mocker.patch("app.services.self_registration.get_webhook_handler", mock_get_webhook_handler_for_fixed_json_payload) - await register_integration_in_gundi(gundi_client=mock_gundi_client_v2,) + await register_integration_in_gundi(gundi_client=mock_gundi_client_v2, ) assert mock_gundi_client_v2.register_integration_type.called mock_gundi_client_v2.register_integration_type.assert_called_with( @@ -218,22 +288,40 @@ async def test_register_integration_with_service_url_setting( 'service_url': service_url, "actions": [ { - 'description': 'X Tracker Pull Observations action', - 'is_periodic_action': True, - 'name': 'Pull Observations', - 'schema': { - 'properties': { - 'lookback_days': { - 'default': 10, - 'title': 'Lookback Days', - 'type': 'integer' + "type": "pull", + "name": "Pull Observations", + "value": "pull_observations", + "description": "X Tracker Pull Observations action", + "schema": { + "title": "MockPullActionConfiguration", + "type": "object", + "properties": { + "lookback_days": { + "title": "Data lookback days", + "description": "Number of days to look back for data.", + "default": 30, + "minimum": 1, "maximum": 30, + "type": "integer" + }, + "force_fetch": { + "title": "Force fetch", + "description": "Force fetch even if in a quiet period.", + "default": False, + "type": "boolean" } }, - 'title': 'MockPullActionConfiguration', - 'type': 'object' + "definitions": {} }, - 'type': 'pull', - 'value': 'pull_observations' + "ui_schema": { + "lookback_days": { + "ui:widget": "range" + }, + "force_fetch": { + "ui:widget": "select" + }, + "ui:order": ["lookback_days", "force_fetch"], + }, + "is_periodic_action": True } ], "webhook": { @@ -254,11 +342,16 @@ async def test_register_integration_with_service_url_setting( "type": "boolean" } }, + "definitions": {}, "required": [ "allowed_devices_list", "deduplication_enabled" ] - } + }, + "ui_schema": { + "allowed_devices_list": {"ui:widget": "list"}, + "deduplication_enabled": {"ui:widget": "radio"} + }, } } ) diff --git a/app/services/utils.py b/app/services/utils.py index 673cd66..da4a1b5 100644 --- a/app/services/utils.py +++ b/app/services/utils.py @@ -1,9 +1,9 @@ import struct -from typing import Annotated, Union +from typing import Annotated import typing -from pydantic import create_model -from pydantic.fields import Field - +from pydantic import create_model, BaseModel +from pydantic.fields import Field, FieldInfo, Undefined, NoArgAnyCallable +from typing import Any, Dict, Optional, Union, List def find_config_for_action(configurations, action_id): @@ -167,3 +167,209 @@ def _make_field(self, factory, field, alias) -> None: ... ) + +class GlobalUISchemaOptions(BaseModel): + order: Optional[List[str]] + addable: Optional[bool] + copyable: Optional[bool] + orderable: Optional[bool] + removable: Optional[bool] + label: Optional[bool] + duplicateKeySuffixSeparator: Optional[str] + + +class UIOptions(GlobalUISchemaOptions): + classNames: Optional[str] + style: Optional[Dict[str, Any]] # Assuming style is a dictionary of CSS properties + title: Optional[str] + description: Optional[str] + placeholder: Optional[str] + help: Optional[str] + autofocus: Optional[bool] + autocomplete: Optional[str] # Type of HTMLInputElement['autocomplete'] + disabled: Optional[bool] + emptyValue: Optional[Any] + enumDisabled: Optional[Union[List[Union[str, int, bool]], None]] # List of disabled enum options + hideError: Optional[bool] + readonly: Optional[bool] + filePreview: Optional[bool] + inline: Optional[bool] + inputType: Optional[str] + rows: Optional[int] + submitButtonOptions: Optional[Dict[str, Any]] # Assuming UISchemaSubmitButtonOptions is a dict + widget: Optional[Union[str, Any]] # Either a widget implementation or its name + enumNames: Optional[List[str]] # List of labels for enum values + + +class FieldInfoWithUIOptions(FieldInfo): + + def __init__(self, *args, **kwargs): + """ + Extends the Pydantic Field class to support ui:schema generation + :param kwargs: ui_options: UIOptions + """ + self.ui_options = kwargs.pop("ui_options", None) + super().__init__(*args, **kwargs) + + def ui_schema(self, *args, **kwargs): + """Generates a UI schema from model field ui_options""" + if not self.ui_options: + return {} + ui_schema = {} + ui_options = self.ui_options.__fields__ + for field_name, model_field in ui_options.items(): + if value := getattr(self.ui_options, field_name, model_field.default): + ui_schema[f"ui:{field_name}"] = value + return ui_schema + + +def FieldWithUIOptions( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny', Any]] = None, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny', Any]] = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + allow_inf_nan: Optional[bool] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + unique_items: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + allow_mutation: bool = True, + regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, + ui_options: UIOptions = None, + **extra: Any, +) -> FieldInfoWithUIOptions: + """ + Used to provide extra information about a field, either for the model schema or complex validation. Some arguments + apply only to number fields (``int``, ``float``, ``Decimal``) and some apply only to ``str``. + + :param default: since this is replacing the field’s default, its first argument is used + to set the default, use ellipsis (``...``) to indicate the field is required + :param default_factory: callable that will be called when a default value is needed for this field + If both `default` and `default_factory` are set, an error is raised. + :param alias: the public name of the field + :param title: can be any string, used in the schema + :param description: can be any string, used in the schema + :param exclude: exclude this field while dumping. + Takes same values as the ``include`` and ``exclude`` arguments on the ``.dict`` method. + :param include: include this field while dumping. + Takes same values as the ``include`` and ``exclude`` arguments on the ``.dict`` method. + :param const: this field is required and *must* take it's default value + :param gt: only applies to numbers, requires the field to be "greater than". The schema + will have an ``exclusiveMinimum`` validation keyword + :param ge: only applies to numbers, requires the field to be "greater than or equal to". The + schema will have a ``minimum`` validation keyword + :param lt: only applies to numbers, requires the field to be "less than". The schema + will have an ``exclusiveMaximum`` validation keyword + :param le: only applies to numbers, requires the field to be "less than or equal to". The + schema will have a ``maximum`` validation keyword + :param multiple_of: only applies to numbers, requires the field to be "a multiple of". The + schema will have a ``multipleOf`` validation keyword + :param allow_inf_nan: only applies to numbers, allows the field to be NaN or infinity (+inf or -inf), + which is a valid Python float. Default True, set to False for compatibility with JSON. + :param max_digits: only applies to Decimals, requires the field to have a maximum number + of digits within the decimal. It does not include a zero before the decimal point or trailing decimal zeroes. + :param decimal_places: only applies to Decimals, requires the field to have at most a number of decimal places + allowed. It does not include trailing decimal zeroes. + :param min_items: only applies to lists, requires the field to have a minimum number of + elements. The schema will have a ``minItems`` validation keyword + :param max_items: only applies to lists, requires the field to have a maximum number of + elements. The schema will have a ``maxItems`` validation keyword + :param unique_items: only applies to lists, requires the field not to have duplicated + elements. The schema will have a ``uniqueItems`` validation keyword + :param min_length: only applies to strings, requires the field to have a minimum length. The + schema will have a ``minLength`` validation keyword + :param max_length: only applies to strings, requires the field to have a maximum length. The + schema will have a ``maxLength`` validation keyword + :param allow_mutation: a boolean which defaults to True. When False, the field raises a TypeError if the field is + assigned on an instance. The BaseModel Config must set validate_assignment to True + :param regex: only applies to strings, requires the field match against a regular expression + pattern string. The schema will have a ``pattern`` validation keyword + :param discriminator: only useful with a (discriminated a.k.a. tagged) `Union` of sub models with a common field. + The `discriminator` is the name of this common field to shorten validation and improve generated schema + :param repr: show this field in the representation + :param ui_options: UIOptions instance used to set ui properties for the ui schema + :param **extra: any additional keyword arguments will be added as is to the schema + """ + field_info = FieldInfoWithUIOptions( + default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + exclude=exclude, + include=include, + const=const, + gt=gt, + ge=ge, + lt=lt, + le=le, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + min_items=min_items, + max_items=max_items, + unique_items=unique_items, + min_length=min_length, + max_length=max_length, + allow_mutation=allow_mutation, + regex=regex, + discriminator=discriminator, + repr=repr, + ui_options=ui_options, + **extra, + ) + field_info._validate() + return field_info + + +class UISchemaModelMixin: + + @classmethod + def ui_schema(cls, *args, **kwargs): + """Generates a UI schema from model""" + ui_schema = {} + # Iterate through the fields and generate UI schema + for field_name, model_field in cls.__fields__.items(): + if getattr(model_field.field_info, "ui_options", None): + ui_schema[field_name] = model_field.field_info.ui_schema() + # Include global options + if global_options := cls.__fields__.get('ui_global_options'): + if getattr(global_options, "type_", None) == GlobalUISchemaOptions: + model = global_options.default + for field_name, model_field in model.__fields__.items(): + if value := getattr(model, field_name, model_field.default): + ui_schema[f"ui:{field_name}"] = value + return ui_schema + + + @classmethod + def schema(cls, **kwargs): + # Call the parent schema method to get the original schema + json_schema_dict = super().schema(**kwargs) + + # Remove ui schema fields from the properties and definitions + properties = json_schema_dict.get('properties', {}) + for field in ["ui_options", "ui_global_options"]: + properties.pop(field, None) + json_schema_dict['properties'] = properties + definitions = json_schema_dict.get('definitions', {}) + for field in ["UIOptions", "GlobalUISchemaOptions"]: + definitions.pop(field, None) + json_schema_dict['definitions'] = definitions + return json_schema_dict diff --git a/app/settings/base.py b/app/settings/base.py index 400ff90..440a737 100644 --- a/app/settings/base.py +++ b/app/settings/base.py @@ -66,5 +66,5 @@ REGISTER_ON_START = env.bool("REGISTER_ON_START", False) INTEGRATION_TYPE_SLUG = env.str("INTEGRATION_TYPE_SLUG", None) # Define a string id here e.g. "my_tracker" INTEGRATION_SERVICE_URL = env.str("INTEGRATION_SERVICE_URL", None) # Define a string id here e.g. "my_tracker" -PROCESS_PUBSUB_MESSAGES_IN_BACKGROUND = env.bool("PROCESS_PUBSUB_MESSAGES_IN_BACKGROUND", True) +PROCESS_PUBSUB_MESSAGES_IN_BACKGROUND = env.bool("PROCESS_PUBSUB_MESSAGES_IN_BACKGROUND", False) PROCESS_WEBHOOKS_IN_BACKGROUND = env.bool("PROCESS_WEBHOOKS_IN_BACKGROUND", True) diff --git a/app/webhooks/core.py b/app/webhooks/core.py index 855761c..da32b79 100644 --- a/app/webhooks/core.py +++ b/app/webhooks/core.py @@ -3,12 +3,11 @@ import json from typing import Optional, Union from pydantic import BaseModel -from pydantic.fields import Field from fastapi.encoders import jsonable_encoder -from app.services.utils import StructHexString +from app.services.utils import StructHexString, UISchemaModelMixin, FieldWithUIOptions, UIOptions -class WebhookConfiguration(BaseModel): +class WebhookConfiguration(UISchemaModelMixin, BaseModel): class Config: extra = "allow" @@ -19,19 +18,34 @@ class HexStringConfig(WebhookConfiguration): class DynamicSchemaConfig(WebhookConfiguration): - json_schema: dict + json_schema: dict = FieldWithUIOptions( + default={}, + description="JSON Schema to validate the data.", + ui_options=UIOptions( + widget="textarea", # ToDo: Use a better (custom) widget to render the JSON schema + ) + ) -class JQTransformConfig(BaseModel): - jq_filter: str = Field( +class JQTransformConfig(UISchemaModelMixin, BaseModel): + jq_filter: str = FieldWithUIOptions( default=".", description="JQ filter to transform JSON data.", - example=". | map(select(.isActive))" + example=". | map(select(.isActive))", + ui_options=UIOptions( + widget="textarea", # ToDo: Use a better (custom) widget to render the JQ filter + ) ) class GenericJsonTransformConfig(JQTransformConfig, DynamicSchemaConfig): - output_type: str = Field(..., description="Output type for the transformed data: 'obv' or 'event'") + output_type: str = FieldWithUIOptions( + ..., + description="Output type for the transformed data: 'obv' or 'event'", + ui_options=UIOptions( + widget="text", # ToDo: Use a select or a better widget to render the output type + ) + ) class GenericJsonTransformWithHexStrConfig(HexStringConfig, GenericJsonTransformConfig): diff --git a/requirements-base.in b/requirements-base.in index 9db68bd..7c4e316 100644 --- a/requirements-base.in +++ b/requirements-base.in @@ -4,10 +4,11 @@ environs~=9.5.0 pydantic~=1.10.15 fastapi~=0.103.2 uvicorn~=0.23.2 -gundi-core~=1.5.0 -gundi-client-v2~=2.3.2 +gundi-core~=1.7.1 +gundi-client-v2~=2.3.8 stamina~=23.2.0 redis~=5.0.1 gcloud-aio-pubsub~=6.0.0 click~=8.1.7 pyjq~=2.6.0 +python-json-logger~=2.0.7