diff --git a/packages/common-library/src/common_library/json_serialization.py b/packages/common-library/src/common_library/json_serialization.py index 66ae07b739c..418b8342bc0 100644 --- a/packages/common-library/src/common_library/json_serialization.py +++ b/packages/common-library/src/common_library/json_serialization.py @@ -1,6 +1,6 @@ -""" Helpers for json serialization - - built-in json-like API - - implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance +"""Helpers for json serialization +- built-in json-like API +- implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance """ import datetime @@ -118,6 +118,28 @@ def pydantic_encoder(obj: Any) -> Any: raise TypeError(msg) +def representation_encoder(obj: Any): + """ + A fallback encoder that uses `pydantic_encoder` to serialize objects. + If serialization fails, it falls back to using `str(obj)`. + + This is practical for representation purposes, such as logging or debugging. + + Example: + >>> from common_library.json_serialization import json_dumps, representation_encoder + >>> class CustomObject: + ... def __str__(self): + ... return "CustomObjectRepresentation" + >>> obj = CustomObject() + >>> json_dumps(obj, default=representation_encoder) + '"CustomObjectRepresentation"' + """ + try: + return pydantic_encoder(obj) + except TypeError: + return str(obj) + + def json_dumps( obj: Any, *, diff --git a/packages/common-library/tests/test_json_serialization.py b/packages/common-library/tests/test_json_serialization.py index ab2092b61ae..b1062902032 100644 --- a/packages/common-library/tests/test_json_serialization.py +++ b/packages/common-library/tests/test_json_serialization.py @@ -13,6 +13,7 @@ SeparatorTuple, json_dumps, json_loads, + representation_encoder, ) from faker import Faker from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, HttpUrl, TypeAdapter @@ -110,3 +111,25 @@ class M(BaseModel): http_url=faker.url(), ) json_dumps(obj) + + +def test_json_dumps_with_representation_encoder(): + class CustomObject: + def __str__(self): + return "CustomObjectRepresentation" + + class SomeModel(BaseModel): + x: int + + obj = { + "custom": CustomObject(), + "some": SomeModel(x=42), + } + + # Using representation_encoder as the default encoder + result = json_dumps(obj, default=representation_encoder, indent=1) + + assert ( + result + == '{\n "custom": "CustomObjectRepresentation",\n "some": {\n "x": 42\n }\n}' + ) diff --git a/packages/service-library/src/servicelib/logging_errors.py b/packages/service-library/src/servicelib/logging_errors.py index f3b19a5ea4f..2e6150acc39 100644 --- a/packages/service-library/src/servicelib/logging_errors.py +++ b/packages/service-library/src/servicelib/logging_errors.py @@ -1,9 +1,9 @@ import logging -from pprint import pformat from typing import Any, TypedDict from common_library.error_codes import ErrorCodeStr from common_library.errors_classes import OsparcErrorMixin +from common_library.json_serialization import json_dumps, representation_encoder from .logging_utils import LogExtra, get_log_record_extra @@ -27,14 +27,15 @@ def create_troubleshotting_log_message( error_context -- Additional context surrounding the exception, such as environment variables or function-specific data. This can be derived from exc.error_context() (relevant when using the OsparcErrorMixin) tip -- Helpful suggestions or possible solutions explaining why the error may have occurred and how it could potentially be resolved """ - debug_data = pformat( + debug_data = json_dumps( { "exception_type": f"{type(error)}", "exception_details": f"{error}", "error_code": error_code, - "context": pformat(error_context, indent=1), + "context": error_context, "tip": tip, }, + default=representation_encoder, indent=1, ) @@ -82,7 +83,7 @@ def create_troubleshotting_log_kwargs( error=error, error_code=error_code, error_context=context, - tip=tip, + tip=tip or getattr(error, "tip", None), ) return { diff --git a/services/web/server/src/simcore_service_webserver/users/_users_service.py b/services/web/server/src/simcore_service_webserver/users/_users_service.py index 20a882128bb..29361eb8f09 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_service.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_service.py @@ -362,7 +362,8 @@ async def get_my_profile( ) except GroupExtraPropertiesNotFoundError as err: raise MissingGroupExtraPropertiesForProductError( - user_id=user_id, product_name=product_name + user_id=user_id, + product_name=product_name, ) from err return my_profile, preferences diff --git a/services/web/server/src/simcore_service_webserver/users/exceptions.py b/services/web/server/src/simcore_service_webserver/users/exceptions.py index 9f1bb48ef0a..eb4b7503deb 100644 --- a/services/web/server/src/simcore_service_webserver/users/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/users/exceptions.py @@ -3,8 +3,7 @@ from ..errors import WebServerBaseError -class UsersBaseError(WebServerBaseError): - ... +class UsersBaseError(WebServerBaseError): ... class UserNotFoundError(UsersBaseError): @@ -64,3 +63,4 @@ class BillingDetailsNotFoundError(UsersBaseError): class MissingGroupExtraPropertiesForProductError(UsersBaseError): msg_template = "Missing group_extra_property for product_name={product_name}" + tip = "Add a new row in group_extra_property table and assign it to this product"