diff --git a/amt/api/http_browser_caching.py b/amt/api/http_browser_caching.py index e9b29e9e4..dcdf78c28 100644 --- a/amt/api/http_browser_caching.py +++ b/amt/api/http_browser_caching.py @@ -11,6 +11,8 @@ from starlette.staticfiles import StaticFiles from starlette.types import Scope +from amt.core.exceptions import AMTNotFound, AMTOnlyStatic + class StaticFilesCache(StaticFiles): def __init__( @@ -62,17 +64,17 @@ class URLComponents(NamedTuple): @lru_cache(maxsize=1000) def url_for_cache(name: str, /, **path_params: str) -> str: if name != "static": - raise ValueError("Only static files are supported.") + raise AMTOnlyStatic() url_parts: ParseResult = urllib.parse.urlparse(path_params["path"]) # type: ignore if url_parts.scheme or url_parts.hostname: # type: ignore - raise ValueError("Only local URLS are supported.") + raise AMTOnlyStatic() query_list: dict[str, str] = dict(x.split("=") for x in url_parts.query.split("&")) if url_parts.query else {} # type: ignore resolved_url_path: str = "/" + name + "/" + url_parts.path # type: ignore _, stat_result = static_files.lookup_path(url_parts.path) # type: ignore if not stat_result: - raise ValueError(f"Static file {url_parts.path} not found.") # type: ignore + raise AMTNotFound() etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size) etag = f"{md5_hexdigest(etag_base.encode(), usedforsecurity=False)}" diff --git a/amt/api/routes/project.py b/amt/api/routes/project.py index ca4228d6c..9d49ad08c 100644 --- a/amt/api/routes/project.py +++ b/amt/api/routes/project.py @@ -12,7 +12,7 @@ resolve_base_navigation_items, resolve_sub_menu, ) -from amt.core.exceptions import NotFound, RepositoryError +from amt.core.exceptions import AMTNotFound, AMTRepositoryError from amt.enums.status import Status from amt.models import Project from amt.services.projects import ProjectsService @@ -29,8 +29,8 @@ def get_project_or_error(project_id: int, projects_service: ProjectsService, req logger.debug(f"getting project with id {project_id}") project = projects_service.get(project_id) request.state.path_variables = {"project_id": project_id} - except RepositoryError as e: - raise NotFound from e + except AMTRepositoryError as e: + raise AMTNotFound from e return project @@ -170,7 +170,7 @@ async def get_assessment_card( if not assessment_card_data: logger.warning("assessment card not found") - raise NotFound() + raise AMTNotFound() context = { "assessment_card": assessment_card_data, @@ -215,7 +215,7 @@ async def get_model_card( if not model_card_data: logger.warning("model card not found") - raise NotFound() + raise AMTNotFound() context = { "model_card": model_card_data, diff --git a/amt/clients/clients.py b/amt/clients/clients.py index a96ae3ac4..e60242f4b 100644 --- a/amt/clients/clients.py +++ b/amt/clients/clients.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone import httpx +from amt.core.exceptions import AMTNotFound from amt.schema.github import RepositoryContent logger = logging.getLogger(__name__) @@ -36,7 +37,8 @@ def _get(self, url: str) -> httpx.Response: Private function that performs a GET request to given URL. """ response = self.client.get(url) - response.raise_for_status() + if response.status_code != 200: + raise AMTNotFound() return response @@ -47,7 +49,7 @@ def get_client(repo_type: str) -> Client: case "github": return GitHubClient() case _: - raise ValueError(f"unknown repository type: {repo_type}") + raise AMTNotFound() class GitHubPagesClient(Client): diff --git a/amt/core/config.py b/amt/core/config.py index 7ff4d2646..e04e55b35 100644 --- a/amt/core/config.py +++ b/amt/core/config.py @@ -10,7 +10,7 @@ from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings, SettingsConfigDict -from amt.core.exceptions import SettingsError +from amt.core.exceptions import AMTSettingsError from amt.core.types import DatabaseSchemaType, EnvironmentType, LoggingLevelType logger = logging.getLogger(__name__) @@ -86,19 +86,19 @@ def SQLALCHEMY_DATABASE_URI(self) -> str: @model_validator(mode="after") def _enforce_database_rules(self: SelfSettings) -> SelfSettings: if self.ENVIRONMENT == "production" and self.APP_DATABASE_SCHEME == "sqlite": - raise SettingsError("APP_DATABASE_SCHEME") + raise AMTSettingsError("APP_DATABASE_SCHEME") return self @model_validator(mode="after") def _enforce_debug_rules(self: SelfSettings) -> SelfSettings: if self.ENVIRONMENT == "production" and self.DEBUG: - raise SettingsError("DEBUG") + raise AMTSettingsError("DEBUG") return self @model_validator(mode="after") def _enforce_autocreate_rules(self: SelfSettings) -> SelfSettings: if self.ENVIRONMENT == "production" and self.AUTO_CREATE_SCHEMA: - raise SettingsError("AUTO_CREATE_SCHEMA") + raise AMTSettingsError("AUTO_CREATE_SCHEMA") return self diff --git a/amt/core/exception_handlers.py b/amt/core/exception_handlers.py index be661d3ce..1806eb231 100644 --- a/amt/core/exception_handlers.py +++ b/amt/core/exception_handlers.py @@ -3,65 +3,62 @@ from fastapi import Request, status from fastapi.exceptions import RequestValidationError from fastapi.responses import HTMLResponse -from fastapi_csrf_protect.exceptions import CsrfProtectError # type: ignore from starlette.exceptions import HTTPException as StarletteHTTPException from amt.api.deps import templates +from amt.core.exceptions import AMTHTTPException, AMTNotFound, AMTRepositoryError +from amt.core.internationalization import ( + get_current_translation, +) logger = logging.getLogger(__name__) -async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> HTMLResponse: - logger.debug(f"http_exception_handler: {exc.status_code} - {exc.detail}") +async def general_exception_handler(request: Request, exc: Exception) -> HTMLResponse: + exception_name = exc.__class__.__name__ - if request.state.htmx: - return templates.TemplateResponse( - request, - "errors/_HTTPException.html.j2", - {"status_code": exc.status_code, "status_message": exc.detail}, - status_code=exc.status_code, - ) + logger.debug(f"general_exception_handler {exception_name}: {exc}") - return templates.TemplateResponse( - request, - "errors/HTTPException.html.j2", - {"status_code": exc.status_code, "status_message": exc.detail, "breadcrumbs": []}, - status_code=exc.status_code, - ) + translations = get_current_translation(request) + message = None + if isinstance(exc, AMTRepositoryError | AMTHTTPException): + message = exc.getmessage(translations) + elif isinstance(exc, StarletteHTTPException): + message = AMTNotFound().getmessage(translations) if exc.status_code == status.HTTP_404_NOT_FOUND else exc.detail + elif isinstance(exc, RequestValidationError): + messages: list[str] = [f"{error['loc'][-1]}: {error['msg']}" for error in exc.errors()] + message = "\n".join(messages) -async def validation_exception_handler(request: Request, exc: RequestValidationError) -> HTMLResponse: - logger.debug(f"validation_exception_handler: {exc.errors()}") - errors = exc.errors() - messages: list[str] = [f"{error['loc'][-1]}: {error['msg']}" for error in errors] - - if request.state.htmx: - return templates.TemplateResponse( - request, - "errors/_RequestValidation.html.j2", - {"message": messages}, - status_code=status.HTTP_400_BAD_REQUEST, - ) + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + if isinstance(exc, StarletteHTTPException): + status_code = exc.status_code + elif isinstance(exc, RequestValidationError): + status_code = status.HTTP_400_BAD_REQUEST - return templates.TemplateResponse( - request, "errors/RequestValidation.html.j2", {"message": messages}, status_code=status.HTTP_400_BAD_REQUEST + # todo: what if request.state.htmx does not exist? + template_name = ( + f"errors/_{exception_name}_{status_code}.html.j2" + if request.state.htmx + else f"errors/{exception_name}_{status_code}.html.j2" ) + fallback_template_name = "errors/_Exception.html.j2" if request.state.htmx else "errors/Exception.html.j2" + response: HTMLResponse | None = None -async def csrf_protect_exception_handler(request: Request, exc: CsrfProtectError) -> HTMLResponse: - logger.debug(f"csrf_protect_exception_handler: {exc.status_code} - {exc.message}") - - if request.state.htmx: - return templates.TemplateResponse( + try: + response = templates.TemplateResponse( request, - "errors/_CsrfProtectError.html.j2", - {"status_code": exc.status_code, "message": exc.message}, - status_code=exc.status_code, + template_name, + {"message": message}, + status_code=status_code, + ) + except Exception: + response = templates.TemplateResponse( + request, + fallback_template_name, + {"message": message}, + status_code=status_code, ) - return templates.TemplateResponse( - request, - "errors/CsrfProtectError.html.j2", - {"status_code": exc.status_code, "message": exc.message}, - status_code=exc.status_code, - ) + return response diff --git a/amt/core/exceptions.py b/amt/core/exceptions.py index 56e02d72a..2a4304bf7 100644 --- a/amt/core/exceptions.py +++ b/amt/core/exceptions.py @@ -1,60 +1,67 @@ -from pathlib import Path +from gettext import gettext as _ +from babel.support import NullTranslations from fastapi import status -from fastapi.exceptions import HTTPException, ValidationException +from fastapi.exceptions import HTTPException class AMTHTTPException(HTTPException): - pass + def getmessage(self, translations: NullTranslations) -> str: + return translations.gettext(self.detail) -class AMTValidationException(ValidationException): - pass +class AMTError(Exception): + def getmessage(self, translations: NullTranslations) -> str: + return translations.gettext(self.detail) # type: ignore -class AMTError(RuntimeError): - """ - A generic, AMT-specific error. - """ +class AMTSettingsError(AMTError): + def __init__(self, field: str) -> None: + self.detail: str = _( + "An error occurred while configuring the options for '{field}'. Please check the settings and try again." + ).format(field=field) + super().__init__(self.detail) -class SettingsError(AMTError): - def __init__(self, field: str) -> None: - self.message: str = f"Settings error for options {field}" - exception_name: str = self.__class__.__name__ - super().__init__(f"{exception_name}: {self.message}") +class AMTRepositoryError(AMTHTTPException): + def __init__(self, detail: str | None = "Repository error") -> None: + self.detail: str = _("An internal server error occurred while processing your request. Please try again later.") + super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, self.detail) + +class AMTInstrumentError(AMTHTTPException): + def __init__(self) -> None: + self.detail: str = _("An error occurred while processing the instrument. Please try again later.") + super().__init__(status.HTTP_501_NOT_IMPLEMENTED, self.detail) -class RepositoryError(AMTHTTPException): - def __init__(self, message: str = "Repository error") -> None: - self.message: str = message - exception_name: str = self.__class__.__name__ - super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, f"{exception_name}: {self.message}") +class AMTNotFound(AMTHTTPException): + def __init__(self) -> None: + self.detail: str = _( + "The requested page or resource could not be found. Please check the URL or query and try again." + ) + super().__init__(status.HTTP_404_NOT_FOUND, self.detail) -class InstrumentError(AMTHTTPException): - def __init__(self, message: str = "Instrument error") -> None: - self.message: str = message - exception_name: str = self.__class__.__name__ - super().__init__(status.HTTP_501_NOT_IMPLEMENTED, f"{exception_name}: {self.message}") +class AMTCSRFProtectError(AMTHTTPException): + def __init__(self) -> None: + self.detail: str = _("CSRF check failed.") + super().__init__(status.HTTP_401_UNAUTHORIZED, self.detail) -class UnsafeFileError(AMTHTTPException): - def __init__(self, file: Path) -> None: - self.message: str = f"Unsafe file error for file {file}" - exception_name: str = self.__class__.__name__ - super().__init__(status.HTTP_400_BAD_REQUEST, f"{exception_name}: {self.message}") +class AMTOnlyStatic(AMTHTTPException): + def __init__(self) -> None: + self.detail: str = _("Only static files are supported.") + super().__init__(status.HTTP_400_BAD_REQUEST, self.detail) -class RepositoryNoResultFound(AMTHTTPException): - def __init__(self, message: str = "No entity found") -> None: - self.message: str = message - exception_name: str = self.__class__.__name__ - super().__init__(status.HTTP_204_NO_CONTENT, f"{exception_name}: {self.message}") + +class AMTKeyError(AMTHTTPException): + def __init__(self, field: str) -> None: + self.detail: str = _("Key not correct: {field}").format(field=field) + super().__init__(status.HTTP_400_BAD_REQUEST, self.detail) -class NotFound(AMTHTTPException): - def __init__(self, message: str = "Not found") -> None: - self.message: str = message - exception_name: str = self.__class__.__name__ - super().__init__(status.HTTP_404_NOT_FOUND, f"{exception_name}: {self.message}") +class AMTValueError(AMTHTTPException): + def __init__(self, field: str) -> None: + self.detail: str = _("Value not correct: {field}").format(field=field) + super().__init__(status.HTTP_400_BAD_REQUEST, self.detail) diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 90a284014..efb1ce8d5 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-04 10:24+0200\n" +"POT-Creation-Date: 2024-09-30 10:07+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -77,22 +77,52 @@ msgstr "" msgid "Details" msgstr "" -#: amt/site/templates/errors/_HTTPException404.html.j2:1 -msgid "Page not found" +#: amt/core/exceptions.py:20 +msgid "" +"An error occurred while configuring the options for '{field}'. Please " +"check the settings and try again." +msgstr "" + +#: amt/core/exceptions.py:26 +msgid "" +"An internal server error occurred while processing your request. Please " +"try again later." msgstr "" -#: amt/site/templates/errors/_HTTPException404.html.j2:3 -msgid "The page or file you requested is not found. Please check the URL." +#: amt/core/exceptions.py:32 +msgid "An error occurred while processing the instrument. Please try again later." msgstr "" -#: amt/site/templates/errors/_RequestValidation.html.j2:1 -msgid "Request Validation Error" +#: amt/core/exceptions.py:37 +msgid "" +"The requested page or resource could not be found. Please check the URL " +"or query and try again." +msgstr "" + +#: amt/core/exceptions.py:43 +msgid "CSRF check failed." msgstr "" -#: amt/site/templates/errors/error.html.j2:4 +#: amt/core/exceptions.py:49 +msgid "Only static files are supported." +msgstr "" + +#: amt/core/exceptions.py:54 +msgid "Key not correct: {field}" +msgstr "" + +#: amt/core/exceptions.py:59 +msgid "Value not correct: {field}" +msgstr "" + +#: amt/site/templates/errors/Exception.html.j2:6 msgid "An error occurred" msgstr "" +#: amt/site/templates/errors/_Exception.html.j2:1 +msgid "An error occurred. Please try again later" +msgstr "" + #: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index 292a7d55e..b4fe15178 100644 Binary files a/amt/locale/en_US/LC_MESSAGES/messages.mo and b/amt/locale/en_US/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index a6f363fb5..16836484d 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-04 10:24+0200\n" +"POT-Creation-Date: 2024-09-30 10:07+0200\n" "PO-Revision-Date: 2024-07-25 21:01+0200\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" @@ -78,22 +78,52 @@ msgstr "" msgid "Details" msgstr "" -#: amt/site/templates/errors/_HTTPException404.html.j2:1 -msgid "Page not found" +#: amt/core/exceptions.py:20 +msgid "" +"An error occurred while configuring the options for '{field}'. Please " +"check the settings and try again." +msgstr "" + +#: amt/core/exceptions.py:26 +msgid "" +"An internal server error occurred while processing your request. Please " +"try again later." msgstr "" -#: amt/site/templates/errors/_HTTPException404.html.j2:3 -msgid "The page or file you requested is not found. Please check the URL." +#: amt/core/exceptions.py:32 +msgid "An error occurred while processing the instrument. Please try again later." msgstr "" -#: amt/site/templates/errors/_RequestValidation.html.j2:1 -msgid "Request Validation Error" +#: amt/core/exceptions.py:37 +msgid "" +"The requested page or resource could not be found. Please check the URL " +"or query and try again." +msgstr "" + +#: amt/core/exceptions.py:43 +msgid "CSRF check failed." msgstr "" -#: amt/site/templates/errors/error.html.j2:4 +#: amt/core/exceptions.py:49 +msgid "Only static files are supported." +msgstr "" + +#: amt/core/exceptions.py:54 +msgid "Key not correct: {field}" +msgstr "" + +#: amt/core/exceptions.py:59 +msgid "Value not correct: {field}" +msgstr "" + +#: amt/site/templates/errors/Exception.html.j2:6 msgid "An error occurred" msgstr "" +#: amt/site/templates/errors/_Exception.html.j2:1 +msgid "An error occurred. Please try again later" +msgstr "" + #: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" diff --git a/amt/locale/nl_FY/LC_MESSAGES/messages.mo b/amt/locale/nl_FY/LC_MESSAGES/messages.mo index ab207836e..56118685b 100644 Binary files a/amt/locale/nl_FY/LC_MESSAGES/messages.mo and b/amt/locale/nl_FY/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/nl_FY/LC_MESSAGES/messages.po b/amt/locale/nl_FY/LC_MESSAGES/messages.po index 51b17686e..6df5fc9e7 100644 --- a/amt/locale/nl_FY/LC_MESSAGES/messages.po +++ b/amt/locale/nl_FY/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-04 10:24+0200\n" +"POT-Creation-Date: 2024-09-30 10:07+0200\n" "PO-Revision-Date: 2024-07-25 21:01+0200\n" "Last-Translator: FULL NAME \n" "Language: nl_FY\n" @@ -24,75 +24,105 @@ msgstr "" #: amt/api/ai_act_profile.py:25 msgid "Is the application open source?" -msgstr "" +msgstr "Is de applikaasje iepen boarne?" #: amt/api/ai_act_profile.py:27 msgid "Publication Category" -msgstr "" +msgstr "Publikaasje Category" #: amt/api/ai_act_profile.py:29 msgid "Is there a systemic risk?" -msgstr "" +msgstr "Is der in systemic risiko?" #: amt/api/ai_act_profile.py:31 msgid "Is there a transparency obligation?" -msgstr "" +msgstr "is der in ferplichting foar transparânsje" #: amt/api/ai_act_profile.py:33 msgid "Role" -msgstr "" +msgstr "Rol" #: amt/api/navigation.py:39 msgid "Home" -msgstr "" +msgstr "thús" #: amt/api/navigation.py:40 amt/site/templates/projects/index.html.j2:12 msgid "Projects" -msgstr "" +msgstr "Projekten" #: amt/api/navigation.py:41 msgid "Overview" -msgstr "" +msgstr "Oersicht" #: amt/api/navigation.py:42 amt/site/templates/projects/tasks.html.j2:8 msgid "Tasks" -msgstr "" +msgstr "Taken" #: amt/api/navigation.py:43 msgid "New" -msgstr "" +msgstr "Nij" #: amt/api/navigation.py:44 msgid "System card" -msgstr "" +msgstr "Systeem kaart" #: amt/api/navigation.py:45 msgid "Assessment card" -msgstr "" +msgstr "Beoardielingskaart" #: amt/api/navigation.py:46 msgid "Model card" -msgstr "" +msgstr "Modelkaart" #: amt/api/navigation.py:47 msgid "Details" -msgstr "" +msgstr "Details" -#: amt/site/templates/errors/_HTTPException404.html.j2:1 -msgid "Page not found" -msgstr "" +#: amt/core/exceptions.py:20 +msgid "" +"An error occurred while configuring the options for '{field}'. Please " +"check the settings and try again." +msgstr "Der barde in flater by it konfigurearjen fan de opsjes foar '{field}'. Kontrolearje asjebleaft de ynstellings en besykje it nochris." -#: amt/site/templates/errors/_HTTPException404.html.j2:3 -msgid "The page or file you requested is not found. Please check the URL." -msgstr "" +#: amt/core/exceptions.py:26 +msgid "" +"An internal server error occurred while processing your request. Please " +"try again later." +msgstr "Der barde in ynterne serverflater by it ferwurkjen fan jo fersyk. Besykje it letter nochris." -#: amt/site/templates/errors/_RequestValidation.html.j2:1 -msgid "Request Validation Error" -msgstr "" +#: amt/core/exceptions.py:32 +msgid "An error occurred while processing the instrument. Please try again later." +msgstr "Der barde in flater by it ferwurkjen fan it ynstrumint. Besykje it letter nochris." -#: amt/site/templates/errors/error.html.j2:4 +#: amt/core/exceptions.py:37 +msgid "" +"The requested page or resource could not be found. Please check the URL " +"or query and try again." +msgstr "De frege side of boarne koe net fûn wurde. Kontrolearje asjebleaft de URL of query en besykje it nochris" + +#: amt/core/exceptions.py:43 +msgid "CSRF check failed." +msgstr "CSRF-kontrôle mislearre." + +#: amt/core/exceptions.py:49 +msgid "Only static files are supported." +msgstr "Allinich statyske bestannen wurde stipe." + +#: amt/core/exceptions.py:54 +msgid "Key not correct: {field}" +msgstr "Key net korrekt: {field}" + +#: amt/core/exceptions.py:59 +msgid "Value not correct: {field}" +msgstr "Wearde net korrekt: {field}" + +#: amt/site/templates/errors/Exception.html.j2:6 msgid "An error occurred" -msgstr "" +msgstr "Der barde in flater" + +#: amt/site/templates/errors/_Exception.html.j2:1 +msgid "An error occurred. Please try again later" +msgstr "Der barde in flater. Besykje it letter nochris" #: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" @@ -116,45 +146,45 @@ msgstr "Dien" #: amt/site/templates/macros/tasks.html.j2:14 msgid "Unknown" -msgstr "" +msgstr "Ûnbekend" #: amt/site/templates/pages/assessment_card.html.j2:9 #: amt/site/templates/pages/model_card.html.j2:8 #: amt/site/templates/pages/system_card.html.j2:9 msgid "Last updated" -msgstr "" +msgstr "Lêst bywurke" #: amt/site/templates/pages/assessment_card.html.j2:9 #: amt/site/templates/pages/model_card.html.j2:8 #: amt/site/templates/pages/system_card.html.j2:9 msgid "ago" -msgstr "" +msgstr "lyn" #: amt/site/templates/pages/assessment_card.html.j2:15 #: amt/site/templates/pages/model_card.html.j2:14 #: amt/site/templates/pages/model_card.html.j2:38 #: amt/site/templates/pages/system_card.html.j2:61 msgid "Attribute" -msgstr "" +msgstr "Attribute" #: amt/site/templates/pages/assessment_card.html.j2:16 #: amt/site/templates/pages/model_card.html.j2:15 #: amt/site/templates/pages/model_card.html.j2:39 #: amt/site/templates/pages/system_card.html.j2:62 msgid "Value" -msgstr "" +msgstr "Wearde" #: amt/site/templates/pages/assessment_card.html.j2:38 msgid "Question" -msgstr "" +msgstr "Fraach" #: amt/site/templates/pages/assessment_card.html.j2:39 msgid "Answer" -msgstr "" +msgstr "Antwurd" #: amt/site/templates/pages/index.html.j2:3 msgid "AMT Placeholder informatie pagina's" -msgstr "" +msgstr "AMT Placeholder ynformaasje pagina's" #: amt/site/templates/parts/footer.html.j2:4 #: amt/site/templates/parts/header.html.j2:8 @@ -163,20 +193,20 @@ msgstr "" #: amt/site/templates/parts/footer.html.j2:15 msgid "About us" -msgstr "" +msgstr "Oer ús" #: amt/site/templates/parts/footer.html.j2:27 msgid "Contact" -msgstr "" +msgstr "Kontakt" #: amt/site/templates/parts/header.html.j2:8 msgid "version" -msgstr "" +msgstr "ferzje" #: amt/site/templates/parts/header.html.j2:70 #: amt/site/templates/parts/header.html.j2:107 msgid "Language" -msgstr "" +msgstr "Taal" #: amt/site/templates/projects/index.html.j2:19 msgid "New project" @@ -196,49 +226,73 @@ msgstr "" #: amt/site/templates/projects/new.html.j2:26 msgid "Your project name here" -msgstr "" +msgstr "Jo projektnamme hjir" #: amt/site/templates/projects/new.html.j2:35 msgid "AI Act Profile" -msgstr "" +msgstr "AI Act profyl" #: amt/site/templates/projects/new.html.j2:37 msgid "" "The AI Act profile provides insight into, among other things, the type of" " AI system and the associated obligations from the European AI Act." -msgstr "" +msgstr "It profyl AI Wet jout ynsjoch yn ûnder oare it type KI-systeem en de dêrby hearrende ferplichtings út de Europeeske KI-wet." #: amt/site/templates/projects/new.html.j2:39 msgid "The AI Act decision tree " -msgstr "" +msgstr "De AI Act beslút beam" #: amt/site/templates/projects/new.html.j2:40 msgid "helps you determine the value of these fields." -msgstr "" +msgstr "helpt jo de wearde fan dizze fjilden te bepalen." #: amt/site/templates/projects/new.html.j2:96 msgid "Yes" -msgstr "" +msgstr "Ja" #: amt/site/templates/projects/new.html.j2:106 msgid "No" -msgstr "" +msgstr "Nee" #: amt/site/templates/projects/new.html.j2:113 msgid "Instruments" -msgstr "" +msgstr "Ynstruminten" #: amt/site/templates/projects/new.html.j2:115 msgid "" "Overview of instruments for the responsible development, deployment, " -"assessment and monitoring of algorithms and AI-systems." -msgstr "" +"assessment and monitoring of algorithms and AI-systems" +msgstr "Oersjoch fan ynstruminten foar de ferantwurdlike ûntwikkeling, ynset, beoardieling en tafersjoch fan algoritmen en AI-systemen" #: amt/site/templates/projects/new.html.j2:123 msgid "Choose one or more instruments" -msgstr "" +msgstr "Kies ien of mear ynstruminten" #: amt/site/templates/projects/new.html.j2:137 msgid "Create Project" -msgstr "" - +msgstr "Project oanmeitsje" + +#~ msgid "Your projectname here" +#~ msgstr "Jo projektnamme hjir" + +#~ msgid "To find your AI Act Profile you can use the" +#~ msgstr "Om jo AI Act-profyl te finen kinne jo de" + +#~ msgid "" +#~ "The AI Act profile provides insight " +#~ "into, among other things, the type " +#~ "of AI system and the associated " +#~ "obligations from the European AI Act." +#~ "\n" +#~ " The" +#~ msgstr "" + +#~ msgid "AI Act decision tree " +#~ msgstr "" + +#~ msgid "" +#~ "Overview of instruments for the " +#~ "responsible development, deployment, assessment " +#~ "and monitoring of algorithms and AI-" +#~ "systems" +#~ msgstr "" diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index b28757591..c14b6aae1 100644 Binary files a/amt/locale/nl_NL/LC_MESSAGES/messages.mo and b/amt/locale/nl_NL/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index ee0466db9..ba81d532c 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-04 10:24+0200\n" +"POT-Creation-Date: 2024-09-30 10:07+0200\n" "PO-Revision-Date: 2024-07-25 21:01+0200\n" "Last-Translator: FULL NAME \n" "Language: nl_NL\n" @@ -64,37 +64,65 @@ msgstr "Nieuw" #: amt/api/navigation.py:44 msgid "System card" -msgstr "" +msgstr "Systeemkaart" #: amt/api/navigation.py:45 msgid "Assessment card" -msgstr "" +msgstr "Beoordelingskaart" #: amt/api/navigation.py:46 msgid "Model card" -msgstr "" +msgstr "Model kaart" #: amt/api/navigation.py:47 msgid "Details" -msgstr "" +msgstr "Details" -#: amt/site/templates/errors/_HTTPException404.html.j2:1 -msgid "Page not found" -msgstr "Pagina niet gevonden" +#: amt/core/exceptions.py:20 +msgid "" +"An error occurred while configuring the options for '{field}'. Please " +"check the settings and try again." +msgstr "Er is een fout opgetreden bij het configureren van de opties voor '{field}'. Controleer de instellingen en probeer het opnieuw." -#: amt/site/templates/errors/_HTTPException404.html.j2:3 -msgid "The page or file you requested is not found. Please check the URL." -msgstr "" -"De pagina die u wilde zien of het bestand dat u wilde bekijken is niet " -"gevonden. Controleer de URL." +#: amt/core/exceptions.py:28 +msgid "" +"An internal server error occurred while processing your request. Please " +"try again later." +msgstr "Er is een fout opgetreden. Probeer het later opnieuw." + +#: amt/core/exceptions.py:32 +msgid "An error occurred while processing the instrument. Please try again later." +msgstr "Er is een fout opgetreden tijdens het verwerken van het instrument. Probeer het later opnieuw." -#: amt/site/templates/errors/_RequestValidation.html.j2:1 -msgid "Request Validation Error" -msgstr "Aanvraag Validatie Fout" +#: amt/core/exceptions.py:37 +msgid "" +"The requested page or resource could not be found. Please check the URL " +"or query and try again." +msgstr "De gevraagde pagina of bron kon niet worden gevonden. Controleer de URL of query en probeer het opnieuw." + +#: amt/core/exceptions.py:43 +msgid "CSRF check failed." +msgstr "CSRF-controle mislukt." + +#: amt/core/exceptions.py:49 +msgid "Only static files are supported." +msgstr "Alleen statische bestanden worden ondersteund." + +#: amt/core/exceptions.py:54 +msgid "Key not correct: {field}" +msgstr "Sleutel niet correct: {field}" -#: amt/site/templates/errors/error.html.j2:4 +#: amt/core/exceptions.py:59 +msgid "Value not correct: {field}" +msgstr "Waarde is niet correct: {field}" + +#: amt/site/templates/errors/Exception.html.j2:6 msgid "An error occurred" -msgstr "Er is is een fout opgetreden" +msgstr "Er is een fout opgetreden" + +#: amt/site/templates/errors/_Exception.html.j2:1 +msgid "An error occurred. Please try again later" +msgstr "Er is een fout opgetreden. Probeer het later opnieuw." #: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" @@ -156,7 +184,7 @@ msgstr "Antwoord" #: amt/site/templates/pages/index.html.j2:3 msgid "AMT Placeholder informatie pagina's" -msgstr "" +msgstr "AMT Placeholder informatie pagina's" #: amt/site/templates/parts/footer.html.j2:4 #: amt/site/templates/parts/header.html.j2:8 @@ -249,3 +277,11 @@ msgstr "Kies één of meerdere instrumenten" msgid "Create Project" msgstr "Maak project" +#~ msgid "To find your AI Act Profile you can use the" +#~ msgstr "Om het AI Verordening Profiel te bepalen kunt gebruik maken van de" + +#~ msgid "The page or file you requested is not found. Please check the URL." +#~ msgstr "" +#~ "De pagina die u wilde zien of " +#~ "het bestand dat u wilde bekijken " +#~ "is niet gevonden. Controleer de URL." diff --git a/amt/middleware/csrf.py b/amt/middleware/csrf.py index a126dc7b2..d088d7f56 100644 --- a/amt/middleware/csrf.py +++ b/amt/middleware/csrf.py @@ -9,7 +9,7 @@ from starlette.types import ASGIApp from amt.core.csrf import get_csrf_config # type: ignore # noqa -from amt.core.exception_handlers import csrf_protect_exception_handler +from amt.core.exceptions import AMTCSRFProtectError RequestResponseEndpoint = typing.Callable[[Request], typing.Awaitable[Response]] @@ -74,6 +74,5 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - try: response = await call_next(request) except CsrfProtectError as e: - logger.warning(f"CsrfProtectError: {e}") - return await csrf_protect_exception_handler(request, e) + raise AMTCSRFProtectError() from e return response diff --git a/amt/repositories/projects.py b/amt/repositories/projects.py index 3d661e802..956327f6c 100644 --- a/amt/repositories/projects.py +++ b/amt/repositories/projects.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from sqlalchemy_utils import escape_like # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType] -from amt.core.exceptions import RepositoryError +from amt.core.exceptions import AMTRepositoryError from amt.models import Project from amt.repositories.deps import get_session @@ -34,7 +34,7 @@ def delete(self, project: Project) -> None: except Exception as e: logger.exception("Error deleting project") self.session.rollback() - raise RepositoryError from e + raise AMTRepositoryError from e return None def save(self, project: Project) -> Project: @@ -45,7 +45,7 @@ def save(self, project: Project) -> Project: except SQLAlchemyError as e: logger.exception("Error saving project") self.session.rollback() - raise RepositoryError from e + raise AMTRepositoryError from e return project def find_by_id(self, project_id: int) -> Project: @@ -54,7 +54,7 @@ def find_by_id(self, project_id: int) -> Project: return self.session.execute(statement).scalars().one() except NoResultFound as e: logger.exception("Project not found") - raise RepositoryError from e + raise AMTRepositoryError from e def paginate(self, skip: int, limit: int, search: str) -> list[Project]: try: @@ -65,4 +65,4 @@ def paginate(self, skip: int, limit: int, search: str) -> list[Project]: return list(self.session.execute(statement).scalars()) except Exception as e: logger.exception("Error paginating projects") - raise RepositoryError from e + raise AMTRepositoryError from e diff --git a/amt/repositories/tasks.py b/amt/repositories/tasks.py index 96853c205..735094fb4 100644 --- a/amt/repositories/tasks.py +++ b/amt/repositories/tasks.py @@ -7,7 +7,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session -from amt.core.exceptions import RepositoryError +from amt.core.exceptions import AMTRepositoryError from amt.models import Task from amt.repositories.deps import get_session @@ -64,7 +64,7 @@ def save(self, task: Task) -> Task: except Exception as e: logger.exception("Could not store task") self.session.rollback() - raise RepositoryError from e + raise AMTRepositoryError from e return task def save_all(self, tasks: Sequence[Task]) -> None: @@ -79,7 +79,7 @@ def save_all(self, tasks: Sequence[Task]) -> None: except Exception as e: logger.exception("Could not store all tasks") self.session.rollback() - raise RepositoryError from e + raise AMTRepositoryError from e def delete(self, task: Task) -> None: """ @@ -93,7 +93,7 @@ def delete(self, task: Task) -> None: except Exception as e: logger.exception("Could not delete task") self.session.rollback() - raise RepositoryError from e + raise AMTRepositoryError from e return None def find_by_id(self, task_id: int) -> Task: @@ -107,4 +107,4 @@ def find_by_id(self, task_id: int) -> Task: return self.session.execute(statement).scalars().one() except NoResultFound as e: logger.exception("Task not found") - raise RepositoryError from e + raise AMTRepositoryError from e diff --git a/amt/server.py b/amt/server.py index e8c0c3bc7..5d6cd8273 100644 --- a/amt/server.py +++ b/amt/server.py @@ -10,12 +10,7 @@ from amt.api.main import api_router from amt.core.config import PROJECT_DESCRIPTION, PROJECT_NAME, VERSION, get_settings from amt.core.db import check_db, init_db -from amt.core.exception_handlers import ( - http_exception_handler as amt_http_exception_handler, -) -from amt.core.exception_handlers import ( - validation_exception_handler as amt_validation_exception_handler, -) +from amt.core.exception_handlers import general_exception_handler as amt_general_exception_handler from amt.core.log import configure_logging from amt.utils.mask import Mask @@ -63,12 +58,16 @@ def create_app() -> FastAPI: app.mount("/static", static_files, name="static") @app.exception_handler(StarletteHTTPException) - async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> HTMLResponse: # type: ignore - return await amt_http_exception_handler(request, exc) + async def HTTPException_exception_handler(request: Request, exc: Exception) -> HTMLResponse: # type: ignore + return await amt_general_exception_handler(request, exc) @app.exception_handler(RequestValidationError) - async def validation_exception_handler(request: Request, exc: RequestValidationError) -> HTMLResponse: # type: ignore - return await amt_validation_exception_handler(request, exc) + async def request_validation_exception_handler(request: Request, exc: Exception) -> HTMLResponse: # type: ignore + return await amt_general_exception_handler(request, exc) + + @app.exception_handler(Exception) + async def general_exception_handler(request: Request, exc: Exception) -> HTMLResponse: # type: ignore + return await amt_general_exception_handler(request, exc) app.include_router(api_router) diff --git a/amt/services/instruments.py b/amt/services/instruments.py index 6c1a66fee..0517222e0 100644 --- a/amt/services/instruments.py +++ b/amt/services/instruments.py @@ -4,7 +4,7 @@ import yaml from amt.clients.clients import get_client -from amt.core.exceptions import InstrumentError +from amt.core.exceptions import AMTInstrumentError from amt.schema.github import RepositoryContent from amt.schema.instrument import Instrument @@ -28,7 +28,7 @@ def fetch_github_content(self, url: str) -> Instrument: if "urn" not in data: # todo: this is now an HTTP error, while a service can also be used from another context logger.exception("Key 'urn' not found in instrument.") - raise InstrumentError("Key 'urn' not found in instrument.") + raise AMTInstrumentError() return Instrument(**data) diff --git a/amt/services/storage.py b/amt/services/storage.py index 346f63d8d..b738e5a4f 100644 --- a/amt/services/storage.py +++ b/amt/services/storage.py @@ -7,6 +7,8 @@ from typing_extensions import Unpack from yaml import dump +from amt.core.exceptions import AMTKeyError, AMTValueError + class WriterFactoryArguments(TypedDict): location: str | Path @@ -33,17 +35,17 @@ def init(storage_type: str = "file", **kwargs: Unpack[WriterFactoryArguments]) - match storage_type: case "file": if not all(k in kwargs for k in ("location", "filename")): - raise KeyError("The `location` or `filename` variables are not provided as input for init()") + raise AMTKeyError("`location` | `filename`") return FileSystemStorageService(location=Path(kwargs["location"]), filename=str(kwargs["filename"])) case _: - raise ValueError(f"Unknown storage type: {storage_type}") + raise AMTValueError(storage_type) class FileSystemStorageService(Storage): def __init__(self, location: Path = Path("./tests/data"), filename: str = "system_card.yaml") -> None: self.base_dir = location if not filename.endswith(".yaml"): - raise ValueError(f"Filename {filename} must end with .yaml instead of .{filename.split('.')[-1]}") + raise AMTValueError(filename) self.filename = filename self.path = self.base_dir / self.filename diff --git a/amt/services/tasks.py b/amt/services/tasks.py index 2b1905841..f581ce4d2 100644 --- a/amt/services/tasks.py +++ b/amt/services/tasks.py @@ -67,9 +67,6 @@ def move_task( self.system_card.name = task.title self.storage_writer.write(self.system_card.model_dump()) - if not isinstance(status_id, int): - raise TypeError("status_id must be an integer") # pragma: no cover - # update the status for the task (this may not be needed if the status has not changed) task.status_id = status_id diff --git a/amt/site/templates/errors/CsrfProtectError.html.j2 b/amt/site/templates/errors/CsrfProtectError.html.j2 deleted file mode 100644 index d77efc1b8..000000000 --- a/amt/site/templates/errors/CsrfProtectError.html.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'layouts/base.html.j2' %} - -{% block content %} -{% include 'errors/_CsrfProtectError.html.j2' %} -{% endblock %} diff --git a/amt/site/templates/errors/Exception.html.j2 b/amt/site/templates/errors/Exception.html.j2 new file mode 100644 index 000000000..56f457364 --- /dev/null +++ b/amt/site/templates/errors/Exception.html.j2 @@ -0,0 +1,11 @@ +{% extends 'layouts/base.html.j2' %} + +{% block content %} +
+
+

{% trans %}An error occurred{% endtrans %}

+ {% include 'errors/_Exception.html.j2' %} +
+
+ +{% endblock %} diff --git a/amt/site/templates/errors/HTTPException.html.j2 b/amt/site/templates/errors/HTTPException.html.j2 deleted file mode 100644 index bc9ac2c1f..000000000 --- a/amt/site/templates/errors/HTTPException.html.j2 +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'layouts/base.html.j2' %} - -{% block content %} -
- - {% if status_code == 404 %} - {% include 'errors/_HTTPException404.html.j2' %} - {% else %} - {% include 'errors/_HTTPException.html.j2' %} - {% endif %} -
-{% endblock %} diff --git a/amt/site/templates/errors/RequestValidation.html.j2 b/amt/site/templates/errors/RequestValidation.html.j2 deleted file mode 100644 index cdb3b584f..000000000 --- a/amt/site/templates/errors/RequestValidation.html.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'layouts/base.html.j2' %} - -{% block content %} -{% include 'errors/_RequestValidation.html.j2' %} -{% endblock %} diff --git a/amt/site/templates/errors/_CsrfProtectError.html.j2 b/amt/site/templates/errors/_CsrfProtectError.html.j2 deleted file mode 100644 index 3f2cb32cc..000000000 --- a/amt/site/templates/errors/_CsrfProtectError.html.j2 +++ /dev/null @@ -1,2 +0,0 @@ -

