From 5185d5ce90d8533483ba382e0a331f5374b9184e Mon Sep 17 00:00:00 2001 From: Harold Booth Date: Mon, 30 Sep 2024 17:14:20 -0400 Subject: [PATCH] feat: add centralized error handling Added centralized error handling facility, along with common base error class. Additionally, fixed a number of typing errors to get tests to pass. --- src/dioptra/restapi/errors.py | 406 +++++++++++++++--- src/dioptra/restapi/utils.py | 24 ++ src/dioptra/restapi/v1/artifacts/__init__.py | 3 - src/dioptra/restapi/v1/artifacts/service.py | 40 +- .../restapi/v1/entrypoints/__init__.py | 3 - src/dioptra/restapi/v1/entrypoints/errors.py | 77 ---- src/dioptra/restapi/v1/entrypoints/service.py | 124 +++--- .../restapi/v1/experiments/__init__.py | 3 - src/dioptra/restapi/v1/experiments/errors.py | 53 --- src/dioptra/restapi/v1/experiments/service.py | 89 ++-- src/dioptra/restapi/v1/groups/__init__.py | 3 - src/dioptra/restapi/v1/groups/errors.py | 41 -- src/dioptra/restapi/v1/groups/service.py | 39 +- src/dioptra/restapi/v1/jobs/__init__.py | 3 - src/dioptra/restapi/v1/jobs/errors.py | 105 ----- src/dioptra/restapi/v1/jobs/service.py | 58 +-- src/dioptra/restapi/v1/models/__init__.py | 3 - src/dioptra/restapi/v1/models/errors.py | 65 --- src/dioptra/restapi/v1/models/service.py | 81 ++-- .../v1/plugin_parameter_types/__init__.py | 3 - .../v1/plugin_parameter_types/errors.py | 95 ---- .../v1/plugin_parameter_types/service.py | 92 ++-- src/dioptra/restapi/v1/plugins/__init__.py | 4 - src/dioptra/restapi/v1/plugins/errors.py | 121 ------ src/dioptra/restapi/v1/plugins/service.py | 198 +++++---- src/dioptra/restapi/v1/queues/__init__.py | 3 +- src/dioptra/restapi/v1/queues/errors.py | 63 --- src/dioptra/restapi/v1/queues/service.py | 64 +-- .../restapi/v1/shared/drafts/service.py | 13 +- .../restapi/v1/shared/snapshots/service.py | 21 +- src/dioptra/restapi/v1/shared/tags/service.py | 19 +- src/dioptra/restapi/v1/tags/__init__.py | 3 - src/dioptra/restapi/v1/tags/errors.py | 49 --- src/dioptra/restapi/v1/tags/service.py | 63 +-- src/dioptra/restapi/v1/users/__init__.py | 3 - src/dioptra/restapi/v1/users/errors.py | 115 ----- src/dioptra/restapi/v1/users/service.py | 72 ++-- src/dioptra/restapi/v1/workflows/errors.py | 41 -- src/dioptra/restapi/v1/workflows/lib/views.py | 21 +- src/dioptra/restapi/v1/workflows/service.py | 3 +- tests/unit/restapi/lib/actions.py | 2 +- .../unit/restapi/test_utils.py | 71 +-- tests/unit/restapi/v1/test_artifact.py | 2 +- tests/unit/restapi/v1/test_entrypoint.py | 4 +- tests/unit/restapi/v1/test_model.py | 4 +- tests/unit/restapi/v1/test_plugin.py | 8 +- .../restapi/v1/test_plugin_parameter_type.py | 2 +- tests/unit/restapi/v1/test_queue.py | 4 +- tests/unit/restapi/v1/test_tag.py | 4 +- tests/unit/restapi/v1/test_user.py | 8 +- 50 files changed, 924 insertions(+), 1471 deletions(-) delete mode 100644 src/dioptra/restapi/v1/entrypoints/errors.py delete mode 100644 src/dioptra/restapi/v1/experiments/errors.py delete mode 100644 src/dioptra/restapi/v1/groups/errors.py delete mode 100644 src/dioptra/restapi/v1/jobs/errors.py delete mode 100644 src/dioptra/restapi/v1/models/errors.py delete mode 100644 src/dioptra/restapi/v1/plugin_parameter_types/errors.py delete mode 100644 src/dioptra/restapi/v1/plugins/errors.py delete mode 100644 src/dioptra/restapi/v1/queues/errors.py delete mode 100644 src/dioptra/restapi/v1/tags/errors.py delete mode 100644 src/dioptra/restapi/v1/users/errors.py delete mode 100644 src/dioptra/restapi/v1/workflows/errors.py rename src/dioptra/restapi/v1/artifacts/errors.py => tests/unit/restapi/test_utils.py (51%) diff --git a/src/dioptra/restapi/errors.py b/src/dioptra/restapi/errors.py index ba72c03a5..7dcbf2903 100644 --- a/src/dioptra/restapi/errors.py +++ b/src/dioptra/restapi/errors.py @@ -20,87 +20,391 @@ """ from __future__ import annotations +import typing +from io import StringIO + +import structlog +from flask import request from flask_restx import Api +from structlog.stdlib import BoundLogger + +LOGGER: BoundLogger = structlog.stdlib.get_logger() + +""" +Helper function to add attribute name/value pairs to StringIO instance as an error +message. +Args: + buffer: The StringIO instance into which the attribute name/value pairs are to be + added. + attributes: the list of attribute value pairs to add to the buffer. +""" + + +def add_attribute_values(buffer: StringIO, **kwargs: typing.Any) -> None: + index = 0 + length = len(kwargs) + for key, value in kwargs.items(): + if index != 0: + buffer.write(", ") + if index == length - 1: + buffer.write("and ") + buffer.write(f"{key} having value ({value})") + index += 1 + + +class DioptraError(Exception): + """ + Generic Dioptra Error. + Args: + message: An error specific message to display that provide context for why the + error was raised. + """ + + def __init__(self, message: str): + self.message: str = message + + def to_message(self) -> str: + if self.__cause__ is not None: + if isinstance(self.__cause__, DioptraError): + self.message = f"{self.message} Cause: {self.__cause__.to_message()}" + else: + self.message = f"{self.message} Cause: {self.__cause__}" + return self.message -class BackendDatabaseError(Exception): +class SubmissionError(DioptraError): + """The submission input is in error. + Args: + type: the resource type + action: the action + """ + + def __init__(self, type: str, action: str): + super().__init__(f"Input Error while attempting to {action} for {type}.") + self.resource_type = type + self.action = action + + +class EntityDoesNotExistError(DioptraError): + """ + The requested entity does not exist. + Args: + entity_type: the entity type name (e.g. "group" or "queue") + kwargs: the attribute value pairs used to request the entity + """ + + def __init__(self, entity_type: str, **kwargs: typing.Any): + buffer = StringIO() + buffer.write(f"Failed to locate {entity_type} with ") + add_attribute_values(buffer, **kwargs) + buffer.write(".") + super().__init__(buffer.getvalue()) + self.entity_type = entity_type + self.entity_attributes = kwargs + + +class EntityExistsError(DioptraError): + """ + The requested entity already exists. + Args: + entity_type: the entity type name (e.g. "group" or "queue") + existing_id: the id of the existing entity + kwargs: the attribute value pairs used to request the entity + """ + + def __init__(self, entity_type: str, existing_id: int, **kwargs: typing.Any): + buffer = StringIO() + buffer.write(f"The {entity_type} with ") + add_attribute_values(buffer, **kwargs) + buffer.write(" is not available.") + + super().__init__(buffer.getvalue()) + self.entity_type = entity_type + self.entity_attributes = kwargs + self.existing_id = existing_id + + +class LockError(DioptraError): + def __init__(self, message: str): + super().__init__(message) + + +class ReadOnlyLockError(LockError): + """The type has a read-only lock and cannot be modified.""" + + def __init__(self, type: str, **kwargs: typing.Any): + buffer = StringIO() + buffer.write(f"The {type} type with ") + add_attribute_values(buffer, **kwargs) + buffer.write(" has a read-only lock and cannot be modified.") + + super().__init__(buffer.getvalue()) + self.entity_type = type + self.entity_attributes = kwargs + + +class QueueLockedError(LockError): + """The requested queue is locked.""" + + def __init__(self, type: str, **kwargs: typing.Any): + super().__init__("The requested queue is locked.") + + +class BackendDatabaseError(DioptraError): """The backend database returned an unexpected response.""" + def __init__(self): + super().__init__( + "The backend database returned an unexpected response, please " + "contact the system administrator." + ) -class SearchNotImplementedError(Exception): + +class SearchNotImplementedError(DioptraError): """The search functionality has not been implemented.""" + def __init__(self): + super().__init__("The search functionality has not been implemented.") -class SearchParseError(Exception): - """The search query could not be parsed.""" +class SearchParseError(DioptraError): + """The search query could not be parsed.""" -class ResourceDoesNotExistError(Exception): - """The resource does not exist.""" + def __init__(self, context: str, error: str): + super().__init__("The provided search query could not be parsed.") + self.context = context + self.error = error -class DraftDoesNotExistError(Exception): +class DraftDoesNotExistError(DioptraError): """The requested draft does not exist.""" + def __init__(self): + super().__init__("The requested draft does not exist.") + -class DraftAlreadyExistsError(Exception): +class DraftAlreadyExistsError(DioptraError): """The draft already exists.""" + def __init__(self, type: str, id: int): + super().__init__(f"A draft for a [{type}] with id: {id} already exists.") + self.resource_type = type + self.resource_id = id -def register_base_error_handlers(api: Api) -> None: - @api.errorhandler(BackendDatabaseError) - def handle_backend_database_error(error): - return { - "message": "The backend database returned an unexpected response, please " - "contact the system administrator" - }, 500 - @api.errorhandler(SearchNotImplementedError) - def handle_search_not_implemented_error(error): - return {"message": "The search functionality has not been implemented"}, 501 +class SortParameterValidationError(DioptraError): + """The sort parameters are not valid.""" - @api.errorhandler(SearchParseError) - def handle_search_parse_error(error): - return { - "message": "The provided search query could not be parsed", - "query": error.args[0], - "reason": error.args[1], - }, 422 + def __init__(self, type: str, column: str, **kwargs): + super().__init__(f"The sort parameter, {column}, for {type} is not sortable.") - @api.errorhandler(ResourceDoesNotExistError) - def handle_resource_does_not_exist(error): - return {"message": "Not Found - The requested resource does not exist"}, 404 - @api.errorhandler(DraftDoesNotExistError) - def handle_draft_does_not_exist(error): - return {"message": "Not Found - The requested draft does not exist"}, 404 +class QueryParameterValidationError(DioptraError): + """Input parameters failed validation.""" - @api.errorhandler(DraftAlreadyExistsError) - def handle_draft_already_exists(error): - return ( - {"message": "Bad Request - The draft for this resource already exists."}, - 400, + def __init__(self, type: str, constraint: str, **kwargs): + buffer = StringIO() + buffer.write(f"Input parameters for {type} failed {constraint} check for ") + add_attribute_values(buffer, **kwargs) + buffer.write(".") + super().__init__(buffer.getvalue()) + self.resource_type = type + self.constraint = constraint + self.parameters = kwargs + + +class QueryParameterNotUniqueError(QueryParameterValidationError): + """Query Parameters failed unique validatation check.""" + + def __init__(self, type: str, **kwargs): + super().__init__(type, "unique", **kwargs) + + +class JobInvalidStatusTransitionError(DioptraError): + """The requested status transition is invalid.""" + + def __init__(self): + super().__init__("The requested job status update is invalid.") + + +class JobInvalidParameterNameError(DioptraError): + """The requested job parameter name is invalid.""" + + def __init__(self): + super().__init__( + "A provided job parameter name does not match any entrypoint " "parameters." + ) + + +class JobMlflowRunAlreadySetError(DioptraError): + """The requested job already has an mlflow run id set.""" + + def __init__(self): + super().__init__( + "The requested job already has an mlflow run id set. It may " + "not be changed." + ) + + +class EntryPointNotRegisteredToExperimentError(DioptraError): + """The requested entry point is not registered to the provided experiment.""" + + def __init__(self): + super().__init__( + "The requested entry point is not registered to the provided " "experiment." + ) + + +class QueueNotRegisteredToEntryPointError(DioptraError): + """The requested queue is not registered to the provided entry point.""" + + def __init__(self): + super().__init__( + "The requested queue is not registered to the provided entry " "point." + ) + + +class PluginParameterTypeMatchesBuiltinTypeError(DioptraError): + """The plugin parameter type name cannot match a built-in type.""" + + def __init__(self): + super().__init__( + "The requested plugin parameter type name matches a built-in " + "type. Please select another and resubmit." ) -def register_error_handlers(api: Api) -> None: +# User Errors +class NoCurrentUserError(DioptraError): + """There is no currently logged-in user.""" + + def __init__(self): + super().__init__("There is no currently logged-in user.") + + +class UserPasswordChangeError(DioptraError): + """Password change failed.""" + + def __init__(self, message: str): + super().__init__(message) + + +class UserPasswordError(DioptraError): + """Password Error.""" + + def __init__(self, message: str): + super().__init__(message) + + +STATUS_MESSAGE: typing.Final[dict[int, str]] = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 409: "Bad Request", + 422: "Unprocessable Content", + 500: "Internal Error", + 501: "Not Implemented", +} + + +def error_result( + error: DioptraError, status: int, detail: dict[str, typing.Any] +) -> tuple[dict[str, typing.Any], int]: + prefix = STATUS_MESSAGE.get(status, "Error") + return { + "error": error.__class__.__name__, + "message": f"{prefix} - {error.message}", + "detail": detail, + "originating_path": request.full_path, + }, status + + +# Silenced Complexity error for this function since it is a straitfoward registration of +# error handlers +def register_error_handlers(api: Api, **kwargs) -> None: # noqa: C901 """Registers the error handlers with the main application. Args: api: The main REST |Api| object. """ + log: BoundLogger = kwargs.get("log", LOGGER.new()) + + @api.errorhandler(EntityDoesNotExistError) + def handle_resource_does_not_exist_error(error: EntityDoesNotExistError): + log.debug( + "Entity not found", entity_type=error.entity_type, **error.entity_attributes + ) + return error_result( + error, 404, {"entity_type": error.entity_type, **error.entity_attributes} + ) + + @api.errorhandler(EntityExistsError) + def handle_entity_exists_error(error: EntityExistsError): + log.debug( + "Entity exists", + entity_type=error.entity_type, + existing_id=error.existing_id, + **error.entity_attributes, + ) + return error_result( + error, + 409, + { + "entity_type": error.entity_type, + "existing_id": error.existing_id, + "entity_attributes": {**error.entity_attributes}, + }, + ) + + @api.errorhandler(BackendDatabaseError) + def handle_backend_database_error(error: BackendDatabaseError): + log.error(error.to_message()) + return error_result(error, 500, {}) + + @api.errorhandler(SearchNotImplementedError) + def handle_search_not_implemented_error(error: SearchNotImplementedError): + log.debug(error.to_message()) + return error_result(error, 501, {}) + + @api.errorhandler(SearchParseError) + def handle_search_parse_error(error: SearchParseError): + log.debug(error.to_message()) + return error_result( + error, 422, {"query": error.args[0], "reason": error.args[1]} + ) + + @api.errorhandler(DraftDoesNotExistError) + def handle_draft_does_not_exist(error: DraftDoesNotExistError): + log.debug(error.to_message()) + return error_result(error, 404, {}) + + @api.errorhandler(DraftAlreadyExistsError) + def handle_draft_already_exists(error: DraftAlreadyExistsError): + log.debug(error.to_message()) + return error_result(error, 400, {}) + + @api.errorhandler(LockError) + def handle_lock_error(error: LockError): + log.debug(error.to_message()) + return error_result(error, 403, {}) + + @api.errorhandler(NoCurrentUserError) + def handle_no_current_user_error(error: NoCurrentUserError): + log.debug(error.to_message()) + return error_result(error, 401, {}) + + @api.errorhandler(UserPasswordChangeError) + def handle_password_change_error(error: UserPasswordChangeError): + log.debug(error.to_message()) + return error_result(error, 403, {}) + + @api.errorhandler(UserPasswordError) + def handle_user_password_error(error: UserPasswordError): + log.debug(error.to_message()) + return error_result(error, 401, {}) - from dioptra.restapi import v1 - - register_base_error_handlers(api) - v1.artifacts.errors.register_error_handlers(api) - v1.entrypoints.errors.register_error_handlers(api) - v1.experiments.errors.register_error_handlers(api) - v1.groups.errors.register_error_handlers(api) - v1.jobs.errors.register_error_handlers(api) - v1.models.errors.register_error_handlers(api) - v1.plugin_parameter_types.errors.register_error_handlers(api) - v1.plugins.errors.register_error_handlers(api) - v1.queues.errors.register_error_handlers(api) - v1.tags.errors.register_error_handlers(api) - v1.users.errors.register_error_handlers(api) + @api.errorhandler(DioptraError) + def handle_base_error(error: DioptraError): + log.debug(error.to_message()) + return error_result(error, 400, {}) diff --git a/src/dioptra/restapi/utils.py b/src/dioptra/restapi/utils.py index 5208cf124..ecdbb5f0f 100644 --- a/src/dioptra/restapi/utils.py +++ b/src/dioptra/restapi/utils.py @@ -25,6 +25,7 @@ import datetime import functools +from collections import Counter from importlib.resources import as_file, files from typing import Any, Callable, List, Protocol, Type, cast @@ -325,3 +326,26 @@ def setup_injection(api: Api, injector: Injector) -> None: ma.URL: str, ma.UUID: str, } + + +# Validation Functions +def find_non_unique(name: str, parameters: list[dict[str, Any]]) -> list[str]: + """ + Finds all values of a key that are not unique in a list of dictionaries. + Useful for checking that a provided input satisfies uniqueness constraints. + + Note that the key name must be in every dictionary of the provided list. + + Args: + name: the name of the parameter to check + parameters: the input parameters to check + + Returns: + A list of all values that were provided more than once, or an empty list if all + values of the key were unique + """ + name_count: Counter = Counter() + # this line fails if a parameter is missing a "name" value + name_count.update([parameter[name] for parameter in parameters]) + # create a list of all name values that appear more than once + return [key for key in name_count.keys() if name_count[key] > 1] diff --git a/src/dioptra/restapi/v1/artifacts/__init__.py b/src/dioptra/restapi/v1/artifacts/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/artifacts/__init__.py +++ b/src/dioptra/restapi/v1/artifacts/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/artifacts/service.py b/src/dioptra/restapi/v1/artifacts/service.py index 29e08a564..96cc99f5e 100644 --- a/src/dioptra/restapi/v1/artifacts/service.py +++ b/src/dioptra/restapi/v1/artifacts/service.py @@ -26,18 +26,17 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.jobs.service import ExperimentJobIdService, JobIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - ArtifactAlreadyExistsError, - ArtifactDoesNotExistError, - ArtifactSortError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() RESOURCE_TYPE: Final[str] = "artifact" @@ -97,14 +96,14 @@ def create( The newly created artifact object. Raises: - ArtifactAlreadyExistsError: If the artifact already exists. + EntityExistsError: If the artifact already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._artifact_uri_service.get(uri, log=log) is not None: - log.debug("Artifact uri already exists", uri=uri) - raise ArtifactAlreadyExistsError + duplicate = self._artifact_uri_service.get(uri, log=log) + if duplicate is not None: + raise EntityExistsError(RESOURCE_TYPE, duplicate.resource_id, uri=uri) job_dict = cast( utils.JobDict, @@ -220,8 +219,7 @@ def get( sort_column = sort_column.asc() latest_artifacts_stmt = latest_artifacts_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise ArtifactSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) artifacts = db.session.scalars(latest_artifacts_stmt).all() @@ -310,7 +308,7 @@ def get( artifact_uri: str, error_if_not_found: bool = False, **kwargs, - ) -> utils.ArtifactDict | None: + ) -> models.Artifact | None: """Fetch an artifact by its unique uri. Args: @@ -323,7 +321,7 @@ def get( The artifact object if found, otherwise None. Raises: - ArtifactDoesNotExistError: If the artifact is not found and + EntityDoesNotExistError: If the artifact is not found and `error_if_not_found` is True. """ @@ -345,8 +343,7 @@ def get( if artifact is None: if error_if_not_found: - log.debug("Artifact not found", artifact_uri=artifact_uri) - raise ArtifactDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, artifact_uri=artifact_uri) return None @@ -373,7 +370,7 @@ def get( The artifact object if found, otherwise None. Raises: - ArtifactDoesNotExistError: If the artifact is not found and + EntityDoesNotExistError: If the artifact is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -393,8 +390,7 @@ def get( if artifact is None: if error_if_not_found: - log.debug("Artifact not found", artifact_id=artifact_id) - raise ArtifactDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, artifact_id=artifact_id) return None @@ -433,9 +429,9 @@ def modify( The updated artifact object. Raises: - ArtifactDoesNotExistError: If the artifact is not found and + EntityDoesNotExistError: If the artifact is not found and `error_if_not_found` is True. - ArtifactAlreadyExistsError: If the artifact name already exists. + EntityExistsError: If the artifact name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) diff --git a/src/dioptra/restapi/v1/entrypoints/__init__.py b/src/dioptra/restapi/v1/entrypoints/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/entrypoints/__init__.py +++ b/src/dioptra/restapi/v1/entrypoints/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/entrypoints/errors.py b/src/dioptra/restapi/v1/entrypoints/errors.py deleted file mode 100644 index 907983092..000000000 --- a/src/dioptra/restapi/v1/entrypoints/errors.py +++ /dev/null @@ -1,77 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the entrypoint endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class EntrypointAlreadyExistsError(Exception): - """The entrypoint name already exists.""" - - -class EntrypointDoesNotExistError(Exception): - """The requested entrypoint does not exist.""" - - -class EntrypointPluginDoesNotExistError(Exception): - """The requested plugin does not exist for the entrypoint.""" - - -class EntrypointParameterNamesNotUniqueError(Exception): - """Multiple entrypoint parameters share the same name.""" - - -class EntrypointSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(EntrypointDoesNotExistError) - def handle_entrypoint_does_not_exist_error(error): - return {"message": "Not Found - The requested entrypoint does not exist"}, 404 - - @api.errorhandler(EntrypointPluginDoesNotExistError) - def handle_entrypoint_plugin_does_not_exist_error(error): - return { - "message": "Not Found - The requested plugin does not exist for this " - "entrypoint" - }, 404 - - @api.errorhandler(EntrypointAlreadyExistsError) - def handle_entrypoint_already_exists_error(error): - return ( - { - "message": "Bad Request - The entrypoint name on the registration form " - "already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(EntrypointParameterNamesNotUniqueError) - def handle_entrypoint_parameter_names_not_unique_error(error): - return { - "message": "Bad Request - The entrypoint contains multiple parameters " - "with the same name." - }, 400 - - @api.errorhandler(EntrypointSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/entrypoints/service.py b/src/dioptra/restapi/v1/entrypoints/service.py index bc0af723d..a8bdf5456 100644 --- a/src/dioptra/restapi/v1/entrypoints/service.py +++ b/src/dioptra/restapi/v1/entrypoints/service.py @@ -27,23 +27,23 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import resource_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + QueryParameterNotUniqueError, + SortParameterValidationError, +) +from dioptra.restapi.utils import find_non_unique from dioptra.restapi.v1 import utils from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.plugins.service import PluginIdsService -from dioptra.restapi.v1.queues.errors import QueueDoesNotExistError +from dioptra.restapi.v1.queues.service import RESOURCE_TYPE as QUEUE_RESOURCE_TYPE from dioptra.restapi.v1.queues.service import QueueIdsService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - EntrypointAlreadyExistsError, - EntrypointDoesNotExistError, - EntrypointParameterNamesNotUniqueError, - EntrypointPluginDoesNotExistError, - EntrypointSortError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() +PLUGIN_RESOURCE_TYPE: Final[str] = "entry_point_plugin" RESOURCE_TYPE: Final[str] = "entry_point" SEARCHABLE_FIELDS: Final[dict[str, Any]] = { @@ -110,17 +110,15 @@ def create( The newly created entrypoint object. Raises: - EntrypointAlreadyExistsError: If a entrypoint with the given name already - exists. + EntityExistsError: If a entrypoint with the given name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if ( - self._entrypoint_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Entrypoint name already exists", name=name, group_id=group_id) - raise EntrypointAlreadyExistsError + duplicate = self._entrypoint_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) group = self._group_id_service.get(group_id, error_if_not_found=True) queues = self._queue_ids_service.get(queue_ids, error_if_not_found=True) @@ -263,8 +261,7 @@ def get( sort_column = sort_column.asc() entrypoints_stmt = entrypoints_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise EntrypointSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) entrypoints = list(db.session.scalars(entrypoints_stmt).unique().all()) @@ -344,7 +341,7 @@ def get( The entrypoint object if found, otherwise None. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found and + EntityDoesNotExistError: If the entrypoint is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -364,8 +361,9 @@ def get( if entrypoint is None: if error_if_not_found: - log.debug("Entrypoint not found", entrypoint_id=entrypoint_id) - raise EntrypointDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, entrypoint_id=entrypoint_id + ) return None @@ -420,15 +418,16 @@ def modify( The updated entrypoint object. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found and + EntityDoesNotExistError: If the entrypoint is not found and `error_if_not_found` is True. - EntrypointAlreadyExistsError: If the entrypoint name already exists. + EntityExistsError: If the entrypoint name already exists. + QueryParameterNotUniqueError: If the values for the "name" parameter in the + parameters list is not unique """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - - parameter_names = [parameter["name"] for parameter in parameters] - if len(parameter_names) > len(set(parameter_names)): - raise EntrypointParameterNamesNotUniqueError + duplicates = find_non_unique("name", parameters) + if len(duplicates) > 0: + raise QueryParameterNotUniqueError(RESOURCE_TYPE, name=duplicates) entrypoint_dict = self.get( entrypoint_id, error_if_not_found=error_if_not_found, log=log @@ -439,13 +438,14 @@ def modify( entrypoint = entrypoint_dict["entry_point"] group_id = entrypoint.resource.group_id - if ( - name != entrypoint.name - and self._entrypoint_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Entrypoint name already exists", name=name, group_id=group_id) - raise EntrypointAlreadyExistsError + if name != entrypoint.name: + duplicate = self._entrypoint_name_service.get( + name, group_id=group_id, log=log + ) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) queues = self._queue_ids_service.get(queue_ids, error_if_not_found=True) @@ -517,7 +517,7 @@ def delete(self, entrypoint_id: int, **kwargs) -> dict[str, Any]: entrypoint_resource = db.session.scalars(stmt).first() if entrypoint_resource is None: - raise EntrypointDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, entrypoint_id=entrypoint_id) deleted_resource_lock = models.ResourceLock( resource_lock_type=resource_lock_types.DELETE, @@ -567,7 +567,7 @@ def get( The plugin snapshots for the entrypoint. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found. + EntityDoesNotExistError: If the entrypoint is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug("Get entrypoint by id", entrypoint_id=entrypoint_id) @@ -600,8 +600,8 @@ def append( The updated entrypoint object. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found. - EntrypointAlreadyExistsError: If the entrypoint name already exists. + EntityDoesNotExistError: If the entrypoint is not found. + EntityExistsError: If the entrypoint name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -711,7 +711,7 @@ def get( The plugin snapshots for the entrypoint. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found. + EntityDoesNotExistError: If the entrypoint or plugin is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug("Get entrypoint by id", entrypoint_id=entrypoint_id) @@ -728,7 +728,9 @@ def get( for entry_point_plugin_file in entrypoint.entry_point_plugin_files } if plugin_id not in plugins: - raise EntrypointPluginDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_RESOURCE_TYPE, entrypoint_id=entrypoint_id, plugin_id=plugin_id + ) plugin = utils.PluginWithFilesDict( plugin=plugins[plugin_id], plugin_files=[], has_draft=None @@ -757,8 +759,7 @@ def delete( A dictionary reporting the status of the request. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found. - EntrypointPluginDoesNotExistError: If the plugin is not found. + EntityDoesNotExistError: If the entrypoint or plugin is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -773,7 +774,9 @@ def delete( plugin.plugin.resource_id for plugin in entrypoint.entry_point_plugin_files ) if plugin_id not in plugin_ids: - raise EntrypointPluginDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_RESOURCE_TYPE, entrypoint_id=entrypoint_id, plugin_id=plugin_id + ) # create a new snapshot with the plugin removed new_entrypoint = models.EntryPoint( @@ -830,7 +833,7 @@ def get( The entrypoint object if found, otherwise None. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found and + EntityDoesNotExistError: If the entrypoint is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -852,10 +855,9 @@ def get( entrypoint_ids_missing = set(entrypoint_ids) - set( entrypoint.resource_id for entrypoint in entrypoints ) - log.debug( - "Entrypoint not found", entrypoint_ids=list(entrypoint_ids_missing) + raise EntityDoesNotExistError( + RESOURCE_TYPE, entrypoint_ids=list(entrypoint_ids_missing) ) - raise EntrypointDoesNotExistError return entrypoints @@ -896,7 +898,7 @@ def get( The list of plugins. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found and + EntityDoesNotExistError: If the entrypoint is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -933,9 +935,9 @@ def append( The updated list of queues resource objects. Raises: - EntrypointDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. - QueueDoesNotExistError: If one or more queues are not found. + EntityDoesNotExistError: If one or more queues are not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug( @@ -960,11 +962,6 @@ def append( new_queues = self._queue_ids_service.get( list(new_queue_ids), error_if_not_found=True, log=log ) - if error_if_not_found and len(new_queues) != len(new_queue_ids): - found_queue_ids = set(queue.resource_id for queue in new_queues) - missing_queue_ids = new_queue_ids - found_queue_ids - log.debug(queue_ids=list(missing_queue_ids)) - raise QueueDoesNotExistError entrypoint.children.extend([queue.resource for queue in new_queues]) @@ -995,9 +992,9 @@ def modify( The updated queue resource object. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. - QueueDoesNotExistError: If one or more queues are not found. + EntityDoesNotExistError: If one or more queues are not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -1109,7 +1106,7 @@ def delete(self, entrypoint_id: int, queue_id, **kwargs) -> dict[str, Any]: removed_queue = queue_resources.pop(queue_id, None) if removed_queue is None: - raise QueueDoesNotExistError + raise EntityDoesNotExistError(QUEUE_RESOURCE_TYPE, queue_id=queue_id) plugin_resources = [ resource @@ -1146,7 +1143,7 @@ def get( The entrypoint object if found, otherwise None. Raises: - EntrypointDoesNotExistError: If the entrypoint is not found and + EntityDoesNotExistError: If the entrypoint is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -1167,8 +1164,9 @@ def get( if entrypoint is None: if error_if_not_found: - log.debug("Entrypoint not found", name=name) - raise EntrypointDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, name=name, group_id=group_id + ) return None diff --git a/src/dioptra/restapi/v1/experiments/__init__.py b/src/dioptra/restapi/v1/experiments/__init__.py index 006f5798f..99936763a 100644 --- a/src/dioptra/restapi/v1/experiments/__init__.py +++ b/src/dioptra/restapi/v1/experiments/__init__.py @@ -15,6 +15,3 @@ # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode """The experiments endpoint subpackage.""" -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/experiments/errors.py b/src/dioptra/restapi/v1/experiments/errors.py deleted file mode 100644 index 5b626c030..000000000 --- a/src/dioptra/restapi/v1/experiments/errors.py +++ /dev/null @@ -1,53 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the experiment endpoints.""" -from flask_restx import Api - - -class ExperimentAlreadyExistsError(Exception): - """The experiment name already exists.""" - - -class ExperimentDoesNotExistError(Exception): - """The requested experiment does not exist.""" - - -class ExperimentSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(ExperimentAlreadyExistsError) - def handle_experiment_already_exists_error(error): - return ( - { - "message": "Bad Request - The experiment name on the registration form " - "already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(ExperimentDoesNotExistError) - def handle_experiment_does_not_exist_error(error): - return {"message": "Not Found - The requested experiment does not exist"}, 404 - - @api.errorhandler(ExperimentSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/experiments/service.py b/src/dioptra/restapi/v1/experiments/service.py index aec8e9f9a..df1d1f2ee 100644 --- a/src/dioptra/restapi/v1/experiments/service.py +++ b/src/dioptra/restapi/v1/experiments/service.py @@ -27,19 +27,20 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import resource_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils -from dioptra.restapi.v1.entrypoints.errors import EntrypointDoesNotExistError +from dioptra.restapi.v1.entrypoints.service import ( + RESOURCE_TYPE as ENTRYPOINT_RESOURCE_TYPE, +) from dioptra.restapi.v1.entrypoints.service import EntrypointIdsService from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - ExperimentAlreadyExistsError, - ExperimentDoesNotExistError, - ExperimentSortError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() RESOURCE_TYPE: Final[str] = "experiment" @@ -103,17 +104,16 @@ def create( The newly created experiment object. Raises: - ExperimentAlreadyExistsError: If an experiment with the given name already + EntityExistsError: If an experiment with the given name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if ( - self._experiment_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Experiment name already exists", name=name, group_id=group_id) - raise ExperimentAlreadyExistsError + duplicate = self._experiment_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) group = self._group_id_service.get(group_id, error_if_not_found=True) entrypoints = ( @@ -237,8 +237,7 @@ def get( sort_column = sort_column.asc() experiments_stmt = experiments_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise ExperimentSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) experiments = list(db.session.scalars(experiments_stmt).all()) @@ -320,7 +319,7 @@ def get( The experiment object if found, otherwise none. Raises: - ExperimentDoesNotExistError: If the experiment is not found and if + EntityDoesNotExistError: If the experiment is not found and if `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -340,8 +339,9 @@ def get( if experiment is None: if error_if_not_found: - log.debug("Experiment not found", experiment_id=experiment_id) - raise ExperimentDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, experiment_id=experiment_id + ) return None @@ -398,9 +398,9 @@ def modify( The updated experiment object. Raises: - ExperimentDoesNotExistError: If the experiment is not found and + EntityDoesNotExistError: If the experiment is not found and `error_if_not_found` is True. - ExperimentAlreadyExistsError: If the experiment name already exists. + EntityExistsError: If the experiment name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -413,13 +413,14 @@ def modify( experiment = experiment_dict["experiment"] group_id = experiment.resource.group_id - if ( - name != experiment.name - and self._experiment_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Experiment name already exists", name=name, group_id=group_id) - raise ExperimentAlreadyExistsError + if name != experiment.name: + duplicate = self._experiment_name_service.get( + name, group_id=group_id, log=log + ) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) entrypoints = self._entrypoint_ids_service.get( entrypoint_ids, error_if_not_found=True, log=log @@ -466,7 +467,7 @@ def delete(self, experiment_id: int, **kwargs) -> dict[str, Any]: experiment_resource = db.session.scalars(stmt).first() if experiment_resource is None: - raise ExperimentDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, experiment_id=experiment_id) deleted_resource_lock = models.ResourceLock( resource_lock_type=resource_lock_types.DELETE, @@ -515,7 +516,7 @@ def get( The list of plugins. Raises: - ExperimentDoesNotExistError: If the experiment is not found and + EntityDoesNotExistError: If the experiment is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -553,9 +554,9 @@ def append( The updated list of entrypoints resource objects. Raises: - ExperimentDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the experiment is not found and `error_if_not_found` is True. - EntrypointDoesNotExistError: If one or more entrypoints are not found. + EntityDoesNotExistError: If one or more entrypoints are not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug( @@ -584,8 +585,9 @@ def append( entrypoint.resource_id for entrypoint in new_entrypoints ) missing_entrypoint_ids = new_entrypoint_ids - found_entrypoint_ids - log.debug(entrypoint_ids=list(missing_entrypoint_ids)) - raise EntrypointDoesNotExistError + raise EntityDoesNotExistError( + ENTRYPOINT_RESOURCE_TYPE, entrypoint_ids=list(missing_entrypoint_ids) + ) experiment.children.extend( [entrypoint.resource for entrypoint in new_entrypoints] @@ -620,9 +622,9 @@ def modify( The updated entrypoint resource object. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. - EntrypointDoesNotExistError: If one or more entrypoints are not found. + EntityDoesNotExistError: If one or more entrypoints are not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -727,7 +729,11 @@ def delete(self, experiment_id: int, entrypoint_id, **kwargs) -> dict[str, Any]: removed_entrypoint = entrypoint_resources.pop(entrypoint_id, None) if removed_entrypoint is None: - raise EntrypointDoesNotExistError + raise EntityDoesNotExistError( + ENTRYPOINT_RESOURCE_TYPE, + experiment_id=experiment_id, + entrypoint_id=entrypoint_id, + ) experiment.children = list(entrypoint_resources.values()) @@ -763,7 +769,7 @@ def get( The experiment object if found, otherwise None. Raises: - ExperimentDoesNotExistError: If the experiment is not found and + EntityDoesNotExistError: If the experiment is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -784,8 +790,9 @@ def get( if experiment is None: if error_if_not_found: - log.debug("Experiment not found", name=name) - raise ExperimentDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, experiment_name=name, group_id=group_id + ) return None diff --git a/src/dioptra/restapi/v1/groups/__init__.py b/src/dioptra/restapi/v1/groups/__init__.py index 9d5cefcdb..e80d1a337 100644 --- a/src/dioptra/restapi/v1/groups/__init__.py +++ b/src/dioptra/restapi/v1/groups/__init__.py @@ -15,6 +15,3 @@ # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode """The groups endpoint subpackage.""" -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/groups/errors.py b/src/dioptra/restapi/v1/groups/errors.py deleted file mode 100644 index 3d7449852..000000000 --- a/src/dioptra/restapi/v1/groups/errors.py +++ /dev/null @@ -1,41 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the group endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class GroupNameNotAvailableError(Exception): - """The group name is not available.""" - - -class GroupDoesNotExistError(Exception): - """The requested group does not exist.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(GroupDoesNotExistError) - def handle_user_does_not_exist_error(error): - return {"message": "Not Found - The requested group does not exist"}, 404 - - @api.errorhandler(GroupNameNotAvailableError) - def handle_no_current_user_error(error): - return ( - {"message": "Bad Request - The group name is not available"}, - 400, - ) diff --git a/src/dioptra/restapi/v1/groups/service.py b/src/dioptra/restapi/v1/groups/service.py index 09109bac3..271b9d6ea 100644 --- a/src/dioptra/restapi/v1/groups/service.py +++ b/src/dioptra/restapi/v1/groups/service.py @@ -27,11 +27,13 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import group_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, +) from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import GroupDoesNotExistError, GroupNameNotAvailableError - LOGGER: BoundLogger = structlog.stdlib.get_logger() DEFAULT_GROUP_MEMBER_PERMISSIONS: Final[dict[str, bool]] = { @@ -58,6 +60,8 @@ "name": lambda x: models.Group.name.like(x, escape="/"), } +GROUP_TYPE: Final[str] = "group" + class GroupService(object): """The service methods used for creating and managing groups.""" @@ -104,9 +108,9 @@ def create( """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._group_name_service.get(name) is not None: - log.debug("Group name already exists", name=name) - raise GroupNameNotAvailableError + duplicate = self._group_name_service.get(name) + if duplicate is not None: + raise EntityExistsError("group", duplicate.group_id, name=name) new_group = models.Group(name=name, creator=creator) self._group_member_service.create( @@ -222,7 +226,7 @@ def get( The group object if found, otherwise None. Raises: - UserDoesNotExistError: If the group is not found and `error_if_not_found` + EntityDoesNotExistError: If the group is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -233,8 +237,7 @@ def get( if group is None: if error_if_not_found: - log.debug("Group not found", group_id=group_id) - raise GroupDoesNotExistError + raise EntityDoesNotExistError(GROUP_TYPE, group_id=group_id) return None @@ -261,7 +264,7 @@ def modify( The group object. Raises: - GroupDoesNotExistError: If the group is not found and `error_if_not_found` + EntityDoesNotExistError: If the group is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -272,14 +275,13 @@ def modify( if group is None: if error_if_not_found: - log.debug("Group not found", group_id=group_id) - raise GroupDoesNotExistError + raise EntityDoesNotExistError(GROUP_TYPE, group_id=group_id) return None - if self._group_name_service.get(name, log=log) is not None: - log.debug("Group name already exists", name=name) - raise GroupNameNotAvailableError + duplicate = self._group_name_service.get(name, log=log) + if duplicate is not None: + raise EntityExistsError(GROUP_TYPE, duplicate.group_id, name=name) current_timestamp = datetime.datetime.now(tz=datetime.timezone.utc) group.last_modified_on = current_timestamp @@ -307,7 +309,7 @@ def delete(self, group_id: int, **kwargs) -> dict[str, Any]: group = db.session.scalars(stmt).first() if group is None: - raise GroupDoesNotExistError + raise EntityDoesNotExistError(GROUP_TYPE, group_id=group_id) name = group.name @@ -339,7 +341,7 @@ def get( The group object if found, otherwise None. Raises: - GroupDoesNotExistError: If the group is not found and `error_if_not_found` + EntityDoesNotExistError: If the group is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -350,8 +352,7 @@ def get( if group is None: if error_if_not_found: - log.debug("Group not found", name=name) - raise GroupDoesNotExistError + raise EntityDoesNotExistError(GROUP_TYPE, name=name) return None diff --git a/src/dioptra/restapi/v1/jobs/__init__.py b/src/dioptra/restapi/v1/jobs/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/jobs/__init__.py +++ b/src/dioptra/restapi/v1/jobs/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/jobs/errors.py b/src/dioptra/restapi/v1/jobs/errors.py deleted file mode 100644 index 48a984042..000000000 --- a/src/dioptra/restapi/v1/jobs/errors.py +++ /dev/null @@ -1,105 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the job endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class JobDoesNotExistError(Exception): - """The requested job does not exist.""" - - -class JobInvalidStatusTransitionError(Exception): - """The requested status transition is invalid.""" - - -class JobInvalidParameterNameError(Exception): - """The requested job parameter name is invalid.""" - - -class JobMlflowRunAlreadySetError(Exception): - """The requested job already has an mlflow run id set.""" - - -class ExperimentJobDoesNotExistError(Exception): - """The requested experiment job does not exist.""" - - -class EntryPointNotRegisteredToExperimentError(Exception): - """The requested entry point is not registered to the provided experiment.""" - - -class QueueNotRegisteredToEntryPointError(Exception): - """The requested queue is not registered to the provided entry point.""" - - -class JobSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(JobDoesNotExistError) - def handle_job_does_not_exist_error(error): - return {"message": "Not Found - The requested job does not exist"}, 404 - - @api.errorhandler(JobInvalidStatusTransitionError) - def handle_job_invalid_status_transition_error(error): - return { - "message": "Bad Request - The requested job status update is invalid" - }, 400 - - @api.errorhandler(JobInvalidParameterNameError) - def handle_job_invalid_parameter_name_error(error): - return { - "message": "Bad Request - A provided job parameter name does not match any " - "entrypoint parameters" - }, 400 - - @api.errorhandler(JobMlflowRunAlreadySetError) - def handle_job_mlflow_run_already_set_error(error): - return { - "message": "Bad Request - The requested job already has an mlflow run id " - "set. It may not be changed." - }, 400 - - @api.errorhandler(ExperimentJobDoesNotExistError) - def handle_experiment_job_does_not_exist_error(error): - return { - "message": "Not Found - The requested experiment job does not exist" - }, 404 - - @api.errorhandler(EntryPointNotRegisteredToExperimentError) - def handle_entry_point_not_registered_to_experiment_error(error): - return { - "message": "Bad Request - The requested entry point is not registered to " - "the provided experiment" - }, 400 - - @api.errorhandler(QueueNotRegisteredToEntryPointError) - def handle_queue_not_registered_to_entry_point_error(error): - return { - "message": "Bad Request - The requested queue is not registered to the " - "provided entry point" - }, 400 - - @api.errorhandler(JobSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/jobs/service.py b/src/dioptra/restapi/v1/jobs/service.py index 8592d85ca..b88c30bec 100644 --- a/src/dioptra/restapi/v1/jobs/service.py +++ b/src/dioptra/restapi/v1/jobs/service.py @@ -27,7 +27,16 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntryPointNotRegisteredToExperimentError, + JobInvalidParameterNameError, + JobInvalidStatusTransitionError, + JobMlflowRunAlreadySetError, + QueueNotRegisteredToEntryPointError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils from dioptra.restapi.v1.entrypoints.service import EntrypointIdService from dioptra.restapi.v1.experiments.service import ExperimentIdService @@ -36,17 +45,6 @@ from dioptra.restapi.v1.shared.rq_service import RQServiceV1 from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - EntryPointNotRegisteredToExperimentError, - ExperimentJobDoesNotExistError, - JobDoesNotExistError, - JobInvalidParameterNameError, - JobInvalidStatusTransitionError, - JobMlflowRunAlreadySetError, - JobSortError, - QueueNotRegisteredToEntryPointError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() RESOURCE_TYPE: Final[str] = "job" @@ -125,7 +123,7 @@ def create( The newly created job object. Raises: - JobAlreadyExistsError: If a job with the given name already exists. + EntityExistsError: If a job with the given name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -359,8 +357,7 @@ def get( sort_column = sort_column.asc() jobs_stmt = jobs_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise JobSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) jobs = list(db.session.scalars(jobs_stmt).all()) @@ -408,7 +405,7 @@ def get( The job object if found, otherwise None. Raises: - JobDoesNotExistError: If the job is not found and `error_if_not_found` + EntityDoesNotExistError: If the job is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -427,8 +424,7 @@ def get( if job is None: if error_if_not_found: - log.debug("Job not found", job_id=job_id) - raise JobDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, job_id=job_id) return None @@ -465,7 +461,7 @@ def delete(self, job_id: int, **kwargs) -> dict[str, Any]: job_resource = db.session.scalars(stmt).first() if job_resource is None: - raise JobDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, job_id=job_id) deleted_resource_lock = models.ResourceLock( resource_lock_type="delete", @@ -523,8 +519,7 @@ def get( job = db.session.scalars(stmt).first() if job is None: - log.debug("Job not found", job_id=job_id) - raise JobDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, job_id=job_id) return {"status": job.status, "id": job.resource_id} @@ -609,7 +604,7 @@ def get( query. Raises: - ExperimentDoesNotExistError: If the experiment is not found. + EntityDoesNotExistError: If the experiment is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug("Get full list of jobs for experiment", experiment_id=experiment_id) @@ -674,8 +669,7 @@ def get( sort_column = sort_column.asc() jobs_stmt = jobs_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise JobSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) jobs = list(db.session.scalars(jobs_stmt).all()) @@ -721,7 +715,7 @@ def get(self, experiment_id: int, job_id: int, **kwargs) -> utils.JobDict: The job object if found, otherwise None. Raises: - ExperimentJobDoesNotExistError: If the job associated with the experiment + EntityDoesNotExistError: If the job associated with the experiment is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -733,12 +727,9 @@ def get(self, experiment_id: int, job_id: int, **kwargs) -> utils.JobDict: experiment_job = db.session.scalar(experiment_job_stmt) if experiment_job is None: - log.debug( - "Experiment Job not found", - job_id=job_id, - experiment_id=experiment_id, + raise EntityDoesNotExistError( + RESOURCE_TYPE, job_id=job_id, experiment_id=experiment_id ) - raise ExperimentJobDoesNotExistError return cast( utils.JobDict, @@ -768,12 +759,9 @@ def delete(self, experiment_id: int, job_id: int, **kwargs) -> dict[str, Any]: experiment_job = db.session.scalar(experiment_job_stmt) if experiment_job is None: - log.debug( - "Job associated with experiment not found", - job_id=job_id, - experiment_id=experiment_id, + raise EntityDoesNotExistError( + RESOURCE_TYPE, job_id=job_id, experiment_id=experiment_id ) - raise ExperimentJobDoesNotExistError return self._job_id_service.delete( job_id=job_id, diff --git a/src/dioptra/restapi/v1/models/__init__.py b/src/dioptra/restapi/v1/models/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/models/__init__.py +++ b/src/dioptra/restapi/v1/models/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/models/errors.py b/src/dioptra/restapi/v1/models/errors.py deleted file mode 100644 index ef9da6579..000000000 --- a/src/dioptra/restapi/v1/models/errors.py +++ /dev/null @@ -1,65 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the model endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class ModelAlreadyExistsError(Exception): - """The model name already exists.""" - - -class ModelDoesNotExistError(Exception): - """The requested model does not exist.""" - - -class ModelVersionDoesNotExistError(Exception): - """The requested version of the model does not exist.""" - - -class ModelSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(ModelDoesNotExistError) - def handle_model_does_not_exist_error(error): - return {"message": "Not Found - The requested model does not exist"}, 404 - - @api.errorhandler(ModelVersionDoesNotExistError) - def handle_model_version_does_not_exist_error(error): - return { - "message": "Not Found - The requested model version does not exist" - }, 404 - - @api.errorhandler(ModelAlreadyExistsError) - def handle_model_already_exists_error(error): - return ( - { - "message": "Bad Request - The model name on the registration form " - "already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(ModelSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/models/service.py b/src/dioptra/restapi/v1/models/service.py index e295efa42..7bfd6bf18 100644 --- a/src/dioptra/restapi/v1/models/service.py +++ b/src/dioptra/restapi/v1/models/service.py @@ -27,19 +27,17 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils from dioptra.restapi.v1.artifacts.service import ArtifactIdService from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - ModelAlreadyExistsError, - ModelDoesNotExistError, - ModelSortError, - ModelVersionDoesNotExistError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() MODEL_RESOURCE_TYPE: Final[str] = "ml_model" @@ -100,13 +98,15 @@ def create( The newly created model object. Raises: - ModelAlreadyExistsError: If a model with the given name already exists. + EntityExistsError: If a model with the given name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._model_name_service.get(name, group_id=group_id, log=log) is not None: - log.debug("Model name already exists", name=name, group_id=group_id) - raise ModelAlreadyExistsError + duplicate = self._model_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + MODEL_RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) group = self._group_id_service.get(group_id, error_if_not_found=True) @@ -220,10 +220,7 @@ def get( sort_column = sort_column.asc() latest_ml_models_stmt = latest_ml_models_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in MODEL_SORTABLE_FIELDS: - log.debug( - f"sort_by_string: '{sort_by_string}' is not in MODEL_SORTABLE_FIELDS" - ) - raise ModelSortError + raise SortParameterValidationError(MODEL_RESOURCE_TYPE, sort_by_string) ml_models = db.session.scalars(latest_ml_models_stmt).all() @@ -326,7 +323,7 @@ def get( The model object if found, otherwise None. Raises: - ModelDoesNotExistError: If the model is not found and `error_if_not_found` + EntityDoesNotExistError: If the model is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -346,8 +343,7 @@ def get( if ml_model is None: if error_if_not_found: - log.debug("Model not found", model_id=model_id) - raise ModelDoesNotExistError + raise EntityDoesNotExistError(MODEL_RESOURCE_TYPE, model_id=model_id) return None @@ -404,9 +400,9 @@ def modify( The updated model object. Raises: - ModelDoesNotExistError: If the model is not found and `error_if_not_found` + EntityDoesNotExistError: If the model is not found and `error_if_not_found` is True. - ModelAlreadyExistsError: If the model name already exists. + EntityExistsError: If the model name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -415,9 +411,6 @@ def modify( ) if ml_model_dict is None: - if error_if_not_found: - raise ModelDoesNotExistError - return None ml_model = ml_model_dict["ml_model"] @@ -425,13 +418,15 @@ def modify( has_draft = ml_model_dict["has_draft"] group_id = ml_model.resource.group_id - if ( - name != ml_model.name - and self._model_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Model name already exists", name=name, group_id=group_id) - raise ModelAlreadyExistsError + if name != ml_model.name: + duplicate = self._model_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + MODEL_RESOURCE_TYPE, + duplicate.resource_id, + name=name, + group_id=group_id, + ) new_ml_model = models.MlModel( name=name, @@ -473,7 +468,7 @@ def delete(self, model_id: int, **kwargs) -> dict[str, Any]: model_resource = db.session.scalars(stmt).first() if model_resource is None: - raise ModelDoesNotExistError + raise EntityDoesNotExistError(MODEL_RESOURCE_TYPE, model_id=model_id) deleted_resource_lock = models.ResourceLock( resource_lock_type="delete", @@ -594,7 +589,7 @@ def get( None. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -705,7 +700,7 @@ def get( The requested version the resource object if found, otherwise None. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -735,8 +730,11 @@ def get( if latest_version is None: if error_if_not_found: - log.debug("Model version not found", version_number=version_number) - raise ModelVersionDoesNotExistError + raise EntityDoesNotExistError( + MODEL_VERSION_RESOURCE_TYPE, + model_id=model_id, + version_number=version_number, + ) return None @@ -766,9 +764,9 @@ def modify( The updated model object. Raises: - ModelDoesNotExistError: If the model is not found and `error_if_not_found` + EntityDoesNotExistError: If the model is not found and `error_if_not_found` is True. - ModelAlreadyExistsError: If the model name already exists. + EntityExistsError: If the model name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -827,7 +825,7 @@ def get( The model object if found, otherwise None. Raises: - ModelDoesNotExistError: If the model is not found and `error_if_not_found` + EntityDoesNotExistError: If the model is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -848,8 +846,9 @@ def get( if ml_model is None: if error_if_not_found: - log.debug("Model not found", name=name) - raise ModelDoesNotExistError + raise EntityDoesNotExistError( + MODEL_RESOURCE_TYPE, name=name, group_id=group_id + ) return None diff --git a/src/dioptra/restapi/v1/plugin_parameter_types/__init__.py b/src/dioptra/restapi/v1/plugin_parameter_types/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/plugin_parameter_types/__init__.py +++ b/src/dioptra/restapi/v1/plugin_parameter_types/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/plugin_parameter_types/errors.py b/src/dioptra/restapi/v1/plugin_parameter_types/errors.py deleted file mode 100644 index e45772f31..000000000 --- a/src/dioptra/restapi/v1/plugin_parameter_types/errors.py +++ /dev/null @@ -1,95 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the plugin parameter type endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class PluginParameterTypeMatchesBuiltinTypeError(Exception): - """The plugin parameter type name cannot match a built-in type.""" - - -class PluginParameterTypeAlreadyExistsError(Exception): - """The plugin parameter type name already exists.""" - - -class PluginParameterTypeDoesNotExistError(Exception): - """The requested plugin parameter type does not exist.""" - - -class PluginParameterTypeReadOnlyLockError(Exception): - """The plugin parameter type has a read-only lock and cannot be modified.""" - - -class PluginParameterTypeMissingParameterError(Exception): - """The requested plugin parameter type is missing a required parameter.""" - - -class PluginParameterSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(PluginParameterTypeMatchesBuiltinTypeError) - def handle_plugin_parameter_type_matches_builtin_type_error(error): - return { - "message": "Bad Request - The requested plugin parameter type name " - "matches a built-in type. Please select another and resubmit." - }, 400 - - @api.errorhandler(PluginParameterTypeDoesNotExistError) - def handle_plugin_parameter_type_does_not_exist_error(error): - return { - "message": "Not Found - The requested plugin parameter type does " - "not exist" - }, 404 - - @api.errorhandler(PluginParameterTypeReadOnlyLockError) - def handle_plugin_parameter_type_read_only_lock_error(error): - return { - "message": "Forbidden - The plugin parameter type has a read-only " - "lock and cannot be modified." - }, 403 - - @api.errorhandler(PluginParameterTypeMissingParameterError) - def handle_plugin_parameter_type_missing_parameter_error(error): - return ( - { - "message": "Forbidden - The requested plugin parameter " - "type is missing a required parameter." - }, - 400, - ) - - @api.errorhandler(PluginParameterTypeAlreadyExistsError) - def handle_plugin_parameter_type_already_exists_error(error): - return ( - { - "message": "Bad Request - The plugin parameter type name on " - "the registration form already exists. Please select " - "another and resubmit." - }, - 400, - ) - - @api.errorhandler(PluginParameterSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/plugin_parameter_types/service.py b/src/dioptra/restapi/v1/plugin_parameter_types/service.py index 28e3c1cd7..e4ce40fa8 100644 --- a/src/dioptra/restapi/v1/plugin_parameter_types/service.py +++ b/src/dioptra/restapi/v1/plugin_parameter_types/service.py @@ -25,20 +25,19 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import resource_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + PluginParameterTypeMatchesBuiltinTypeError, + ReadOnlyLockError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters from dioptra.task_engine.type_registry import BUILTIN_TYPES -from .errors import ( - PluginParameterSortError, - PluginParameterTypeAlreadyExistsError, - PluginParameterTypeDoesNotExistError, - PluginParameterTypeMatchesBuiltinTypeError, - PluginParameterTypeReadOnlyLockError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() RESOURCE_TYPE: Final[str] = "plugin_task_parameter_type" @@ -107,8 +106,8 @@ def create( Raises: PluginParameterTypeMatchesBuiltinTypeError: If the plugin parameter type name matches a built-in type. - PluginParameterTypeAlreadyExistsError: If a plugin parameter type - with the given name already exists. + EntityExistsError: If a plugin parameter type with the given name already + exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -120,18 +119,13 @@ def create( ) raise PluginParameterTypeMatchesBuiltinTypeError - if ( - self._plugin_parameter_type_name_service.get( - name, group_id=group_id, log=log - ) - is not None - ): - log.debug( - "Plugin Parameter Type name already exists", - name=name, - group_id=group_id, + duplicate = self._plugin_parameter_type_name_service.get( + name, group_id=group_id, log=log + ) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id ) - raise PluginParameterTypeAlreadyExistsError group = self._group_id_service.get(group_id, error_if_not_found=True) @@ -247,8 +241,7 @@ def get( sort_column ) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise PluginParameterSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) plugin_parameter_types = list( db.session.scalars(plugin_parameter_types_stmt).all() @@ -319,7 +312,7 @@ def get( The plugin parameter type object if found, otherwise None. Raises: - PluginParameterTypeDoesNotExistError: If the plugin parameter type + EntityDoesNotExistError: If the plugin parameter type is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -342,11 +335,9 @@ def get( if plugin_parameter_type is None: if error_if_not_found: - log.debug( - "Plugin Parameter Type not found", - plugin_parameter_type_id=plugin_parameter_type_id, + raise EntityDoesNotExistError( + RESOURCE_TYPE, plugin_parameter_type_id=plugin_parameter_type_id ) - raise PluginParameterTypeDoesNotExistError return None @@ -393,9 +384,9 @@ def modify( The updated plugin parameter type object. Raises: - PluginParameterTypeDoesNotExistError: If the plugin parameter type + EntityDoesNotExistError: If the plugin parameter type is not found and `error_if_not_found` is True. - PluginParameterTypeAlreadyExistsError: If the plugin parameter type + EntityExistsError: If the plugin parameter type name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -411,12 +402,11 @@ def modify( group_id = plugin_parameter_type.resource.group_id if plugin_parameter_type.resource.is_readonly: - log.debug( - "The Plugin Parameter Type is read-only and cannot be modified", + raise ReadOnlyLockError( + RESOURCE_TYPE, plugin_parameter_type_id=plugin_parameter_type_id, name=plugin_parameter_type.name, ) - raise PluginParameterTypeReadOnlyLockError if name.strip().lower() in BUILTIN_TYPES: log.debug( @@ -426,19 +416,14 @@ def modify( ) raise PluginParameterTypeMatchesBuiltinTypeError - if ( - name != plugin_parameter_type.name - and self._plugin_parameter_type_name_service.get( + if name != plugin_parameter_type.name: + duplicate = self._plugin_parameter_type_name_service.get( name, group_id=group_id, log=log ) - is not None - ): - log.debug( - "Plugin Parameter Type name already exists", - name=name, - group_id=group_id, - ) - raise PluginParameterTypeAlreadyExistsError + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) new_plugin_parameter_type = models.PluginTaskParameterType( name=name, @@ -482,14 +467,14 @@ def delete(self, plugin_parameter_type_id: int, **kwargs) -> dict[str, Any]: plugin_parameter_type_resource = db.session.scalars(stmt).first() if plugin_parameter_type_resource is None: - raise PluginParameterTypeDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, plugin_parameter_type_id=plugin_parameter_type_id + ) if plugin_parameter_type_resource.is_readonly: - log.debug( - "The Plugin Parameter Type is read-only and cannot be deleted", - plugin_parameter_type_id=plugin_parameter_type_id, + raise ReadOnlyLockError( + RESOURCE_TYPE, plugin_parameter_type_id=plugin_parameter_type_id ) - raise PluginParameterTypeReadOnlyLockError deleted_resource_lock = models.ResourceLock( resource_lock_type=resource_lock_types.DELETE, @@ -530,7 +515,7 @@ def get( The plugin parameter type object if found, otherwise None. Raises: - PluginParameterTypeDoesNotExistError: If the plugin parameter type + EntityDoesNotExistError: If the plugin parameter type is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -555,8 +540,9 @@ def get( if plugin_parameter_type is None: if error_if_not_found: - log.debug("Plugin Parameter Type not found", name=name) - raise PluginParameterTypeDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, name=name, group_id=group_id + ) return None diff --git a/src/dioptra/restapi/v1/plugins/__init__.py b/src/dioptra/restapi/v1/plugins/__init__.py index 8b612d5e8..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/plugins/__init__.py +++ b/src/dioptra/restapi/v1/plugins/__init__.py @@ -14,7 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode - -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/plugins/errors.py b/src/dioptra/restapi/v1/plugins/errors.py deleted file mode 100644 index 4358d9c58..000000000 --- a/src/dioptra/restapi/v1/plugins/errors.py +++ /dev/null @@ -1,121 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the plugin endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class PluginAlreadyExistsError(Exception): - """The plugin name already exists.""" - - -class PluginDoesNotExistError(Exception): - """The requested plugin does not exist.""" - - -class PluginFileAlreadyExistsError(Exception): - """The plugin file filename already exists.""" - - -class PluginFileDoesNotExistError(Exception): - """The requested plugin file does not exist.""" - - -class PluginTaskParameterTypeNotFoundError(Exception): - """One or more referenced plugin task parameter types were not found.""" - - -class PluginTaskNameAlreadyExistsError(Exception): - """More than one plugin task is being assigned the same name.""" - - -class PluginTaskInputParameterNameAlreadyExistsError(Exception): - """More than one plugin task input parameter is being assigned the same name.""" - - -class PluginTaskOutputParameterNameAlreadyExistsError(Exception): - """More than one plugin task output parameter is being assigned the same name.""" - - -class PluginSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(PluginDoesNotExistError) - def handle_plugin_does_not_exist_error(error): - return {"message": "Not Found - The requested plugin does not exist"}, 404 - - @api.errorhandler(PluginAlreadyExistsError) - def handle_plugin_already_exists_error(error): - return ( - { - "message": "Bad Request - The plugin name on the registration form " - "already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(PluginFileDoesNotExistError) - def handle_plugin_file_does_not_exist_error(error): - return {"message": "Not Found - The requested plugin file does not exist"}, 404 - - @api.errorhandler(PluginFileAlreadyExistsError) - def handle_plugin_file_already_exists_error(error): - return ( - { - "message": "Bad Request - The plugin file filename on the " - "registration form already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(PluginTaskParameterTypeNotFoundError) - def handle_plugin_task_parameter_type_not_found_error(error): - return { - "message": "Bad Request - One or more referenced plugin task parameter " - "types were not found." - }, 400 - - @api.errorhandler(PluginTaskNameAlreadyExistsError) - def handle_plugin_task_name_already_exists_error(error): - return { - "message": "Bad Request - More than one plugin task is being assigned the " - "same name." - }, 400 - - @api.errorhandler(PluginTaskInputParameterNameAlreadyExistsError) - def handle_plugin_task_input_parameter_name_already_exists_error(error): - return { - "message": "Bad Request - More than one plugin task input parameter is " - "being assigned the same name." - }, 400 - - @api.errorhandler(PluginTaskOutputParameterNameAlreadyExistsError) - def handle_plugin_task_output_parameter_name_already_exists_error(error): - return { - "message": "Bad Request - More than one plugin task output parameter is " - "being assigned the same name." - }, 400 - - @api.errorhandler(PluginSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/plugins/service.py b/src/dioptra/restapi/v1/plugins/service.py index 9849f186c..fadb12ec4 100644 --- a/src/dioptra/restapi/v1/plugins/service.py +++ b/src/dioptra/restapi/v1/plugins/service.py @@ -29,23 +29,18 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import resource_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + QueryParameterNotUniqueError, + SortParameterValidationError, +) +from dioptra.restapi.utils import find_non_unique from dioptra.restapi.v1 import utils from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - PluginAlreadyExistsError, - PluginDoesNotExistError, - PluginFileAlreadyExistsError, - PluginFileDoesNotExistError, - PluginSortError, - PluginTaskInputParameterNameAlreadyExistsError, - PluginTaskNameAlreadyExistsError, - PluginTaskOutputParameterNameAlreadyExistsError, - PluginTaskParameterTypeNotFoundError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() PLUGIN_RESOURCE_TYPE: Final[str] = "plugin" @@ -105,13 +100,18 @@ def create( The newly created plugin object. Raises: - PluginAlreadyExistsError: If a plugin with the given name already exists. + EntityExistsError: If a plugin with the given name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._plugin_name_service.get(name, group_id=group_id, log=log) is not None: - log.debug("Plugin name already exists", name=name, group_id=group_id) - raise PluginAlreadyExistsError + duplicate = self._plugin_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + PLUGIN_RESOURCE_TYPE, + duplicate.resource_id, + name=name, + group_id=group_id, + ) group = self._group_id_service.get(group_id, error_if_not_found=True) @@ -222,10 +222,7 @@ def get( sort_column = sort_column.asc() latest_plugins_stmt = latest_plugins_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in PLUGIN_SORTABLE_FIELDS: - log.debug( - f"sort_by_string: '{sort_by_string}' is not in PLUGIN_SORTABLE_FIELDS" - ) - raise PluginSortError + raise SortParameterValidationError(PLUGIN_RESOURCE_TYPE, sort_by_string) plugins = db.session.scalars(latest_plugins_stmt).all() @@ -327,7 +324,7 @@ def get( The plugin object if found, otherwise None. Raises: - PluginDoesNotExistError: If the plugin is not found and `error_if_not_found` + EntityDoesNotExistError: If the plugin is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -347,8 +344,7 @@ def get( if plugin is None: if error_if_not_found: - log.debug("Plugin not found", plugin_id=plugin_id) - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) return None @@ -403,9 +399,9 @@ def modify( The updated plugin object. Raises: - PluginDoesNotExistError: If the plugin is not found and `error_if_not_found` + EntityDoesNotExistError: If the plugin is not found and `error_if_not_found` is True. - PluginAlreadyExistsError: If the plugin name already exists. + EntityExistsError: If the plugin name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -420,13 +416,15 @@ def modify( plugin_files = plugin_dict["plugin_files"] group_id = plugin.resource.group_id - if ( - name != plugin.name - and self._plugin_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Plugin name already exists", name=name, group_id=group_id) - raise PluginAlreadyExistsError + if name != plugin.name: + duplicate = self._plugin_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + PLUGIN_RESOURCE_TYPE, + duplicate.resource_id, + name=name, + group_id=group_id, + ) new_plugin = models.Plugin( name=name, @@ -466,7 +464,7 @@ def delete(self, plugin_id: int, **kwargs) -> dict[str, Any]: plugin_resource = db.session.scalar(stmt) if plugin_resource is None: - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) deleted_resource_lock = models.ResourceLock( resource_lock_type=resource_lock_types.DELETE, @@ -499,11 +497,9 @@ def get( A list of plugin objects. Raises: - PluginDoesNotExistError: If the plugin is not found and `error_if_not_found` + EntityDoesNotExistError: If the plugin is not found and `error_if_not_found` is True. """ - log: BoundLogger = kwargs.get("log", LOGGER.new()) - latest_plugins_stmt = ( select(models.Plugin) .join(models.Resource) @@ -520,8 +516,9 @@ def get( plugin_ids_missing = set(plugin_ids) - set( plugin.resource_id for plugin in plugins ) - log.debug("Plugin not found", plugin_ids=list(plugin_ids_missing)) - raise PluginDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_RESOURCE_TYPE, plugin_ids=list(plugin_ids_missing) + ) # extract list of plugin ids plugin_ids = [plugin.resource_id for plugin in plugins] @@ -609,7 +606,7 @@ def get( The plugin object if found, otherwise None. Raises: - PluginDoesNotExistError: If the plugin is not found and `error_if_not_found` + EntityDoesNotExistError: If the plugin is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -630,8 +627,9 @@ def get( if plugin is None: if error_if_not_found: - log.debug("Plugin not found", name=name) - raise PluginDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_RESOURCE_TYPE, name=name, group_id=group_id + ) return plugin @@ -654,7 +652,7 @@ def get( The plugin file object if found, otherwise None. Raises: - PluginFileDoesNotExistError: If the plugin is not found and + EntityDoesNotExistError: If the plugin is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -679,8 +677,9 @@ def get( if plugin_file is None: if error_if_not_found: - log.debug("Plugin file not found", filename=filename) - raise PluginFileDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_FILE_RESOURCE_TYPE, plugin_id=plugin_id, filename=filename + ) return None @@ -735,7 +734,7 @@ def create( The newly created plugin file object. Raises: - PluginFileAlreadyExistsError: If a plugin file with the given filename + EntityExistsError: If a plugin file with the given filename already exists. """ @@ -749,14 +748,16 @@ def create( ) # Validate that the proposed filename hasn't already been used in the plugin. - if ( - self._plugin_file_name_service.get(filename, plugin_id=plugin_id, log=log) - is not None - ): - log.debug( - "Plugin filename already exists", filename=filename, plugin_id=plugin_id + duplicate = self._plugin_file_name_service.get( + filename, plugin_id=plugin_id, log=log + ) + if duplicate is not None: + raise EntityExistsError( + PLUGIN_FILE_RESOURCE_TYPE, + duplicate.resource_id, + filename=filename, + plugin_id=plugin_id, ) - raise PluginFileAlreadyExistsError # The owner of the PluginFile resource must match the owner of the Plugin # resource. @@ -850,8 +851,7 @@ def get( plugin = db.session.scalar(latest_plugin_stmt) if plugin is None: - log.debug("Plugin not found", plugin_id=plugin_id) - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) latest_plugin_files_count_stmt = ( select(func.count(models.PluginFile.resource_id)) @@ -899,11 +899,9 @@ def get( sort_column = sort_column.asc() latest_plugin_files_stmt = latest_plugin_files_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in PLUGIN_FILE_SORTABLE_FIELDS: - log.debug( - f"sort_by_string: '{sort_by_string}' " - f"is not in PLUGIN_FILE_SORTABLE_FIELDS" + raise SortParameterValidationError( + PLUGIN_FILE_RESOURCE_TYPE, sort_by_string ) - raise PluginSortError plugin_files_dict: dict[int, utils.PluginFileDict] = { plugin_file.resource_id: utils.PluginFileDict( @@ -949,7 +947,7 @@ def delete(self, plugin_id: int, **kwargs) -> dict[str, Any]: plugin_resource = db.session.scalar(stmt) if plugin_resource is None: - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) latest_plugin_files_stmt = ( select(models.PluginFile) @@ -1010,7 +1008,7 @@ def get( The plugin object if found, otherwise None. Raises: - PluginDoesNotExistError: If the plugin or plugin file is not found and + EntityDoesNotExistError: If the plugin or plugin file is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -1032,8 +1030,7 @@ def get( if plugin is None: if error_if_not_found: - log.debug("Plugin not found", plugin_id=plugin_id) - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) return None @@ -1052,8 +1049,11 @@ def get( if plugin_file is None: if error_if_not_found: - log.debug("Plugin file not found", plugin_file_id=plugin_file_id) - raise PluginFileDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_FILE_RESOURCE_TYPE, + plugin_id=plugin_id, + plugin_file_id=plugin_file_id, + ) return None @@ -1102,7 +1102,7 @@ def modify( The updated plugin file object. Raises: - PluginDoesNotExistError: If the plugin or plugin file is not found and + EntityDoesNotExistError: If the plugin or plugin file is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -1120,17 +1120,17 @@ def modify( plugin = plugin_file_dict["plugin"] plugin_file = plugin_file_dict["plugin_file"] - if ( - filename != plugin_file.filename - and self._plugin_file_name_service.get( + if filename != plugin_file.filename: + duplicate = self._plugin_file_name_service.get( filename, plugin_id=plugin_id, log=log ) - is not None - ): - log.debug( - "Plugin filename already exists", filename=filename, plugin_id=plugin_id - ) - raise PluginFileAlreadyExistsError + if duplicate is not None: + raise EntityExistsError( + PLUGIN_FILE_RESOURCE_TYPE, + duplicate.resource_id, + filename=filename, + plugin_id=plugin_id, + ) updated_plugin_file = models.PluginFile( filename=filename, @@ -1173,7 +1173,7 @@ def delete(self, plugin_id: int, plugin_file_id: int, **kwargs) -> dict[str, Any plugin_resource = db.session.scalar(stmt) if plugin_resource is None: - raise PluginDoesNotExistError + raise EntityDoesNotExistError(PLUGIN_RESOURCE_TYPE, plugin_id=plugin_id) plugin_file_stmt = ( select(models.PluginFile) @@ -1189,7 +1189,11 @@ def delete(self, plugin_id: int, plugin_file_id: int, **kwargs) -> dict[str, Any plugin_file = db.session.scalar(plugin_file_stmt) if plugin_file is None: - raise PluginFileDoesNotExistError + raise EntityDoesNotExistError( + PLUGIN_FILE_RESOURCE_TYPE, + plugin_id=plugin_id, + plugin_file_id=plugin_file_id, + ) plugin_file_id_to_return = plugin_file.resource_id # to return to user db.session.add( @@ -1214,27 +1218,21 @@ def _construct_plugin_task( parameter_types_id_to_orm: dict[int, models.PluginTaskParameterType], log: BoundLogger, ) -> models.PluginTask: - input_param_names = [x["name"] for x in task["input_params"]] - unique_input_param_names = set(input_param_names) - - if len(unique_input_param_names) != len(input_param_names): - log.error( - "One or more input parameters have the same name", + duplicates = find_non_unique("name", task["input_params"]) + if len(duplicates) > 0: + raise QueryParameterNotUniqueError( + "plugin task input parameter", plugin_task_name=task["name"], - input_param_names=input_param_names, + input_param_names=duplicates, ) - raise PluginTaskInputParameterNameAlreadyExistsError - - output_param_names = [x["name"] for x in task["output_params"]] - unique_output_param_names = set(output_param_names) - if len(unique_output_param_names) != len(output_param_names): - log.error( - "One or more output parameters have the same name", + duplicates = find_non_unique("name", task["output_params"]) + if len(duplicates) > 0: + raise QueryParameterNotUniqueError( + "plugin task output parameter", plugin_task_name=task["name"], - output_param_names=output_param_names, + output_param_names=duplicates, ) - raise PluginTaskOutputParameterNameAlreadyExistsError input_parameters_list = [] for parameter_number, input_param in enumerate(task["input_params"]): @@ -1306,13 +1304,12 @@ def _get_referenced_parameter_types( if not len(parameter_types) == len(parameter_type_ids): returned_parameter_type_ids = set([x.resource_id for x in parameter_types]) ids_not_found = parameter_type_ids - returned_parameter_type_ids - log.error( - "One or more referenced plugin task parameter types were not found", + raise EntityDoesNotExistError( + "plugin task parameter types", num_expected=len(parameter_type_ids), num_found=len(parameter_types), ids_not_found=sorted(list(ids_not_found)), ) - raise PluginTaskParameterTypeNotFoundError return {x.resource_id: x for x in parameter_types} @@ -1323,12 +1320,9 @@ def _add_plugin_tasks( if not tasks: return None - task_names = [x["name"] for x in tasks] - unique_task_names = set(task_names) - - if len(unique_task_names) != len(tasks): - log.error("One or more tasks have the same name", task_names=task_names) - raise PluginTaskNameAlreadyExistsError + duplicates = find_non_unique("name", tasks) + if len(duplicates) > 0: + raise QueryParameterNotUniqueError("plugin task", task_names=duplicates) parameter_types_id_to_orm = _get_referenced_parameter_types(tasks, log=log) or {} for task in tasks: diff --git a/src/dioptra/restapi/v1/queues/__init__.py b/src/dioptra/restapi/v1/queues/__init__.py index f2c5735af..c68b10594 100644 --- a/src/dioptra/restapi/v1/queues/__init__.py +++ b/src/dioptra/restapi/v1/queues/__init__.py @@ -14,7 +14,6 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors from .controller import api -__all__ = ["api", "errors"] +__all__ = ["api"] diff --git a/src/dioptra/restapi/v1/queues/errors.py b/src/dioptra/restapi/v1/queues/errors.py deleted file mode 100644 index 12d4a3a41..000000000 --- a/src/dioptra/restapi/v1/queues/errors.py +++ /dev/null @@ -1,63 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the queue endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class QueueAlreadyExistsError(Exception): - """The queue name already exists.""" - - -class QueueDoesNotExistError(Exception): - """The requested queue does not exist.""" - - -class QueueLockedError(Exception): - """The requested queue is locked.""" - - -class QueueSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(QueueDoesNotExistError) - def handle_queue_does_not_exist_error(error): - return {"message": "Not Found - The requested queue does not exist"}, 404 - - @api.errorhandler(QueueLockedError) - def handle_queue_locked_error(error): - return {"message": "Forbidden - The requested queue is locked."}, 403 - - @api.errorhandler(QueueAlreadyExistsError) - def handle_queue_already_exists_error(error): - return ( - { - "message": "Bad Request - The queue name on the registration form " - "already exists. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(QueueSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/queues/service.py b/src/dioptra/restapi/v1/queues/service.py index ad21273a6..a9e552637 100644 --- a/src/dioptra/restapi/v1/queues/service.py +++ b/src/dioptra/restapi/v1/queues/service.py @@ -27,13 +27,16 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import resource_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + SortParameterValidationError, +) from dioptra.restapi.v1 import utils from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import QueueAlreadyExistsError, QueueDoesNotExistError, QueueSortError - LOGGER: BoundLogger = structlog.stdlib.get_logger() RESOURCE_TYPE: Final[str] = "queue" @@ -91,18 +94,20 @@ def create( The newly created queue object. Raises: - QueueAlreadyExistsError: If a queue with the given name already exists. - GroupDoesNotExistError: If the group with the provided ID does not exist. + EntityExistsError: If a queue with the given name already exists. + EntityDoesNotExistError: If the group with the provided ID does not exist. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._queue_name_service.get(name, group_id=group_id, log=log) is not None: - log.debug("Queue name already exists", name=name, group_id=group_id) - raise QueueAlreadyExistsError + duplicate = self._queue_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) group = self._group_id_service.get(group_id, error_if_not_found=True) - resource = models.Resource(resource_type="queue", owner=group) + resource = models.Resource(resource_type=RESOURCE_TYPE, owner=group) new_queue = models.Queue( name=name, description=description, resource=resource, creator=current_user ) @@ -202,8 +207,7 @@ def get( sort_column = sort_column.asc() queues_stmt = queues_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise QueueSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) queues = list(db.session.scalars(queues_stmt).all()) @@ -257,7 +261,7 @@ def get( The queue object if found, otherwise None. Raises: - QueueDoesNotExistError: If the queue is not found and `error_if_not_found` + EntityDoesNotExistError: If the queue is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -276,8 +280,7 @@ def get( if queue is None: if error_if_not_found: - log.debug("Queue not found", queue_id=queue_id) - raise QueueDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, queue_id=queue_id) return None @@ -318,9 +321,9 @@ def modify( The updated queue object. Raises: - QueueDoesNotExistError: If the queue is not found and `error_if_not_found` + EntityDoesNotExistError: If the queue is not found and `error_if_not_found` is True. - QueueAlreadyExistsError: If the queue name already exists. + EntityExistsError: If the queue name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -331,13 +334,12 @@ def modify( queue = queue_dict["queue"] group_id = queue.resource.group_id - if ( - name != queue.name - and self._queue_name_service.get(name, group_id=group_id, log=log) - is not None - ): - log.debug("Queue name already exists", name=name, group_id=group_id) - raise QueueAlreadyExistsError + if name != queue.name: + duplicate = self._queue_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.resource_id, name=name, group_id=group_id + ) new_queue = models.Queue( name=name, @@ -368,7 +370,7 @@ def delete(self, queue_id: int, **kwargs) -> dict[str, Any]: A dictionary reporting the status of the request. Raises: - QueueDoesNotExistError: If the queue is not found. + EntityDoesNotExistError: If the queue is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -378,7 +380,7 @@ def delete(self, queue_id: int, **kwargs) -> dict[str, Any]: queue_resource = db.session.scalars(stmt).first() if queue_resource is None: - raise QueueDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, queue_id=queue_id) deleted_resource_lock = models.ResourceLock( resource_lock_type=resource_lock_types.DELETE, @@ -411,7 +413,7 @@ def get( The queue object if found, otherwise None. Raises: - QueueDoesNotExistError: If the queue is not found and `error_if_not_found` + EntityDoesNotExistError: If the queue is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -432,8 +434,9 @@ def get( queue_ids_missing = set(queue_ids) - set( queue.resource_id for queue in queues ) - log.debug("Queue not found", queue_ids=list(queue_ids_missing)) - raise QueueDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, queue_ids=list(queue_ids_missing) + ) return queues @@ -460,7 +463,7 @@ def get( The queue object if found, otherwise None. Raises: - QueueDoesNotExistError: If the queue is not found and `error_if_not_found` + EntityDoesNotExistError: If the queue is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -480,8 +483,7 @@ def get( if queue is None: if error_if_not_found: - log.debug("Queue not found", name=name) - raise QueueDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, name=name) return None diff --git a/src/dioptra/restapi/v1/shared/drafts/service.py b/src/dioptra/restapi/v1/shared/drafts/service.py index 94e22115a..7af3c5bd5 100644 --- a/src/dioptra/restapi/v1/shared/drafts/service.py +++ b/src/dioptra/restapi/v1/shared/drafts/service.py @@ -31,9 +31,8 @@ BackendDatabaseError, DraftAlreadyExistsError, DraftDoesNotExistError, - ResourceDoesNotExistError, + EntityDoesNotExistError, ) -from dioptra.restapi.v1.groups.errors import GroupDoesNotExistError from dioptra.restapi.v1.groups.service import GroupIdService LOGGER: BoundLogger = structlog.stdlib.get_logger() @@ -159,7 +158,7 @@ def create( The newly created draft object. Raises: - GroupDoesNotExistError: If the group with the provided ID does not exist. + EntityDoesNotExistError: If the group with the provided ID does not exist. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -173,7 +172,9 @@ def create( ) resource = db.session.scalar(stmt) if resource is None: - raise GroupDoesNotExistError + raise EntityDoesNotExistError( + self._resource_type, resource_id=base_resource_id + ) group = resource.owner draft_payload = { @@ -407,11 +408,11 @@ def create( resource = db.session.scalars(stmt).first() if resource is None: - raise ResourceDoesNotExistError + raise EntityDoesNotExistError(self._resource_type, resource_id=resource_id) existing_draft, num_other_drafts = self.get(resource_id, log=log) if existing_draft: - raise DraftAlreadyExistsError + raise DraftAlreadyExistsError(self._resource_type, resource_id) draft_payload = { "resource_data": payload, diff --git a/src/dioptra/restapi/v1/shared/snapshots/service.py b/src/dioptra/restapi/v1/shared/snapshots/service.py index 5301a0033..e2769c393 100644 --- a/src/dioptra/restapi/v1/shared/snapshots/service.py +++ b/src/dioptra/restapi/v1/shared/snapshots/service.py @@ -25,7 +25,7 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import BackendDatabaseError, ResourceDoesNotExistError +from dioptra.restapi.errors import BackendDatabaseError, EntityDoesNotExistError from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters LOGGER: BoundLogger = structlog.stdlib.get_logger() @@ -74,7 +74,7 @@ def get( None. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -87,8 +87,9 @@ def get( if resource is None: if error_if_not_found: - log.debug("Resource not found", resource_id=resource_id) - raise ResourceDoesNotExistError + raise EntityDoesNotExistError( + self._resource_type, resource_id=resource_id + ) return None @@ -176,7 +177,7 @@ def get( The requested snapshot the resource object if found, otherwise None. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -189,8 +190,9 @@ def get( if resource is None: if error_if_not_found: - log.debug("Resource not found", resource_id=resource_id) - raise ResourceDoesNotExistError + raise EntityDoesNotExistError( + self._resource_type, resource_id=resource_id + ) return None @@ -207,8 +209,9 @@ def get( if snapshot is None: if error_if_not_found: - log.debug("Resource snapshot not found", snapshot_id=snapshot_id) - raise ResourceDoesNotExistError + raise EntityDoesNotExistError( + self._resource_type + "_snapshot", snapshot_id=snapshot_id + ) return None diff --git a/src/dioptra/restapi/v1/shared/tags/service.py b/src/dioptra/restapi/v1/shared/tags/service.py index 1cb0e4569..c3c181e44 100644 --- a/src/dioptra/restapi/v1/shared/tags/service.py +++ b/src/dioptra/restapi/v1/shared/tags/service.py @@ -25,8 +25,8 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import ResourceDoesNotExistError -from dioptra.restapi.v1.tags.errors import TagDoesNotExistError +from dioptra.restapi.errors import EntityDoesNotExistError +from dioptra.restapi.v1.tags.service import RESOURCE_TYPE as TAG_RESOURCE_TYPE from dioptra.restapi.v1.tags.service import TagIdService LOGGER: BoundLogger = structlog.stdlib.get_logger() @@ -69,7 +69,7 @@ def get( The list of tags if the resource is found, otherwise None. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -100,7 +100,7 @@ def append( The updated tag resource object. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. TagDoesNotExistError: If one or more tags are not found. """ @@ -147,9 +147,9 @@ def modify( The updated tag resource object. Raises: - ResourceDoesNotExistError: If the resource is not found and + EntityDoesNotExistError: If the resource is not found and `error_if_not_found` is True. - TagDoesNotExistError: If one or more tags are not found. + EntityDoesNotExistError: If one or more tags are not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -232,7 +232,9 @@ def delete(self, resource_id: int, tag_id, **kwargs) -> dict[str, Any]: current_tags = resource.tags tag_exists = tag_id in {tag.tag_id for tag in current_tags} if not tag_exists: - raise TagDoesNotExistError + raise EntityDoesNotExistError( + TAG_RESOURCE_TYPE, resource_id=resource_id, tag_id=tag_id + ) resource.tags = [tag for tag in current_tags if tag.tag_id != tag_id] @@ -259,7 +261,6 @@ def get(self, resource_id: int, **kwargs) -> models.Resource: resource = db.session.scalar(stmt) if resource is None: - log.debug(f"{self._resource_type} not found", resource_id=resource_id) - raise ResourceDoesNotExistError + raise EntityDoesNotExistError(self._resource_type, resource_id=resource_id) return resource diff --git a/src/dioptra/restapi/v1/tags/__init__.py b/src/dioptra/restapi/v1/tags/__init__.py index 11ce655e6..ab0a41a34 100644 --- a/src/dioptra/restapi/v1/tags/__init__.py +++ b/src/dioptra/restapi/v1/tags/__init__.py @@ -14,6 +14,3 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/tags/errors.py b/src/dioptra/restapi/v1/tags/errors.py deleted file mode 100644 index 4ff36b234..000000000 --- a/src/dioptra/restapi/v1/tags/errors.py +++ /dev/null @@ -1,49 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the tag endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class TagAlreadyExistsError(Exception): - """The tag name already exists.""" - - -class TagDoesNotExistError(Exception): - """The requested tag does not exist.""" - - -class TagSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(TagDoesNotExistError) - def handle_tag_does_not_exist_error(error): - return {"message": "Not Found - The requested tag does not exist"}, 404 - - @api.errorhandler(TagAlreadyExistsError) - def handle_tag_already_exists_error(error): - return {"message": "Bad Request - The tag name already exists."}, 400 - - @api.errorhandler(TagSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, - ) diff --git a/src/dioptra/restapi/v1/tags/service.py b/src/dioptra/restapi/v1/tags/service.py index 663b5f013..a81a905c9 100644 --- a/src/dioptra/restapi/v1/tags/service.py +++ b/src/dioptra/restapi/v1/tags/service.py @@ -27,14 +27,18 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + SortParameterValidationError, +) from dioptra.restapi.v1.groups.service import GroupIdService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import TagAlreadyExistsError, TagDoesNotExistError, TagSortError - LOGGER: BoundLogger = structlog.stdlib.get_logger() +RESOURCE_TYPE: Final[str] = "tag" SEARCHABLE_FIELDS: Final[dict[str, Any]] = { "name": lambda x: models.Tag.name.like(x), } @@ -85,14 +89,16 @@ def create( The newly created tag object. Raises: - TagAlreadyExistsError: If a tag with the given name already exists. - GroupDoesNotExistError: If the group with the provided ID does not exist. + EntityExistsError: If a tag with the given name already exists. + EntityDoesNotExistError: If the group with the provided ID does not exist. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) - if self._tag_name_service.get(name, group_id=group_id, log=log) is not None: - log.debug("Tag name already exists", name=name, group_id=group_id) - raise TagAlreadyExistsError + duplicate = self._tag_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.tag_id, name=name, group_id=group_id + ) group = self._group_id_service.get(group_id, error_if_not_found=True) @@ -172,8 +178,7 @@ def get( sort_column = sort_column.asc() tags_stmt = tags_stmt.order_by(sort_column) elif sort_by_string and sort_by_string not in SORTABLE_FIELDS: - log.debug(f"sort_by_string: '{sort_by_string}' is not in SORTABLE_FIELDS") - raise TagSortError + raise SortParameterValidationError(RESOURCE_TYPE, sort_by_string) tags = list(db.session.scalars(tags_stmt).all()) @@ -214,7 +219,7 @@ def get( The tag object if found, otherwise None. Raises: - TagDoesNotExistError: If the tag is not found and `error_if_not_found` + EntityDoesNotExistError: If the tag is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -225,8 +230,7 @@ def get( if tag is None: if error_if_not_found: - log.debug("Tag not found", tag_id=tag_id) - raise TagDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, tag_id=tag_id) return None @@ -254,9 +258,9 @@ def modify( The updated tag object. Raises: - TagDoesNotExistError: If the tag is not found and `error_if_not_found` + EntityDoesNotExistError: If the tag is not found and `error_if_not_found` is True. - TagAlreadyExistsError: If the tag name already exists. + EntityExistsError: If the tag name already exists. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -266,12 +270,12 @@ def modify( return None group_id = tag.group_id - if ( - name != tag.name - and self._tag_name_service.get(name, group_id=group_id, log=log) is not None - ): - log.debug("Tag name already exists", name=name, group_id=group_id) - raise TagAlreadyExistsError + if name != tag.name: + duplicate = self._tag_name_service.get(name, group_id=group_id, log=log) + if duplicate is not None: + raise EntityExistsError( + RESOURCE_TYPE, duplicate.tag_id, name=name, group_id=group_id + ) current_timestamp = datetime.datetime.now(tz=datetime.timezone.utc) tag.name = name @@ -301,7 +305,7 @@ def delete( The tag object if found, otherwise None. Raises: - TagDoesNotExistError: If the tag is not found. + EntityDoesNotExistError: If the tag is not found. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) log.debug("Get tag by id", tag_id=tag_id) @@ -310,8 +314,7 @@ def delete( tag = db.session.scalar(stmt) if tag is None: - log.debug("Tag not found", tag_id=tag_id) - raise TagDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, tag_id=tag_id) tag_id = tag.tag_id db.session.delete(tag) @@ -360,7 +363,7 @@ def get( The tag object if found, otherwise None. Raises: - TagDoesNotExistError: If the tag is not found and `error_if_not_found` + EntityDoesNotExistError: If the tag is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -371,8 +374,7 @@ def get( if tag is None: if error_if_not_found: - log.debug("Tag not found", tag_id=tag_id) - raise TagDoesNotExistError + raise EntityDoesNotExistError(RESOURCE_TYPE, tag_id=tag_id) return None @@ -430,7 +432,7 @@ def get( The tag object if found, otherwise None. Raises: - TagDoesNotExistError: If the tag is not found and `error_if_not_found` + EntityDoesNotExistError: If the tag is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -444,8 +446,9 @@ def get( if tag is None: if error_if_not_found: - log.debug("Tag not found", name=name) - raise TagDoesNotExistError + raise EntityDoesNotExistError( + RESOURCE_TYPE, name=name, group_id=group_id + ) return None diff --git a/src/dioptra/restapi/v1/users/__init__.py b/src/dioptra/restapi/v1/users/__init__.py index 0491fe75b..ff72e5b3b 100644 --- a/src/dioptra/restapi/v1/users/__init__.py +++ b/src/dioptra/restapi/v1/users/__init__.py @@ -15,6 +15,3 @@ # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode """The users endpoint subpackage.""" -from . import errors - -__all__ = ["errors"] diff --git a/src/dioptra/restapi/v1/users/errors.py b/src/dioptra/restapi/v1/users/errors.py deleted file mode 100644 index 12ab460f9..000000000 --- a/src/dioptra/restapi/v1/users/errors.py +++ /dev/null @@ -1,115 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the user endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class NoCurrentUserError(Exception): - """There is no currently logged-in user.""" - - -class UserPasswordChangeError(Exception): - """Password change failed.""" - - -class UserPasswordChangeSamePasswordError(Exception): - """Password change failed.""" - - -class UserPasswordExpiredError(Exception): - """Password expired.""" - - -class UserPasswordVerificationError(Exception): - """Password verification failed.""" - - -class UsernameNotAvailableError(Exception): - """The username is not available.""" - - -class UserEmailNotAvailableError(Exception): - """The email address is not available.""" - - -class UserDoesNotExistError(Exception): - """The requested user does not exist.""" - - -class UserRegistrationError(Exception): - """The user registration form contains invalid parameters.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(NoCurrentUserError) - def handle_no_current_user_error(error): - return {"message": "There is no currently logged-in user"}, 401 - - @api.errorhandler(UserPasswordChangeError) - def handle_user_password_change_error_error(error): - return {"message": "Password Change Failed"}, 403 - - @api.errorhandler(UserPasswordChangeSamePasswordError) - def handle_user_password_change_same_error_error(error): - return { - "message": "Password Change Failed - The provided password matches" - "the existing password. Please provide a different password." - }, 403 - - @api.errorhandler(UserPasswordExpiredError) - def handle_user_password_expired_error(error): - return {"message": "Password expired."}, 401 - - @api.errorhandler(UserPasswordVerificationError) - def handle_user_password_verification_error_error(error): - return {"message": "Password Verification Failed"}, 403 - - @api.errorhandler(UserDoesNotExistError) - def handle_user_does_not_exist_error(error): - return {"message": "Not Found - The requested user does not exist"}, 404 - - @api.errorhandler(UsernameNotAvailableError) - def handle_username_not_available_error(error): - return ( - { - "message": "Bad Request - The username on the registration form " - "is not available. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(UserEmailNotAvailableError) - def handle_email_not_available_error(error): - return ( - { - "message": "Bad Request - The email on the registration form " - "is not available. Please select another and resubmit." - }, - 400, - ) - - @api.errorhandler(UserRegistrationError) - def handle_user_registration_error(error): - return ( - { - "message": "Bad Request - The user registration form contains " - "invalid parameters. Please verify and resubmit." - }, - 400, - ) diff --git a/src/dioptra/restapi/v1/users/service.py b/src/dioptra/restapi/v1/users/service.py index eacda9a91..7f425059c 100644 --- a/src/dioptra/restapi/v1/users/service.py +++ b/src/dioptra/restapi/v1/users/service.py @@ -29,7 +29,15 @@ from dioptra.restapi.db import db, models from dioptra.restapi.db.models.constants import user_lock_types -from dioptra.restapi.errors import BackendDatabaseError +from dioptra.restapi.errors import ( + BackendDatabaseError, + EntityDoesNotExistError, + EntityExistsError, + NoCurrentUserError, + QueryParameterValidationError, + UserPasswordChangeError, + UserPasswordError, +) from dioptra.restapi.v1.groups.service import GroupMemberService, GroupNameService from dioptra.restapi.v1.plugin_parameter_types.service import ( BuiltinPluginParameterTypeService, @@ -37,18 +45,6 @@ from dioptra.restapi.v1.shared.password_service import PasswordService from dioptra.restapi.v1.shared.search_parser import construct_sql_query_filters -from .errors import ( - NoCurrentUserError, - UserDoesNotExistError, - UserEmailNotAvailableError, - UsernameNotAvailableError, - UserPasswordChangeError, - UserPasswordChangeSamePasswordError, - UserPasswordExpiredError, - UserPasswordVerificationError, - UserRegistrationError, -) - LOGGER: BoundLogger = structlog.stdlib.get_logger() DEFAULT_GROUP_NAME: Final[str] = "public" @@ -128,17 +124,19 @@ def create( log: BoundLogger = kwargs.get("log", LOGGER.new()) if password != confirm_password: - raise UserRegistrationError( - "The password and confirmation password did not match." + raise QueryParameterValidationError( + "password", "equivalence", password="***", confirmation="***" ) - if self._user_name_service.get(username, log=log) is not None: - log.debug("Username already exists", username=username) - raise UsernameNotAvailableError + duplicate = self._user_name_service.get(username, log=log) + if duplicate is not None: + raise EntityExistsError("User", duplicate.user_id, username=username) - if self._get_user_by_email(email_address, log=log) is not None: - log.debug("Email already exists", email_address=email_address) - raise UserEmailNotAvailableError + duplicate = self._get_user_by_email(email_address, log=log) + if duplicate is not None: + raise EntityExistsError( + "User", duplicate.user_id, email_address=email_address + ) hashed_password = self._user_password_service.hash(password, log=log) new_user: models.User = models.User( @@ -237,7 +235,7 @@ def _get_user_by_email( The user object if found, otherwise None. Raises: - UserDoesNotExistError: If the user is not found and `error_if_not_found` + EntityDoesNotExistError: If the user is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -250,8 +248,7 @@ def _get_user_by_email( if user is None: if error_if_not_found: - log.debug("User not found", email_address=email_address) - raise UserDoesNotExistError + raise EntityDoesNotExistError("User", email_address=email_address) return None @@ -317,7 +314,7 @@ def get( The user object if found, otherwise None. Raises: - UserDoesNotExistError: If the user is not found and `error_if_not_found` + EntityDoesNotExistError: If the user is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -328,8 +325,7 @@ def get( if user is None: if error_if_not_found: - log.debug("User not found", user_id=user_id) - raise UserDoesNotExistError + raise EntityDoesNotExistError("User", user_id=user_id) return None @@ -399,10 +395,9 @@ def get(self, **kwargs) -> models.User: Raises: NoCurrentUserError: If there is no current user. """ - log: BoundLogger = kwargs.get("log", LOGGER.new()) + log: BoundLogger = kwargs.get("log", LOGGER.new()) # noqa: F841 if not current_user.is_authenticated: - log.debug("There is no current user.") raise NoCurrentUserError return cast(models.User, current_user) @@ -527,7 +522,7 @@ def get( The user object if found, otherwise None. Raises: - UserDoesNotExistError: If the user is not found and `error_if_not_found` + EntityDoesNotExistError: If the user is not found and `error_if_not_found` is True. """ log: BoundLogger = kwargs.get("log", LOGGER.new()) @@ -538,8 +533,7 @@ def get( if user is None: if error_if_not_found: - log.debug("User not found", username=username) - raise UserDoesNotExistError + raise EntityDoesNotExistError("User", username=username) return None @@ -595,12 +589,10 @@ def authenticate( ) if not authenticated and error_if_failed: - log.debug("Password authentication failed.") - raise UserPasswordVerificationError + raise UserPasswordError("Password authentication failed.") if expiration_date < current_timestamp: - log.debug("Password expired") - raise UserPasswordExpiredError + raise UserPasswordError("Password expired.") return authenticated @@ -637,15 +629,17 @@ def change( if not self._password_service.verify( password=current_password, hashed_password=str(user.password), log=log ): - raise UserPasswordChangeError + raise UserPasswordChangeError("Invalid Current Password.") if new_password != confirm_new_password: - raise UserPasswordChangeError + raise UserPasswordChangeError( + "Confirmation password does not match new password." + ) if self._password_service.verify( password=new_password, hashed_password=str(user.password), log=log ): - raise UserPasswordChangeSamePasswordError + raise UserPasswordChangeError("New password matches old password.") timestamp = datetime.datetime.now(tz=datetime.timezone.utc) user.password = self._password_service.hash(password=new_password, log=log) diff --git a/src/dioptra/restapi/v1/workflows/errors.py b/src/dioptra/restapi/v1/workflows/errors.py deleted file mode 100644 index c5723ec4a..000000000 --- a/src/dioptra/restapi/v1/workflows/errors.py +++ /dev/null @@ -1,41 +0,0 @@ -# This Software (Dioptra) is being made available as a public service by the -# National Institute of Standards and Technology (NIST), an Agency of the United -# States Department of Commerce. This software was developed in part by employees of -# NIST and in part by NIST contractors. Copyright in portions of this software that -# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant -# to Title 17 United States Code Section 105, works of NIST employees are not -# subject to copyright protection in the United States. However, NIST may hold -# international copyright in software created by its employees and domestic -# copyright (or licensing rights) in portions of software that were assigned or -# licensed to NIST. To the extent that NIST holds copyright in this software, it is -# being made available under the Creative Commons Attribution 4.0 International -# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts -# of the software developed or licensed by NIST. -# -# ACCESS THE FULL CC BY 4.0 LICENSE HERE: -# https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the workflows endpoints.""" -from __future__ import annotations - -from flask_restx import Api - - -class JobEntryPointDoesNotExistError(Exception): - """The job's entry point does not exist.""" - - -class JobExperimentDoesNotExistError(Exception): - """The experiment associated with the job does not exist.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(JobEntryPointDoesNotExistError) - def handle_experiment_job_does_not_exist_error(error): - return {"message": "Not Found - The job's entry point does not exist"}, 404 - - @api.errorhandler(JobExperimentDoesNotExistError) - def handle_experiment_does_not_exist_error(error): - return { - "message": "Not Found - The experiment associated with the job does not " - "exist" - }, 404 diff --git a/src/dioptra/restapi/v1/workflows/lib/views.py b/src/dioptra/restapi/v1/workflows/lib/views.py index 57f273ade..d4bf3340d 100644 --- a/src/dioptra/restapi/v1/workflows/lib/views.py +++ b/src/dioptra/restapi/v1/workflows/lib/views.py @@ -19,8 +19,13 @@ from structlog.stdlib import BoundLogger from dioptra.restapi.db import db, models - -from ..errors import JobEntryPointDoesNotExistError +from dioptra.restapi.errors import EntityDoesNotExistError +from dioptra.restapi.v1.entrypoints.service import ( + RESOURCE_TYPE as ENTRYPONT_RESOURCE_TYPE, +) +from dioptra.restapi.v1.experiments.service import ( + RESOURCE_TYPE as EXPERIMENT_RESOURCE_TYPE, +) LOGGER: BoundLogger = structlog.stdlib.get_logger() @@ -48,11 +53,7 @@ def get_entry_point( entry_point = db.session.scalar(entry_point_stmt) if entry_point is None: - log.debug( - "The job's entrypoint does not exist", - job_id=job_id, - ) - raise JobEntryPointDoesNotExistError + raise EntityDoesNotExistError(ENTRYPONT_RESOURCE_TYPE, job_id=job_id) return entry_point @@ -78,11 +79,7 @@ def get_experiment(job_id: int, logger: BoundLogger | None = None) -> models.Exp experiment = db.session.scalar(experiment_stmt) if experiment is None: - log.debug( - "The experiment associated with the job does not exist", - job_id=job_id, - ) - raise JobEntryPointDoesNotExistError + raise EntityDoesNotExistError(EXPERIMENT_RESOURCE_TYPE, job_id=job_id) return experiment diff --git a/src/dioptra/restapi/v1/workflows/service.py b/src/dioptra/restapi/v1/workflows/service.py index 77be2e77e..d5769e274 100644 --- a/src/dioptra/restapi/v1/workflows/service.py +++ b/src/dioptra/restapi/v1/workflows/service.py @@ -20,7 +20,8 @@ import structlog from structlog.stdlib import BoundLogger -from .lib import package_job_files, views +from .lib import views +from .lib.package_job_files import package_job_files from .schema import FileTypes LOGGER: BoundLogger = structlog.stdlib.get_logger() diff --git a/tests/unit/restapi/lib/actions.py b/tests/unit/restapi/lib/actions.py index 93e5d78b0..95ccb48cc 100644 --- a/tests/unit/restapi/lib/actions.py +++ b/tests/unit/restapi/lib/actions.py @@ -227,7 +227,7 @@ def register_tag( def register_artifact( client: FlaskClient, - uri: int, + uri: str, job_id: int, group_id: int, description: str | None = None, diff --git a/src/dioptra/restapi/v1/artifacts/errors.py b/tests/unit/restapi/test_utils.py similarity index 51% rename from src/dioptra/restapi/v1/artifacts/errors.py rename to tests/unit/restapi/test_utils.py index 169617cad..513a2e62e 100644 --- a/src/dioptra/restapi/v1/artifacts/errors.py +++ b/tests/unit/restapi/test_utils.py @@ -14,42 +14,53 @@ # # ACCESS THE FULL CC BY 4.0 LICENSE HERE: # https://creativecommons.org/licenses/by/4.0/legalcode -"""Error handlers for the artifact endpoints.""" from __future__ import annotations -from flask_restx import Api +import pytest +from dioptra.restapi.utils import find_non_unique -class ArtifactAlreadyExistsError(Exception): - """The queue name already exists.""" +def test_find_non_unique(): + assert ( + len( + find_non_unique( + "name", + [ + {"name": "hello", "foo": "bar"}, + {"name": "goodbye", "bar": "foo"}, + {"name": "hello", "lo": "behold"}, + ], + ) + ) + == 1 + ) -class ArtifactDoesNotExistError(Exception): - """The requested artifact does not exist.""" - - -class ArtifactSortError(Exception): - """The requested sortBy column is not a sortable field.""" - - -def register_error_handlers(api: Api) -> None: - @api.errorhandler(ArtifactDoesNotExistError) - def handle_artifact_does_not_exist_error(error): - return {"message": "Not Found - The requested artifact does not exist"}, 404 - - @api.errorhandler(ArtifactAlreadyExistsError) - def handle_artifact_already_exists_error(error): - return ( - { - "message": "Bad Request - The artifact uri on the registration form " - "already exists. Please select another and resubmit." - }, - 400, + assert ( + len( + find_non_unique( + "name", + [ + {"name": "hello", "foo": "bar"}, + {"name": "goodbye", "bar": "none"}, + {"name": "today", "lo": "hi"}, + ], + ) ) + == 0 + ) - @api.errorhandler(ArtifactSortError) - def handle_queue_sort_error(error): - return ( - {"message": "Bad Request - This column can not be sorted."}, - 400, + assert ( + len( + find_non_unique( + "name", + [ + {"name": "hello", "foo": "bar"}, + {"name": "goodbye", "bar": "none"}, + {"name": "hello", "lo": "behold"}, + {"name": "goodbye", "lo": "behold"}, + ], + ) ) + == 2 + ) diff --git a/tests/unit/restapi/v1/test_artifact.py b/tests/unit/restapi/v1/test_artifact.py index 32e6212e5..5c57008a2 100644 --- a/tests/unit/restapi/v1/test_artifact.py +++ b/tests/unit/restapi/v1/test_artifact.py @@ -216,7 +216,7 @@ def assert_registering_existing_artifact_uri_fails( group_id=group_id, job_id=job_id, ) - assert response.status_code == 400 + assert response.status_code == 409 # -- Tests ----------------------------------------------------------------------------- diff --git a/tests/unit/restapi/v1/test_entrypoint.py b/tests/unit/restapi/v1/test_entrypoint.py index 7e9189101..34d83622f 100644 --- a/tests/unit/restapi/v1/test_entrypoint.py +++ b/tests/unit/restapi/v1/test_entrypoint.py @@ -316,7 +316,7 @@ def assert_registering_existing_entrypoint_name_fails( plugin_ids=plugin_ids, queue_ids=queue_ids, ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_entrypoint_name_matches_expected_name( @@ -442,7 +442,7 @@ def assert_entrypoint_must_have_unique_param_names( plugin_ids=plugin_ids, queue_ids=queue_ids, ) - assert response.status_code == 400 + assert response.status_code == 409 # -- Tests ----------------------------------------------------------------------------- diff --git a/tests/unit/restapi/v1/test_model.py b/tests/unit/restapi/v1/test_model.py index ee2ba1811..f29e48033 100644 --- a/tests/unit/restapi/v1/test_model.py +++ b/tests/unit/restapi/v1/test_model.py @@ -317,7 +317,7 @@ def assert_registering_existing_model_name_fails( response = actions.register_model( client, name=name, description="", group_id=group_id ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_model_name_matches_expected_name( @@ -383,7 +383,7 @@ def assert_cannot_rename_model_with_existing_name( new_name=existing_name, new_description=existing_description, ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_retrieving_model_version_by_version_number_works( diff --git a/tests/unit/restapi/v1/test_plugin.py b/tests/unit/restapi/v1/test_plugin.py index edbe1157a..4fc6ef617 100644 --- a/tests/unit/restapi/v1/test_plugin.py +++ b/tests/unit/restapi/v1/test_plugin.py @@ -345,7 +345,7 @@ def assert_registering_existing_plugin_name_fails( response = actions.register_plugin( client, name=name, description="", group_id=group_id ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_plugin_name_matches_expected_name( @@ -411,7 +411,7 @@ def assert_cannot_rename_plugin_with_existing_name( new_name=existing_name, new_description=existing_description, ) - assert response.status_code == 400 + assert response.status_code == 409 # -- Assertions Plugin Files ----------------------------------------------------------- @@ -620,7 +620,7 @@ def assert_registering_existing_plugin_filename_fails( contents=contents, description=description, ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_plugin_filename_matches_expected_name( @@ -678,7 +678,7 @@ def assert_cannot_rename_plugin_file_with_existing_name( new_description=existing_description, new_contents=existing_contents, ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_plugin_file_is_not_found( diff --git a/tests/unit/restapi/v1/test_plugin_parameter_type.py b/tests/unit/restapi/v1/test_plugin_parameter_type.py index 4d5f9fd3b..8553513ca 100644 --- a/tests/unit/restapi/v1/test_plugin_parameter_type.py +++ b/tests/unit/restapi/v1/test_plugin_parameter_type.py @@ -356,7 +356,7 @@ def assert_cannot_rename_plugin_parameter_type_to_existing_name( new_structure=new_structure, new_description=new_description, ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_cannot_delete_invalid_plugin_parameter_type( diff --git a/tests/unit/restapi/v1/test_queue.py b/tests/unit/restapi/v1/test_queue.py index fff422f18..79d6ab688 100644 --- a/tests/unit/restapi/v1/test_queue.py +++ b/tests/unit/restapi/v1/test_queue.py @@ -260,7 +260,7 @@ def assert_registering_existing_queue_name_fails( response = actions.register_queue( client, name=name, description="", group_id=group_id ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_queue_name_matches_expected_name( @@ -352,7 +352,7 @@ def assert_cannot_rename_queue_with_existing_name( new_name=existing_name, new_description=existing_description, ) - assert response.status_code == 400 + assert response.status_code == 409 # -- Tests ----------------------------------------------------------------------------- diff --git a/tests/unit/restapi/v1/test_tag.py b/tests/unit/restapi/v1/test_tag.py index 1f1735323..6ca000900 100644 --- a/tests/unit/restapi/v1/test_tag.py +++ b/tests/unit/restapi/v1/test_tag.py @@ -227,7 +227,7 @@ def assert_registering_existing_tag_name_fails( AssertionError: If the response status code is not 400. """ response = actions.register_tag(client, name=name, group_id=group_id) - assert response.status_code == 400 + assert response.status_code == 409 def assert_tag_name_matches_expected_name( @@ -291,7 +291,7 @@ def assert_cannot_rename_tag_with_existing_name( tag_id=tag_id, new_name=existing_name, ) - assert response.status_code == 400 + assert response.status_code == 409 # -- Tests ----------------------------------------------------------------------------- diff --git a/tests/unit/restapi/v1/test_user.py b/tests/unit/restapi/v1/test_user.py index 9dbc42c38..48c866ebd 100644 --- a/tests/unit/restapi/v1/test_user.py +++ b/tests/unit/restapi/v1/test_user.py @@ -287,7 +287,7 @@ def assert_registering_existing_username_fails( response = actions.register_user( client, existing_username, non_existing_email, password ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_registering_existing_email_fails( @@ -308,7 +308,7 @@ def assert_registering_existing_email_fails( response = actions.register_user( client, non_existing_username, existing_email, password ) - assert response.status_code == 400 + assert response.status_code == 409 def assert_user_username_matches_expected_name( @@ -463,9 +463,9 @@ def assert_login_is_unauthorized( password: The password of the user to be logged in. Raises: - AssertionError: If the response status code is not 403. + AssertionError: If the response status code is not 401. """ - assert actions.login(client, username, password).status_code == 403 + assert actions.login(client, username, password).status_code == 401 def assert_new_password_cannot_be_existing(