diff --git a/api/specs/web-server/_auth.py b/api/specs/web-server/_auth.py index 4c323869e04..d47fe0aae5e 100644 --- a/api/specs/web-server/_auth.py +++ b/api/specs/web-server/_auth.py @@ -6,7 +6,6 @@ from typing import Any -from _common import Error, Log from fastapi import APIRouter, status from models_library.api_schemas_webserver.auth import ( AccountRequestInfo, @@ -15,6 +14,7 @@ UnregisterCheck, ) from models_library.generics import Envelope +from models_library.rest_error import EnvelopedError, Log from pydantic import BaseModel, Field, confloat from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.login._2fa_handlers import Resend2faBody @@ -75,7 +75,7 @@ async def register(_body: RegisterBody): "/auth/unregister", response_model=Envelope[Log], status_code=status.HTTP_200_OK, - responses={status.HTTP_409_CONFLICT: {"model": Envelope[Error]}}, + responses={status.HTTP_409_CONFLICT: {"model": EnvelopedError}}, ) async def unregister_account(_body: UnregisterCheck): ... @@ -107,7 +107,7 @@ async def phone_confirmation(_body: PhoneConfirmationBody): responses={ # status.HTTP_503_SERVICE_UNAVAILABLE status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized reset due to invalid token code", } }, @@ -122,7 +122,7 @@ async def login(_body: LoginBody): operation_id="auth_login_2fa", responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized reset due to invalid token code", } }, @@ -137,7 +137,7 @@ async def login_2fa(_body: LoginTwoFactorAuthBody): operation_id="auth_resend_2fa_code", responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized reset due to invalid token code", } }, @@ -161,7 +161,7 @@ async def logout(_body: LogoutBody): status_code=status.HTTP_204_NO_CONTENT, responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized reset due to invalid token code", } }, @@ -174,7 +174,7 @@ async def check_auth(): "/auth/reset-password", response_model=Envelope[Log], operation_id="auth_reset_password", - responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"model": Envelope[Error]}}, + responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"model": EnvelopedError}}, ) async def reset_password(_body: ResetPasswordBody): """a non logged-in user requests a password reset""" @@ -186,7 +186,7 @@ async def reset_password(_body: ResetPasswordBody): operation_id="auth_reset_password_allowed", responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized reset due to invalid token code", } }, @@ -201,11 +201,11 @@ async def reset_password_allowed(code: str, _body: ResetPasswordConfirmation): operation_id="auth_change_email", responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized user. Login required", }, status.HTTP_503_SERVICE_UNAVAILABLE: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unable to send confirmation email", }, }, @@ -233,15 +233,15 @@ class PasswordCheckSchema(BaseModel): operation_id="auth_change_password", responses={ status.HTTP_401_UNAUTHORIZED: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "unauthorized user. Login required", }, status.HTTP_409_CONFLICT: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "mismatch between new and confirmation passwords", }, status.HTTP_422_UNPROCESSABLE_ENTITY: { - "model": Envelope[Error], + "model": EnvelopedError, "description": "current password is invalid", }, }, diff --git a/api/specs/web-server/_common.py b/api/specs/web-server/_common.py index 637f4101819..5afbea3d1d2 100644 --- a/api/specs/web-server/_common.py +++ b/api/specs/web-server/_common.py @@ -5,13 +5,22 @@ import sys from collections.abc import Callable from pathlib import Path -from typing import Annotated, NamedTuple, Optional, Union, get_args, get_origin +from typing import ( + Annotated, + Any, + Generic, + NamedTuple, + Optional, + TypeVar, + Union, + get_args, + get_origin, +) from common_library.json_serialization import json_dumps from common_library.pydantic_fields_extension import get_type from fastapi import Query -from models_library.basic_types import LogLevel -from pydantic import BaseModel, ConfigDict, Field, Json, create_model +from pydantic import BaseModel, Json, create_model from pydantic.fields import FieldInfo CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent @@ -78,43 +87,14 @@ def as_query(model_class: type[BaseModel]) -> type[BaseModel]: return create_model(new_model_name, **fields) -class Log(BaseModel): - level: LogLevel | None = Field("INFO", description="log level") - message: str = Field( - ..., - description="log message. If logger is USER, then it MUST be human readable", - ) - logger: str | None = Field( - None, description="name of the logger receiving this message" - ) - - model_config = ConfigDict( - json_schema_extra={ - "example": { - "message": "Hi there, Mr user", - "level": "INFO", - "logger": "user-logger", - } - } - ) - +ErrorT = TypeVar("ErrorT") -class ErrorItem(BaseModel): - code: str = Field( - ..., - description="Typically the name of the exception that produced it otherwise some known error code", - ) - message: str = Field(..., description="Error message specific to this item") - resource: str | None = Field( - None, description="API resource affected by this error" - ) - field: str | None = Field(None, description="Specific field within the resource") +class EnvelopeE(BaseModel, Generic[ErrorT]): + """Complementary to models_library.generics.Envelope just for the generators""" -class Error(BaseModel): - logs: list[Log] | None = Field(None, description="log messages") - errors: list[ErrorItem] | None = Field(None, description="errors metadata") - status: int | None = Field(None, description="HTTP error code") + error: ErrorT | None = None + data: Any | None = None class ParamSpec(NamedTuple): diff --git a/api/specs/web-server/_folders.py b/api/specs/web-server/_folders.py index f98b5e98308..88a2b19ce9e 100644 --- a/api/specs/web-server/_folders.py +++ b/api/specs/web-server/_folders.py @@ -17,7 +17,9 @@ FolderReplaceBodyParams, ) from models_library.generics import Envelope +from models_library.rest_error import EnvelopedError from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.folders._exceptions_handlers import _TO_HTTP_ERROR_MAP from simcore_service_webserver.folders._models import ( FolderSearchQueryParams, FoldersListQueryParams, @@ -29,6 +31,9 @@ tags=[ "folders", ], + responses={ + i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values() + }, ) diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index 958b8457bca..fce290fffb0 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -17,7 +17,9 @@ WorkspaceReplaceBodyParams, ) from models_library.generics import Envelope +from models_library.rest_error import EnvelopedError from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.folders._exceptions_handlers import _TO_HTTP_ERROR_MAP from simcore_service_webserver.workspaces._groups_api import WorkspaceGroupGet from simcore_service_webserver.workspaces._models import ( WorkspacesGroupsBodyParams, @@ -31,6 +33,9 @@ tags=[ "workspaces", ], + responses={ + i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values() + }, ) diff --git a/packages/models-library/src/models_library/rest_error.py b/packages/models-library/src/models_library/rest_error.py new file mode 100644 index 00000000000..3ba2475c9b3 --- /dev/null +++ b/packages/models-library/src/models_library/rest_error.py @@ -0,0 +1,121 @@ +from dataclasses import dataclass +from typing import Annotated + +from models_library.generics import Envelope +from pydantic import BaseModel, ConfigDict, Field + +from .basic_types import IDStr, LogLevel + + +class Log(BaseModel): + level: LogLevel | None = Field("INFO", description="log level") + message: str = Field( + ..., + description="log message. If logger is USER, then it MUST be human readable", + ) + logger: str | None = Field( + None, description="name of the logger receiving this message" + ) + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "message": "Hi there, Mr user", + "level": "INFO", + "logger": "user-logger", + } + } + ) + + +class ErrorItem(BaseModel): + code: str = Field( + ..., + description="Typically the name of the exception that produced it otherwise some known error code", + ) + message: str = Field(..., description="Error message specific to this item") + resource: str | None = Field( + None, description="API resource affected by this error" + ) + field: str | None = Field(None, description="Specific field within the resource") + + +@dataclass +class LogMessageType: + # NOTE: deprecated! + message: str + level: str = "INFO" + logger: str = "user" + + +@dataclass +class ErrorItemType: + # NOTE: deprecated! + code: str + message: str + resource: str | None + field: str | None + + @classmethod + def from_error(cls, err: BaseException): + return cls( + code=err.__class__.__name__, message=str(err), resource=None, field=None + ) + + +class ErrorGet(BaseModel): + message: Annotated[ + str, + Field( + min_length=5, + description="Message displayed to the user", + ), + ] + support_id: Annotated[ + IDStr | None, + Field(description="ID to track the incident during support", alias="supportId"), + ] = None + + # NOTE: The fields blow are DEPRECATED. Still here to keep compatibilty with front-end until updated + status: Annotated[int, Field(deprecated=True)] = 400 + errors: Annotated[ + list[ErrorItemType], + Field(deprecated=True, default_factory=list, json_schema_extra={"default": []}), + ] + logs: Annotated[ + list[LogMessageType], + Field(deprecated=True, default_factory=list, json_schema_extra={"default": []}), + ] + + model_config = ConfigDict( + populate_by_name=True, + extra="ignore", # Used to prune extra fields from internal data + frozen=True, + json_schema_extra={ + "examples": [ + { + "message": "Sorry you do not have sufficient access rights for product" + }, + { + "message": "Opps this error was unexpected. We are working on that!", + "supportId": "OEC:12346789", + }, + ] + }, + ) + + +class EnvelopedError(Envelope[None]): + error: ErrorGet + + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + {"error": {"message": "display error message here"}}, + { + "error": {"message": "failure", "supportId": "OEC:123455"}, + "data": None, + }, + ] + }, + ) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py index 3ce30f4131a..ba92081b9e4 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py @@ -82,13 +82,17 @@ def _do_assert_error( assert is_error(expected_status_code) - assert len(error["errors"]) >= 1 + # New versions of the error models might not have this attribute + details = error.get("errors", []) + if expected_msg: - messages = [detail["message"] for detail in error["errors"]] + assert details + messages = [e["message"] for e in details] assert expected_msg in messages if expected_error_code: - codes = [detail["code"] for detail in error["errors"]] + assert details + codes = [e["code"] for e in details] assert expected_error_code in codes return data, error diff --git a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py index d5f5a04283b..e1d1848406e 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py @@ -14,11 +14,11 @@ from aiohttp.web_response import StreamResponse from common_library.error_codes import create_error_code from common_library.json_serialization import json_dumps +from models_library.rest_error import ErrorGet, ErrorItemType, LogMessageType from ..logging_errors import create_troubleshotting_log_kwargs from ..mimetype_constants import MIMETYPE_APPLICATION_JSON from ..utils import is_production_environ -from .rest_models import ErrorItemType, ErrorType, LogMessageType from .rest_responses import ( create_data_response, create_http_error, @@ -98,7 +98,7 @@ async def _middleware_handler(request: web.Request, handler: Handler): err.content_type = MIMETYPE_APPLICATION_JSON if not err.text or not is_enveloped_from_text(err.text): - error = ErrorType( + error = ErrorGet( errors=[ ErrorItemType.from_error(err), ], diff --git a/packages/service-library/src/servicelib/aiohttp/rest_models.py b/packages/service-library/src/servicelib/aiohttp/rest_models.py deleted file mode 100644 index 36902f17b77..00000000000 --- a/packages/service-library/src/servicelib/aiohttp/rest_models.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass -class LogMessageType: - message: str - level: str = "INFO" - logger: str = "user" - - -@dataclass -class ErrorItemType: - code: str - message: str - resource: str | None - field: str | None - - @classmethod - def from_error(cls, err: BaseException): - return cls( - code=err.__class__.__name__, message=str(err), resource=None, field=None - ) - - -@dataclass -class ErrorType: - logs: list[LogMessageType] = field(default_factory=list) - errors: list[ErrorItemType] = field(default_factory=list) - status: int = 400 - message: str = "Unexpected error" diff --git a/packages/service-library/src/servicelib/aiohttp/rest_responses.py b/packages/service-library/src/servicelib/aiohttp/rest_responses.py index 1cce04ae015..d16f33b9e57 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_responses.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_responses.py @@ -5,17 +5,16 @@ import inspect import json from collections.abc import Mapping -from dataclasses import asdict from typing import Any from aiohttp import web, web_exceptions from aiohttp.web_exceptions import HTTPError, HTTPException from common_library.json_serialization import json_dumps +from models_library.rest_error import ErrorGet, ErrorItemType from servicelib.aiohttp.status import HTTP_200_OK from ..mimetype_constants import MIMETYPE_APPLICATION_JSON from ..status_codes_utils import get_code_description -from .rest_models import ErrorItemType, ErrorType _ENVELOPE_KEYS = ("data", "error") @@ -101,21 +100,23 @@ def create_http_error( default_message = reason or get_code_description(http_error_cls.status_code) if is_internal_error and skip_internal_error_details: - error = ErrorType( + error = ErrorGet( errors=[], + logs=[], status=http_error_cls.status_code, message=default_message, ) else: items = [ErrorItemType.from_error(err) for err in errors] - error = ErrorType( + error = ErrorGet( errors=items, + logs=[], status=http_error_cls.status_code, message=default_message, ) assert not http_error_cls.empty_body # nosec - payload = wrap_as_envelope(error=asdict(error)) + payload = wrap_as_envelope(error=error) return http_error_cls( reason=reason, diff --git a/packages/service-library/src/servicelib/aiohttp/rest_utils.py b/packages/service-library/src/servicelib/aiohttp/rest_utils.py index e026e211977..78fa58ac8ac 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_utils.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_utils.py @@ -1,5 +1,3 @@ -from dataclasses import asdict - from aiohttp import web from aiohttp.web import RouteDef, RouteTableDef from common_library.json_serialization import json_dumps @@ -13,11 +11,7 @@ class EnvelopeFactory: """ def __init__(self, data=None, error=None): - enveloped = {"data": data, "error": error} - for key, value in enveloped.items(): - if value is not None and not isinstance(value, dict): - enveloped[key] = asdict(value) - self._envelope = enveloped + self._envelope = {"data": data, "error": error} def as_dict(self) -> dict: return self._envelope diff --git a/packages/service-library/src/servicelib/logging_utils.py b/packages/service-library/src/servicelib/logging_utils.py index 9ffc6b32c5b..1ee9494a84b 100644 --- a/packages/service-library/src/servicelib/logging_utils.py +++ b/packages/service-library/src/servicelib/logging_utils.py @@ -51,6 +51,27 @@ } +class LogExtra(TypedDict): + log_uid: NotRequired[str] + log_oec: NotRequired[str] + + +def get_log_record_extra( + *, + user_id: int | str | None = None, + error_code: str | None = None, +) -> LogExtra | None: + extra: LogExtra = {} + + if user_id: + assert int(user_id) > 0 # nosec + extra["log_uid"] = f"{user_id}" + if error_code: + extra["log_oec"] = error_code + + return extra or None + + class CustomFormatter(logging.Formatter): """Custom Formatter does these 2 things: 1. Overrides 'funcName' with the value of 'func_name_override', if it exists. @@ -66,8 +87,10 @@ def format(self, record) -> str: record.funcName = record.func_name_override if hasattr(record, "file_name_override"): record.filename = record.file_name_override - if not hasattr(record, "log_uid"): - record.log_uid = None # Default value if user is not provided in the log + + for name in LogExtra.__optional_keys__: # pylint: disable=no-member + if not hasattr(record, name): + setattr(record, name, None) if self.log_format_local_dev_enabled: levelname = record.levelname @@ -80,11 +103,18 @@ def format(self, record) -> str: # SEE https://docs.python.org/3/library/logging.html#logrecord-attributes -DEFAULT_FORMATTING = "log_level=%(levelname)s | log_timestamp=%(asctime)s | log_source=%(name)s:%(funcName)s(%(lineno)d) | log_uid=%(log_uid)s | log_msg=%(message)s" +DEFAULT_FORMATTING = ( + "log_level=%(levelname)s " + "| log_timestamp=%(asctime)s " + "| log_source=%(name)s:%(funcName)s(%(lineno)d) " + "| log_uid=%(log_uid)s " + "| log_oec=%(log_oec)s" + "| log_msg=%(message)s" +) LOCAL_FORMATTING = "%(levelname)s: [%(asctime)s/%(processName)s] [%(name)s:%(funcName)s(%(lineno)d)] - %(message)s" # Graylog Grok pattern extractor: -# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{DATA:log_source} \| log_msg=%{GREEDYDATA:log_msg} +# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{DATA:log_source} \| (log_uid=%{WORD:log_uid} \| )?log_msg=%{GREEDYDATA:log_msg} def config_all_loggers( @@ -336,31 +366,10 @@ def log_catch(logger: logging.Logger, *, reraise: bool = True) -> Iterator[None] raise exc from exc -class LogExtra(TypedDict): - log_uid: NotRequired[str] - log_oec: NotRequired[str] - - LogLevelInt: TypeAlias = int LogMessageStr: TypeAlias = str -def get_log_record_extra( - *, - user_id: int | str | None = None, - error_code: str | None = None, -) -> LogExtra | None: - extra: LogExtra = {} - - if user_id: - assert int(user_id) > 0 # nosec - extra["log_uid"] = f"{user_id}" - if error_code: - extra["log_oec"] = error_code - - return extra or None - - def _un_capitalize(s: str) -> str: return s[:1].lower() + s[1:] if s else "" diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 688d2187cb3..33979c6bf3d 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -93,7 +93,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/verify-phone-number: post: tags: @@ -160,7 +160,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/validate-code-login: post: tags: @@ -186,7 +186,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/two_factor:resend: post: tags: @@ -212,7 +212,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/logout: post: tags: @@ -248,7 +248,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/reset-password: post: tags: @@ -274,7 +274,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/reset-password/{code}: post: tags: @@ -307,7 +307,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/change-password: post: tags: @@ -333,19 +333,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' '409': description: mismatch between new and confirmation passwords content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' '422': description: current password is invalid content: application/json: schema: - $ref: '#/components/schemas/Envelope_Error_' + $ref: '#/components/schemas/EnvelopedError' /v0/auth/confirmation/{code}: get: tags: @@ -2613,6 +2613,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_FolderGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable get: tags: - folders @@ -2679,6 +2703,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_FolderGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/folders:search: get: tags: @@ -2734,6 +2782,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_FolderGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/folders/{folder_id}: get: tags: @@ -2756,6 +2828,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_FolderGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable put: tags: - folders @@ -2783,6 +2879,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_FolderGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable delete: tags: - folders @@ -2800,6 +2920,30 @@ paths: responses: '204': description: Successful Response + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/tasks: get: tags: @@ -4396,7 +4540,7 @@ paths: '403': description: ProjectInvalidRightsError '404': - description: ProjectNotFoundError, UserDefaultWalletNotFoundError + description: UserDefaultWalletNotFoundError, ProjectNotFoundError '409': description: ProjectTooManyProjectOpenedError '422': @@ -5821,6 +5965,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_WorkspaceGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable get: tags: - workspaces @@ -5867,6 +6035,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_WorkspaceGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/workspaces/{workspace_id}: get: tags: @@ -5889,6 +6081,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_WorkspaceGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable put: tags: - workspaces @@ -5916,6 +6132,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_WorkspaceGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable delete: tags: - workspaces @@ -5933,6 +6173,30 @@ paths: responses: '204': description: Successful Response + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/workspaces/{workspace_id}/groups/{group_id}: post: tags: @@ -5970,6 +6234,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_WorkspaceGroupGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable put: tags: - workspaces @@ -6006,6 +6294,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_WorkspaceGroupGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable delete: tags: - workspaces @@ -6032,6 +6344,30 @@ paths: responses: '204': description: Successful Response + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/workspaces/{workspace_id}/groups: get: tags: @@ -6055,6 +6391,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_WorkspaceGroupGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/email:test: post: tags: @@ -7486,19 +7846,6 @@ components: title: Error type: object title: Envelope[ComputationTaskGet] - Envelope_Error_: - properties: - data: - anyOf: - - $ref: '#/components/schemas/Error' - - type: 'null' - error: - anyOf: - - {} - - type: 'null' - title: Error - type: object - title: Envelope[Error] Envelope_FileMetaDataGet_: properties: data: @@ -8897,60 +9244,80 @@ components: title: Error type: object title: Envelope[str] - Error: + EnvelopedError: properties: - logs: - anyOf: - - items: - $ref: '#/components/schemas/Log' - type: array - - type: 'null' - title: Logs - description: log messages - errors: + data: + type: 'null' + title: Data + error: + $ref: '#/components/schemas/ErrorGet' + type: object + required: + - error + title: EnvelopedError + ErrorGet: + properties: + message: + type: string + minLength: 5 + title: Message + description: Message displayed to the user + supportId: anyOf: - - items: - $ref: '#/components/schemas/ErrorItem' - type: array + - type: string + maxLength: 100 + minLength: 1 - type: 'null' - title: Errors - description: errors metadata + title: Supportid + description: ID to track the incident during support status: - anyOf: - - type: integer - - type: 'null' + type: integer title: Status - description: HTTP error code + default: 400 + deprecated: true + errors: + items: + $ref: '#/components/schemas/ErrorItemType' + type: array + title: Errors + default: [] + deprecated: true + logs: + items: + $ref: '#/components/schemas/LogMessageType' + type: array + title: Logs + default: [] + deprecated: true type: object - title: Error - ErrorItem: + required: + - message + title: ErrorGet + ErrorItemType: properties: code: type: string title: Code - description: Typically the name of the exception that produced it otherwise - some known error code message: type: string title: Message - description: Error message specific to this item resource: anyOf: - type: string - type: 'null' title: Resource - description: API resource affected by this error field: anyOf: - type: string - type: 'null' title: Field - description: Specific field within the resource type: object required: - code - message - title: ErrorItem + - resource + - field + title: ErrorItemType ExtractedResults: properties: progress: @@ -9944,6 +10311,23 @@ components: - WARNING - ERROR title: LogLevel + LogMessageType: + properties: + message: + type: string + title: Message + level: + type: string + title: Level + default: INFO + logger: + type: string + title: Logger + default: user + type: object + required: + - message + title: LogMessageType LoginBody: properties: email: diff --git a/services/web/server/src/simcore_service_webserver/login/utils.py b/services/web/server/src/simcore_service_webserver/login/utils.py index 07cc9a4154c..22d0a57cf3e 100644 --- a/services/web/server/src/simcore_service_webserver/login/utils.py +++ b/services/web/server/src/simcore_service_webserver/login/utils.py @@ -4,10 +4,10 @@ from aiohttp import web from common_library.json_serialization import json_dumps from models_library.products import ProductName +from models_library.rest_error import LogMessageType from models_library.users import UserID from pydantic import PositiveInt from servicelib.aiohttp import observer -from servicelib.aiohttp.rest_models import LogMessageType from servicelib.aiohttp.status import HTTP_200_OK from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from simcore_postgres_database.models.users import UserRole diff --git a/services/web/server/tests/unit/with_dbs/conftest.py b/services/web/server/tests/unit/with_dbs/conftest.py index c943be8c76c..eb24a2d1174 100644 --- a/services/web/server/tests/unit/with_dbs/conftest.py +++ b/services/web/server/tests/unit/with_dbs/conftest.py @@ -15,11 +15,11 @@ import random import sys import textwrap -from collections.abc import AsyncIterator, Awaitable, Callable, Iterator +from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Iterator from copy import deepcopy from decimal import Decimal from pathlib import Path -from typing import Any, AsyncIterable, Final +from typing import Any, Final from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -853,7 +853,7 @@ async def all_product_prices( result = {} for product_name in all_products_names: - usd_or_none = product_price.get(product_name, None) + usd_or_none = product_price.get(product_name) if usd_or_none is not None: await _pre_connection.execute( products_prices.insert().values(