{{status_code}}

-

{{status_message}}

diff --git a/amt/site/templates/errors/_Exception.html.j2 b/amt/site/templates/errors/_Exception.html.j2 new file mode 100644 index 000000000..e83218360 --- /dev/null +++ b/amt/site/templates/errors/_Exception.html.j2 @@ -0,0 +1 @@ +

{% if message is defined and message is not none %}{{message}}{% else %}{% trans %}An error occurred. Please try again later{% endtrans %}{% endif %}

diff --git a/amt/site/templates/errors/_HTTPException.html.j2 b/amt/site/templates/errors/_HTTPException.html.j2 deleted file mode 100644 index 3f2cb32cc..000000000 --- a/amt/site/templates/errors/_HTTPException.html.j2 +++ /dev/null @@ -1,2 +0,0 @@ -

{{status_code}}

-

{{status_message}}

diff --git a/amt/site/templates/errors/_HTTPException404.html.j2 b/amt/site/templates/errors/_HTTPException404.html.j2 deleted file mode 100644 index cab0dee36..000000000 --- a/amt/site/templates/errors/_HTTPException404.html.j2 +++ /dev/null @@ -1,4 +0,0 @@ -

{% trans %}Page not found{% endtrans %}

-

-{% trans %}The page or file you requested is not found. Please check the URL.{% endtrans %} -

diff --git a/amt/site/templates/errors/_RequestValidation.html.j2 b/amt/site/templates/errors/_RequestValidation.html.j2 deleted file mode 100644 index 3177460b8..000000000 --- a/amt/site/templates/errors/_RequestValidation.html.j2 +++ /dev/null @@ -1,2 +0,0 @@ -

{% trans %}Request Validation Error{% endtrans %}

-

{{message}}

diff --git a/amt/site/templates/errors/error.html.j2 b/amt/site/templates/errors/error.html.j2 deleted file mode 100644 index 7ad6e7a83..000000000 --- a/amt/site/templates/errors/error.html.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'layouts/base.html.j2' %} - -{% block content %} -

{% trans %}An error occurred{% endtrans %}

-{% endblock %} diff --git a/amt/site/templates/projects/new.html.j2 b/amt/site/templates/projects/new.html.j2 index ed26c6d72..96b325ecb 100644 --- a/amt/site/templates/projects/new.html.j2 +++ b/amt/site/templates/projects/new.html.j2 @@ -36,6 +36,7 @@

{% trans %}The AI Act profile provides insight into, among other things, the type of AI system and the associated obligations from the European AI Act.{% endtrans %} {% trans %}The AI Act decision tree {% endtrans %} {% trans %}helps you determine the value of these fields.{% endtrans %}

diff --git a/tests/api/routes/test_project.py b/tests/api/routes/test_project.py index f51c0d292..a90f53524 100644 --- a/tests/api/routes/test_project.py +++ b/tests/api/routes/test_project.py @@ -24,7 +24,7 @@ def test_get_unknown_project(client: TestClient) -> None: # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content def test_get_project_tasks(client: TestClient, db: DatabaseTestUtils) -> None: @@ -66,7 +66,7 @@ def test_get_system_card_unknown_project(client: TestClient) -> None: # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content # TODO: Test are now have hard coded URL paths because the system card @@ -95,7 +95,7 @@ def test_get_assessment_card_unknown_project(client: TestClient) -> None: # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content # TODO: Test are now have hard coded URL paths because the system card @@ -111,7 +111,7 @@ def test_get_assessment_card_unknown_assessment(client: TestClient, db: Database # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content # TODO: Test are now have hard coded URL paths because the system card @@ -140,7 +140,7 @@ def test_get_model_card_unknown_project(client: TestClient) -> None: # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content # TODO: Test are now have hard coded URL paths because the system card @@ -156,4 +156,4 @@ def test_get_assessment_card_unknown_model_card(client: TestClient, db: Database # then assert response.status_code == 404 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Page not found" in response.content + assert b"The requested page or resource could not be found." in response.content diff --git a/tests/api/routes/test_tasks_move.py b/tests/api/routes/test_tasks_move.py index cddb8cf05..dea4b06d1 100644 --- a/tests/api/routes/test_tasks_move.py +++ b/tests/api/routes/test_tasks_move.py @@ -29,4 +29,4 @@ def test_task_move_error(client: TestClient, db: DatabaseTestUtils, mock_csrf: G ) assert response.status_code == 500 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"RepositoryError: Repository error" in response.content + assert b"An internal server error occurred while processing your request" in response.content diff --git a/tests/api/test_http_browser_caching.py b/tests/api/test_http_browser_caching.py index 0fcc16f97..e2e515655 100644 --- a/tests/api/test_http_browser_caching.py +++ b/tests/api/test_http_browser_caching.py @@ -5,21 +5,22 @@ import pytest from amt.api import http_browser_caching +from amt.core.exceptions import AMTNotFound, AMTOnlyStatic from starlette.responses import Response def test_url_for_cache_not_static(): - with pytest.raises(ValueError, match="Only static files are supported."): + with pytest.raises(AMTOnlyStatic, match="Only static files are supported."): http_browser_caching.url_for_cache("not-static") def test_url_for_cache_not_local(): - with pytest.raises(ValueError, match="Only local URLS are supported."): + with pytest.raises(AMTOnlyStatic, match="Only static files are supported."): http_browser_caching.url_for_cache("static", path="http://this.is.not.local") def test_url_for_cache_file_not_found(): - with pytest.raises(ValueError, match="Static file this/does/not/exist not found."): + with pytest.raises(AMTNotFound, match="The requested page or resource could not be found."): http_browser_caching.url_for_cache("static", path="this/does/not/exist") diff --git a/tests/cli/test_check_state.py b/tests/cli/test_check_state.py index f1c71a499..2e57f52e8 100644 --- a/tests/cli/test_check_state.py +++ b/tests/cli/test_check_state.py @@ -14,7 +14,7 @@ get_task_timestamp_from_assessment_card, get_tasks_by_priority, ) -from amt.core.exceptions import InstrumentError +from amt.core.exceptions import AMTInstrumentError from amt.schema.assessment_card import AssessmentCard from amt.schema.instrument import InstrumentTask from amt.schema.system_card import SystemCard @@ -220,12 +220,12 @@ def test_cli(capsys: pytest.CaptureFixture[str], system_card: SystemCard): def test_cli_with_exception(capsys: pytest.CaptureFixture[str], system_card: SystemCard): fetch_instruments_orig = InstrumentsService.fetch_instruments - InstrumentsService.fetch_instruments = Mock(side_effect=InstrumentError("Test error message")) + InstrumentsService.fetch_instruments = Mock(side_effect=AMTInstrumentError()) runner = CliRunner() # workaround for https://github.com/pallets/click/issues/824 with capsys.disabled() as _: result = runner.invoke(get_tasks_by_priority, ["urn:instrument:assessment", "example/system_test_card.yaml"]) # type: ignore - assert "Test error message" in result.output + assert "Sorry, an error occurre" in result.output InstrumentsService.fetch_instruments = fetch_instruments_orig diff --git a/tests/clients/test_clients.py b/tests/clients/test_clients.py index 4bdbd5c97..55e3f23db 100644 --- a/tests/clients/test_clients.py +++ b/tests/clients/test_clients.py @@ -1,12 +1,12 @@ import pytest from amt.clients.clients import get_client +from amt.core.exceptions import AMTNotFound from amt.schema.github import RepositoryContent -from httpx import HTTPStatusError from pytest_httpx import HTTPXMock def test_get_client_unknown_client(): - with pytest.raises(ValueError, match="unknown repository type: unknown_client"): + with pytest.raises(AMTNotFound, match="The requested page or resource could not be found."): get_client("unknown_client") @@ -54,11 +54,11 @@ def test_github_ratelimit_exceeded(httpx_mock: HTTPXMock): github_client = get_client("github") # when - with pytest.raises(HTTPStatusError) as exc_info: + with pytest.raises(AMTNotFound) as exc_info: github_client.get_content("https://api.github.com/stuff/123") # then - assert "Client error '403 Forbidden'" in str(exc_info.value) + assert "The requested page or resource could not be found" in str(exc_info.value) def test_get_content_github_pages(httpx_mock: HTTPXMock): diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 457e70efd..842469835 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -1,6 +1,6 @@ import pytest from amt.core.config import Settings -from amt.core.exceptions import SettingsError +from amt.core.exceptions import AMTSettingsError def test_environment_settings(monkeypatch: pytest.MonkeyPatch): @@ -28,27 +28,36 @@ def test_environment_settings(monkeypatch: pytest.MonkeyPatch): def test_environment_settings_production_sqlite_error(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("ENVIRONMENT", "production") monkeypatch.setenv("APP_DATABASE_SCHEME", "sqlite") - with pytest.raises(SettingsError) as e: + with pytest.raises(AMTSettingsError) as e: _settings = Settings(_env_file=None) # pyright: ignore [reportCallIssue] - assert e.value.message == "Settings error for options APP_DATABASE_SCHEME" + assert ( + e.value.detail + == "An error occurred while configuring the options for 'APP_DATABASE_SCHEME'. Please check the settings and try again." # noqa: E501 + ) def test_environment_settings_production_debug_error(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("ENVIRONMENT", "production") monkeypatch.setenv("DEBUG", "True") monkeypatch.setenv("APP_DATABASE_SCHEME", "postgresql") - with pytest.raises(SettingsError) as e: + with pytest.raises(AMTSettingsError) as e: _settings = Settings(_env_file=None) # pyright: ignore [reportCallIssue] - assert e.value.message == "Settings error for options DEBUG" + assert ( + e.value.detail + == "An error occurred while configuring the options for 'DEBUG'. Please check the settings and try again." + ) def test_environment_settings_production_autocreate_error(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("ENVIRONMENT", "production") monkeypatch.setenv("AUTO_CREATE_SCHEMA", "True") monkeypatch.setenv("APP_DATABASE_SCHEME", "postgresql") - with pytest.raises(SettingsError) as e: + with pytest.raises(AMTSettingsError) as e: _settings = Settings(_env_file=None) # pyright: ignore [reportCallIssue] - assert e.value.message == "Settings error for options AUTO_CREATE_SCHEMA" + assert ( + e.value.detail + == "An error occurred while configuring the options for 'AUTO_CREATE_SCHEMA'. Please check the settings and try again." # noqa: E501 + ) diff --git a/tests/core/test_exception_handlers.py b/tests/core/test_exception_handlers.py index 71856ea49..25df09941 100644 --- a/tests/core/test_exception_handlers.py +++ b/tests/core/test_exception_handlers.py @@ -1,3 +1,5 @@ +import pytest +from amt.core.exceptions import AMTCSRFProtectError from amt.schema.project import ProjectNew from fastapi import status from fastapi.testclient import TestClient @@ -20,11 +22,10 @@ def test_request_validation_exception_handler(client: TestClient): def test_request_csrf_protect_exception_handler_invalid_token_in_header(client: TestClient): data = client.get("/projects/new") new_project = ProjectNew(name="default project") - response = client.post( - "/projects/new", json=new_project.model_dump(), headers={"X-CSRF-Token": "1"}, cookies=data.cookies - ) - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert response.headers["content-type"] == "text/html; charset=utf-8" + with pytest.raises(AMTCSRFProtectError): + _response = client.post( + "/projects/new", json=new_project.model_dump(), headers={"X-CSRF-Token": "1"}, cookies=data.cookies + ) def test_http_exception_handler_htmx(client: TestClient): @@ -44,15 +45,13 @@ def test_request_validation_exception_handler_htmx(client: TestClient): def test_request_csrf_protect_exception_handler_invalid_token(client: TestClient): data = client.get("/projects/new") new_project = ProjectNew(name="default project") - response = client.post( - "/projects/new", - json=new_project.model_dump(), - headers={"HX-Request": "true", "X-CSRF-Token": "1"}, - cookies=data.cookies, - ) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert response.headers["content-type"] == "text/html; charset=utf-8" + with pytest.raises(AMTCSRFProtectError): + _response = client.post( + "/projects/new", + json=new_project.model_dump(), + headers={"HX-Request": "true", "X-CSRF-Token": "1"}, + cookies=data.cookies, + ) def test_(client: TestClient): diff --git a/tests/core/test_exceptions.py b/tests/core/test_exceptions.py index 9bcdba269..81e59b4f5 100644 --- a/tests/core/test_exceptions.py +++ b/tests/core/test_exceptions.py @@ -1,33 +1,29 @@ -from pathlib import Path - import pytest -from amt.core.exceptions import InstrumentError, RepositoryNoResultFound, SettingsError, UnsafeFileError +from amt.core.exceptions import AMTInstrumentError, AMTNotFound, AMTSettingsError def test_settings_error(): - with pytest.raises(SettingsError) as exc_info: - raise SettingsError("Wrong") + with pytest.raises(AMTSettingsError) as exc_info: + raise AMTSettingsError("Wrong") - assert exc_info.value.message == "Settings error for options Wrong" + assert ( + exc_info.value.detail + == "An error occurred while configuring the options for 'Wrong'. Please check the settings and try again." + ) def test_instrument_error(): - with pytest.raises(InstrumentError) as exc_info: - raise InstrumentError() - - assert exc_info.value.message == "Instrument error" - - -def test_unsafefile_error(): - test = Path("test") - with pytest.raises(UnsafeFileError) as exc_info: - raise UnsafeFileError(test) + with pytest.raises(AMTInstrumentError) as exc_info: + raise AMTInstrumentError() - assert exc_info.value.message == f"Unsafe file error for file {test}" + assert exc_info.value.detail == "An error occurred while processing the instrument. Please try again later." def test_RepositoryNoResultFound(): - with pytest.raises(RepositoryNoResultFound) as exc_info: - raise RepositoryNoResultFound() + with pytest.raises(AMTNotFound) as exc_info: + raise AMTNotFound() - assert exc_info.value.message == "No entity found" + assert ( + exc_info.value.detail + == "The requested page or resource could not be found. Please check the URL or query and try again." + ) diff --git a/tests/e2e/test_create_project.py b/tests/e2e/test_create_project.py index 25f1bc02c..12265a0c5 100644 --- a/tests/e2e/test_create_project.py +++ b/tests/e2e/test_create_project.py @@ -45,7 +45,7 @@ def test_e2e_create_project_invalid(page: Page): button = page.locator("#button-new-project-create") button.click() - expect(page.get_by_role("heading", name="Request Validation Error")).to_be_visible() + expect(page.get_by_text("name: String should have at")).to_be_visible() @pytest.mark.slow diff --git a/tests/repositories/test_projects.py b/tests/repositories/test_projects.py index 4ccfd39ac..e820eb33d 100644 --- a/tests/repositories/test_projects.py +++ b/tests/repositories/test_projects.py @@ -1,5 +1,5 @@ import pytest -from amt.core.exceptions import RepositoryError +from amt.core.exceptions import AMTRepositoryError from amt.repositories.projects import ProjectsRepository from tests.constants import default_project from tests.database_test_utils import DatabaseTestUtils @@ -53,7 +53,7 @@ def test_save_failed(db: DatabaseTestUtils): project_repository.save(project) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): project_repository.save(project_duplicate) project_repository.delete(project) # cleanup @@ -63,7 +63,7 @@ def test_delete_failed(db: DatabaseTestUtils): project_repository = ProjectsRepository(db.get_session()) project = default_project() - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): project_repository.delete(project) @@ -77,7 +77,7 @@ def test_find_by_id(db: DatabaseTestUtils): def test_find_by_id_failed(db: DatabaseTestUtils): project_repository = ProjectsRepository(db.get_session()) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): project_repository.find_by_id(1) @@ -164,5 +164,5 @@ def test_raises_exception(db: DatabaseTestUtils): db.given([default_project()]) project_repository = ProjectsRepository(db.get_session()) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): project_repository.paginate(skip="a", limit=3, search="") # type: ignore diff --git a/tests/repositories/test_tasks.py b/tests/repositories/test_tasks.py index b1de2816e..6ecad6419 100644 --- a/tests/repositories/test_tasks.py +++ b/tests/repositories/test_tasks.py @@ -1,5 +1,5 @@ import pytest -from amt.core.exceptions import RepositoryError +from amt.core.exceptions import AMTRepositoryError from amt.enums.status import Status from amt.models import Task from amt.repositories.tasks import TasksRepository @@ -64,7 +64,7 @@ def test_save_all_failed(db: DatabaseTestUtils): task: Task = Task(id=1, title="Test title", description="Test description", sort_order=10) tasks_repository.save_all([task]) task_duplicate: Task = Task(id=1, title="Test title duplicate", description="Test description", sort_order=10) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): tasks_repository.save_all([task_duplicate]) tasks_repository.delete(task) # cleanup @@ -87,7 +87,7 @@ def test_save_failed(db: DatabaseTestUtils): task: Task = Task(id=1, title="Test title", description="Test description", sort_order=10) tasks_repository.save(task) task_duplicate: Task = Task(id=1, title="Test title duplicate", description="Test description", sort_order=10) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): tasks_repository.save(task_duplicate) tasks_repository.delete(task) # cleanup @@ -96,7 +96,7 @@ def test_save_failed(db: DatabaseTestUtils): def test_delete_failed(db: DatabaseTestUtils): tasks_repository: TasksRepository = TasksRepository(db.get_session()) task: Task = Task(id=1, title="Test title", description="Test description", sort_order=10) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): tasks_repository.delete(task) @@ -112,7 +112,7 @@ def test_find_by_id(db: DatabaseTestUtils): def test_find_by_id_failed(db: DatabaseTestUtils): tasks_repository: TasksRepository = TasksRepository(db.get_session()) - with pytest.raises(RepositoryError): + with pytest.raises(AMTRepositoryError): tasks_repository.find_by_id(1) diff --git a/tests/services/test_instruments_service.py b/tests/services/test_instruments_service.py index 0ae6c390c..6dc8373a3 100644 --- a/tests/services/test_instruments_service.py +++ b/tests/services/test_instruments_service.py @@ -1,5 +1,5 @@ import pytest -from amt.core.exceptions import InstrumentError +from amt.core.exceptions import AMTInstrumentError from amt.services.instruments import InstrumentsService from pytest_httpx import HTTPXMock from tests.constants import GITHUB_CONTENT_PAYLOAD, GITHUB_LIST_PAYLOAD @@ -71,4 +71,4 @@ def test_fetch_instruments_invalid(httpx_mock: HTTPXMock): ) # then - pytest.raises(InstrumentError, instruments_service.fetch_instruments) + pytest.raises(AMTInstrumentError, instruments_service.fetch_instruments) diff --git a/tests/services/test_storage.py b/tests/services/test_storage.py index c404218eb..9decb2e57 100644 --- a/tests/services/test_storage.py +++ b/tests/services/test_storage.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from amt.core.exceptions import AMTKeyError, AMTValueError from amt.schema.system_card import SystemCard from amt.services.storage import StorageFactory from yaml import safe_load @@ -23,13 +24,13 @@ def test_file_system_writer_empty_yaml(setup_and_teardown: tuple[str, Path]) -> def test_file_system_writer_no_location_variable(setup_and_teardown: tuple[str, Path]) -> None: filename, _ = setup_and_teardown - with pytest.raises(KeyError, match="The `location` or `filename` variables are not provided as input for init()"): + with pytest.raises(AMTKeyError, match="Key not correct"): StorageFactory.init(storage_type="file", filename=filename) # pyright: ignore [reportCallIssue] def test_file_system_writer_no_filename_variable(setup_and_teardown: tuple[str, Path]) -> None: _, location = setup_and_teardown - with pytest.raises(KeyError, match="The `location` or `filename` variables are not provided as input for init()"): + with pytest.raises(AMTKeyError, match="Key not correct"): StorageFactory.init(storage_type="file", location=location) # pyright: ignore [reportCallIssue] @@ -71,15 +72,13 @@ def test_file_system_writer_with_system_card(setup_and_teardown: tuple[str, Path def test_abstract_writer_non_yaml_filename(setup_and_teardown: tuple[str, Path]) -> None: _, location = setup_and_teardown filename = "test.csv" - with pytest.raises( - ValueError, match=f"Filename {filename} must end with .yaml instead of .{filename.split('.')[-1]}" - ): + with pytest.raises(AMTValueError, match="Value not correct"): StorageFactory.init(storage_type="file", location=location, filename=filename) def test_not_valid_writer_type() -> None: writer_type = "kafka" - with pytest.raises(ValueError, match=f"Unknown storage type: {writer_type}"): + with pytest.raises(AMTValueError, match="Value not correct"): StorageFactory.init(storage_type=writer_type) # pyright: ignore [reportCallIssue] diff --git a/tests/site/static/templates/test_template_error.py b/tests/site/static/templates/test_template_error.py index 49727f7a3..79f17c0b1 100644 --- a/tests/site/static/templates/test_template_error.py +++ b/tests/site/static/templates/test_template_error.py @@ -7,7 +7,7 @@ def test_tempate_error(): request = default_fastapi_request() # when - response = templates.TemplateResponse(request, "errors/error.html.j2") + response = templates.TemplateResponse(request, "errors/Exception.html.j2") # then assert response.headers["Content-Type"] == "text/html; charset=utf-8" diff --git a/tests/site/static/templates/test_template_headers.py b/tests/site/static/templates/test_template_headers.py index 607d030b0..472473a7d 100644 --- a/tests/site/static/templates/test_template_headers.py +++ b/tests/site/static/templates/test_template_headers.py @@ -8,7 +8,7 @@ def test_template_caching(): # when response = templates.TemplateResponse( - request, "errors/error.html.j2", headers={"Content-Language": "This is a test"} + request, "errors/Exception.html.j2", headers={"Content-Language": "This is a test"} ) # then