From a149714aab52c93a11c442f894eb5aa9e24e6ef2 Mon Sep 17 00:00:00 2001 From: ChristopherSpelt Date: Thu, 28 Nov 2024 13:32:22 +0100 Subject: [PATCH] Add document upload --- .github/pull_request_template.md | 10 +- BUILD.md | 8 +- CODE_OF_CONDUCT.md | 30 +- CONTRIBUTING.md | 76 +- SECURITY.md | 4 +- USAGE.md | 14 +- amt/api/forms/measure.py | 54 ++ amt/api/forms/organization.py | 2 + amt/api/routes/algorithm.py | 124 +++- amt/core/config.py | 5 + amt/core/exceptions.py | 6 + amt/locale/base.pot | 103 +-- amt/locale/en_US/LC_MESSAGES/messages.po | 103 +-- amt/locale/nl_NL/LC_MESSAGES/messages.po | 109 +-- amt/schema/measure.py | 20 + amt/schema/webform.py | 40 +- amt/services/object_storage.py | 195 +++++ amt/services/task_registry.py | 4 +- amt/site/static/ts/amt.ts | 38 + .../algorithms/details_measure_modal.html.j2 | 50 +- .../algorithms/details_requirements.html.j2 | 6 +- amt/site/templates/macros/form_macros.html.j2 | 167 ++++- description.md | 2 +- package-lock.json | 695 +++++++++++------- package.json | 1 + poetry.lock | 168 ++++- pyproject.toml | 5 +- tests/api/routes/test_algorithm.py | 37 +- 28 files changed, 1501 insertions(+), 575 deletions(-) create mode 100644 amt/api/forms/measure.py create mode 100644 amt/services/object_storage.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5d19472e..ee894d73 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,8 +12,8 @@ Resolves # Please check all the boxes that apply to this pull request using "x": -- [ ] I have tested the changes locally and verified that they work as expected. -- [ ] I have followed the project's coding conventions and style guidelines. -- [ ] I have rebased my branch onto the latest commit of the main branch. -- [ ] I have squashed or reorganized my commits into logical units. -- [ ] I have read, understood and agree to the [Developer Certificate of Origin](../blob/main/DCO.md), which this project utilizes. +- [ ] I have tested the changes locally and verified that they work as expected. +- [ ] I have followed the project's coding conventions and style guidelines. +- [ ] I have rebased my branch onto the latest commit of the main branch. +- [ ] I have squashed or reorganized my commits into logical units. +- [ ] I have read, understood and agree to the [Developer Certificate of Origin](../blob/main/DCO.md), which this project utilizes. diff --git a/BUILD.md b/BUILD.md index 4942174f..5da060e5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -121,10 +121,10 @@ pip install --upgrade setuptools For testing, linting and other feature we use several tools. You can look up the documentation on how to use these: -- [pytest](https://docs.pytest.org/en/) `poetry run pytest` -- [ruff](https://docs.astral.sh/ruff/) `poetry run ruff format` or `poetry run ruff check --fix` -- [coverage](https://coverage.readthedocs.io/en/) `poetry run coverage report` -- [pyright](https://microsoft.github.io/pyright/#/) `poetry run pyright` +- [pytest](https://docs.pytest.org/en/) `poetry run pytest` +- [ruff](https://docs.astral.sh/ruff/) `poetry run ruff format` or `poetry run ruff check --fix` +- [coverage](https://coverage.readthedocs.io/en/) `poetry run coverage report` +- [pyright](https://microsoft.github.io/pyright/#/) `poetry run pyright` ## Devcontainers diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2c6dc0f7..4e2623c6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,24 +14,24 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to a positive environment for our community include: -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community Examples of unacceptable behavior include: -- The use of sexualized language or imagery, and sexual attention or - advances -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or + advances +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0425416..ffef3121 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,11 +9,11 @@ community looks forward to your contributions. 🎉 ## Table of Contents -- [Code of Conduct](#code-of-conduct) -- [I Have a Question](#i-have-a-question) -- [I Want To Contribute](#i-want-to-contribute) -- [Reporting Bugs](#reporting-bugs) -- [Suggesting Enhancements](#suggesting-enhancements) +- [Code of Conduct](#code-of-conduct) +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Enhancements](#suggesting-enhancements) ## Code of Conduct @@ -29,8 +29,8 @@ in this issue. If you then still feel the need to ask a question and need clarification, we recommend the following: -- Open an [Issue](../../issues/new). -- Provide as much context as you can about what you're running into. +- Open an [Issue](../../issues/new). +- Provide as much context as you can about what you're running into. We will then take care of the issue as soon as possible. @@ -49,11 +49,11 @@ A good bug report shouldn't leave others needing to chase you up for more inform investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. -- Make sure that you are using the latest version. -- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there - is not already a bug report existing for your bug or error in the [bug tracker](../..//issues?q=label%3Abug). -- Collect information about the bug -- Possibly your input and the output +- Make sure that you are using the latest version. +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there + is not already a bug report existing for your bug or error in the [bug tracker](../..//issues?q=label%3Abug). +- Collect information about the bug +- Possibly your input and the output #### How Do I Submit a Good Bug Report? @@ -62,21 +62,21 @@ following steps in advance to help us fix any potential bug as fast as possible. We use GitHub issues to track bugs and errors. If you run into an issue with the project: -- Open an [Issue](../../issues/new). (Since we can't be sure at this point whether it - is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) -- Explain the behavior you would expect and the actual behavior. -- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to - recreate the issue on their own. This usually includes your code. -- Provide the information you collected in the previous section. +- Open an [Issue](../../issues/new). (Since we can't be sure at this point whether it + is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to + recreate the issue on their own. This usually includes your code. +- Provide the information you collected in the previous section. Once it's filed: -- The project team will label the issue accordingly. -- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no - obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with - the `needs-repro` tag will not be addressed until they are reproduced. -- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as - `critical`), and the issue will be left to be implemented by someone. +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no + obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with + the `needs-repro` tag will not be addressed until they are reproduced. +- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as + `critical`), and the issue will be left to be implemented by someone. ### Suggesting Enhancements @@ -86,22 +86,22 @@ community to understand your suggestion and find related suggestions. #### Before Submitting an Enhancement -- Make sure that you are using the latest version. -- Perform a [search](../../issues) to see if the enhancement has already been - suggested. If it has, add a comment to the existing issue instead of opening a new one. -- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to - convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful - to the majority of our users and not just a small subset. +- Make sure that you are using the latest version. +- Perform a [search](../../issues) to see if the enhancement has already been + suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to + convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful + to the majority of our users and not just a small subset. #### How Do I Submit a Good Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](../../issues). -- Use a **clear and descriptive title** for the issue to identify the suggestion. -- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point - you can also tell which alternatives do not work for you. -- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part - which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on MacOS and - Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. -- **Explain why this enhancement would be useful** for the community. You may also want to point out the - other projects that solved it better and which could serve as inspiration. +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point + you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part + which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on MacOS and + Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +- **Explain why this enhancement would be useful** for the community. You may also want to point out the + other projects that solved it better and which could serve as inspiration. diff --git a/SECURITY.md b/SECURITY.md index 8175f08d..c8c49a1e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,5 +14,5 @@ CVSS (Common Vulnerability Scoring System) v4.0 Rating: Please report (suspected) security vulnerabilities to NCSC: -- Nederlands: **[NCSC Kwetsbaarheid melden](https://www.ncsc.nl/contact/kwetsbaarheid-melden)** -- English: **[NCSC report vulnerability](https://english.ncsc.nl/contact/reporting-a-vulnerability-cvd)** +- Nederlands: **[NCSC Kwetsbaarheid melden](https://www.ncsc.nl/contact/kwetsbaarheid-melden)** +- English: **[NCSC report vulnerability](https://english.ncsc.nl/contact/reporting-a-vulnerability-cvd)** diff --git a/USAGE.md b/USAGE.md index b53da802..d2064e6c 100644 --- a/USAGE.md +++ b/USAGE.md @@ -9,8 +9,8 @@ on [github](https://github.com/MinBZK/amt/pkgs/container/amt). You can deploy AMT to kubernetes or run the container locally using docker compose. -- Example [kubernetes](https://github.com/MinBZK/ai-validation-infra/tree/main/apps/amt) -- Example [docker compose](./compose.yml) +- Example [kubernetes](https://github.com/MinBZK/ai-validation-infra/tree/main/apps/amt) +- Example [docker compose](./compose.yml) To run amt locally create a compose.yml file and install [docker desktop](https://www.docker.com/products/docker-desktop/). Once you have install docker you can run the @@ -66,11 +66,11 @@ volumes: it is possible to run AMT with the following databases: -- SQLite (tested) -- Postgresql (tested) -- MySQL -- MariaDB -- Oracle +- SQLite (tested) +- Postgresql (tested) +- MySQL +- MariaDB +- Oracle We recommend using postgresql for production grade deployments because that one is tested in our CI/CD. By default AMT will use SQLite which will create a local database within the AMT container. diff --git a/amt/api/forms/measure.py b/amt/api/forms/measure.py new file mode 100644 index 00000000..621671bf --- /dev/null +++ b/amt/api/forms/measure.py @@ -0,0 +1,54 @@ +from gettext import NullTranslations + +from amt.schema.webform import WebForm, WebFormField, WebFormFieldType, WebFormOption, WebFormTextCloneableField + + +async def get_measure_form( + id: str, current_values: dict[str, str | list[str] | list[tuple[str, str]]], translations: NullTranslations +) -> WebForm: + _ = translations.gettext + + measure_form: WebForm = WebForm(id="", post_url="") + + measure_form.fields = [ + WebFormField( + type=WebFormFieldType.SELECT, + name="measure_state", + label=_("Status"), + options=[ + WebFormOption(value="to do", display_value="to do"), + WebFormOption(value="in progress", display_value="in progress"), + WebFormOption(value="in review", display_value="in review"), + WebFormOption(value="done", display_value="done"), + WebFormOption(value="not implemented", display_value="not implemented"), + ], + default_value=current_values.get("measure_state"), + group="1", + ), + WebFormField( + type=WebFormFieldType.TEXTAREA, + name="measure_value", + default_value=current_values.get("measure_value"), + label=_("Information on how this measure is implemented"), + placeholder=_("Describe how the measure has been implemented, including challenges and solutions."), + group="1", + ), + WebFormField( + type=WebFormFieldType.FILE, + name="measure_files", + default_value=current_values.get("measure_files"), + label=_("Add files"), + placeholder=_(""), + group="1", + ), + WebFormTextCloneableField( + clone_button_name=_("Add URI"), + name="measure_links", + default_value=current_values.get("measure_links"), + label=_("Add links to documents"), + placeholder=_("URI"), + group="1", + ), + ] + + return measure_form diff --git a/amt/api/forms/organization.py b/amt/api/forms/organization.py index 115c9679..0039b259 100644 --- a/amt/api/forms/organization.py +++ b/amt/api/forms/organization.py @@ -24,6 +24,7 @@ def get_organization_form(id: str, translations: NullTranslations, user: User | placeholder=_("Name of the organization"), attributes={"onkeyup": "amt.generate_slug('" + id + "name', '" + id + "slug')"}, group="1", + required=True, ), WebFormField( type=WebFormFieldType.TEXT, @@ -32,6 +33,7 @@ def get_organization_form(id: str, translations: NullTranslations, user: User | label=_("Slug"), placeholder=_("The slug for this organization"), group="1", + required=True, ), WebFormSearchField( name="user_ids", diff --git a/amt/api/routes/algorithm.py b/amt/api/routes/algorithm.py index 126f1d3e..f33a8ee9 100644 --- a/amt/api/routes/algorithm.py +++ b/amt/api/routes/algorithm.py @@ -5,11 +5,13 @@ from typing import Annotated, Any, cast import yaml -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, File, Form, Request, Response, UploadFile from fastapi.responses import FileResponse, HTMLResponse from pydantic import BaseModel, Field +from ulid import ULID from amt.api.deps import templates +from amt.api.forms.measure import get_measure_form from amt.api.navigation import ( BaseNavigationItem, Navigation, @@ -18,7 +20,8 @@ resolve_navigation_items, ) from amt.core.authorization import get_user -from amt.core.exceptions import AMTNotFound, AMTRepositoryError +from amt.core.exceptions import AMTError, AMTNotFound, AMTRepositoryError +from amt.core.internationalization import get_current_translation from amt.enums.status import Status from amt.models import Algorithm from amt.models.task import Task @@ -31,6 +34,7 @@ from amt.services.algorithms import AlgorithmsService from amt.services.instruments_and_requirements_state import InstrumentStateService, RequirementsStateService from amt.services.measures import MeasuresService, create_measures_service +from amt.services.object_storage import ObjectStorageService, create_object_storage_service from amt.services.organizations import OrganizationsService from amt.services.requirements import RequirementsService, create_requirements_service from amt.services.tasks import TasksService @@ -76,6 +80,20 @@ async def get_algorithm_or_error( return algorithm +def get_user_id_or_error(request: Request) -> str: + user = get_user(request) + if user is None or user["sub"] is None: + raise AMTError + return user["sub"] + + +def get_measure_task_or_error(system_card: SystemCard, measure_urn: str) -> MeasureTask: + measure_task = find_measure_task(system_card, measure_urn) + if not measure_task: + raise AMTNotFound + return measure_task + + def get_algorithm_details_tabs(request: Request) -> list[NavigationItem]: return resolve_navigation_items( [ @@ -503,17 +521,33 @@ async def get_measure( algorithm_id: int, measure_urn: str, algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], + measures_service: Annotated[MeasuresService, Depends(create_measures_service)], + object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], ) -> HTMLResponse: algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) - measures_service = create_measures_service() measure = await measures_service.fetch_measures([measure_urn]) - measure_task = find_measure_task(algorithm.system_card, measure_urn) + measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn) + + filenames: list[tuple[str, str]] = [] + for file in measure_task.files: + metadata = object_storage_service.get_file_metadata_from_object_name(file) + filenames.append((file.split("/")[-1], f"{metadata.filename}.{metadata.ext}")) + + measure_form = await get_measure_form( + id="measure_state", + current_values={ + "measure_state": measure_task.state, + "measure_value": measure_task.value, + "measure_links": measure_task.links, + "measure_files": filenames, + }, + translations=get_current_translation(request), + ) context = { "measure": measure[0], - "measure_state": measure_task.state, # pyright: ignore [reportOptionalMemberAccess] - "measure_value": measure_task.value, # pyright: ignore [reportOptionalMemberAccess] "algorithm_id": algorithm_id, + "form": measure_form, } return templates.TemplateResponse(request, "algorithms/details_measure_modal.html.j2", context) @@ -522,6 +556,8 @@ async def get_measure( class MeasureUpdate(BaseModel): measure_state: str = Field(default=None) measure_value: str = Field(default=None) + measure_links: list[str] = Field(default=[]) + measure_files: list[str] = Field(default=[]) @router.post("/{algorithm_id}/measure/{measure_urn}") @@ -529,15 +565,26 @@ async def update_measure_value( request: Request, algorithm_id: int, measure_urn: str, - measure_update: MeasureUpdate, algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], requirements_service: Annotated[RequirementsService, Depends(create_requirements_service)], + object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], + measure_state: Annotated[str, Form()], + measure_value: Annotated[str | None, Form()] = None, + measure_links: Annotated[list[str] | None, Form()] = None, + measure_files: Annotated[list[UploadFile] | None, File()] = None, ) -> HTMLResponse: algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) - - measure_task = find_measure_task(algorithm.system_card, measure_urn) - measure_task.state = measure_update.measure_state # pyright: ignore [reportOptionalMemberAccess] - measure_task.value = measure_update.measure_value # pyright: ignore [reportOptionalMemberAccess] + user_id = get_user_id_or_error(request) + measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn) + + paths = ( + object_storage_service.upload_files( + algorithm.organization_id, algorithm.id, measure_urn, user_id, measure_files + ) + if measure_files + else None + ) + measure_task.update(measure_state, measure_value, measure_links, paths) # update for the linked requirements the state based on all it's measures requirement_tasks = await find_requirement_tasks_by_measure_urn(algorithm.system_card, measure_urn) @@ -564,11 +611,6 @@ async def update_measure_value( return templates.Redirect(request, f"/algorithm/{algorithm_id}/details/system_card/requirements") -# !!! -# Implementation of this endpoint is for now independent of the algorithm ID, meaning -# that the same system card is rendered for all algorithm ID's. This is due to the fact -# that the logical process flow of a system card is not complete. -# !!! @router.get("/{algorithm_id}/details/system_card/data") async def get_system_card_data_page( request: Request, @@ -602,11 +644,6 @@ async def get_system_card_data_page( return templates.TemplateResponse(request, "algorithms/details_data.html.j2", context) -# !!! -# Implementation of this endpoint is for now independent of the algorithm ID, meaning -# that the same system card is rendered for all algorithm ID's. This is due to the fact -# that the logical process flow of a system card is not complete. -# !!! @router.get("/{algorithm_id}/details/system_card/instruments") async def get_system_card_instruments( request: Request, @@ -685,11 +722,6 @@ async def get_assessment_card( return templates.TemplateResponse(request, "pages/assessment_card.html.j2", context) -# !!! -# Implementation of this endpoint is for now independent of the algorithm ID, meaning -# that the same system card is rendered for all algorithm ID's. This is due to the fact -# that the logical process flow of a system card is not complete. -# !!! @router.get("/{algorithm_id}/details/system_card/models/{model_card}") async def get_model_card( request: Request, @@ -752,3 +784,43 @@ async def download_algorithm_system_card_as_yaml( return FileResponse(filename, filename=filename) except AMTRepositoryError as e: raise AMTNotFound from e + + +@router.get("/{algorithm_id}/file/{ulid}") +async def get_file( + request: Request, + algorithm_id: int, + ulid: ULID, + algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], + object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], +) -> Response: + algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) + file = object_storage_service.get_file(algorithm.organization_id, algorithm_id, ulid) + file_metadata = object_storage_service.get_file_metadata(algorithm.organization_id, algorithm_id, ulid) + + return Response( + content=file.read(decode_content=True), + headers={ + "Content-Disposition": f"attachment;filename={file_metadata.filename}.{file_metadata.ext}", + "Content-Type": "application/octet-stream", + }, + ) + + +@router.delete("/{algorithm_id}/file/{ulid}") +async def delete_file( + request: Request, + algorithm_id: int, + ulid: ULID, + algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], + object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], +) -> HTMLResponse: + algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) + metadata = object_storage_service.get_file_metadata(algorithm.organization_id, algorithm_id, ulid) + measure_task = get_measure_task_or_error(algorithm.system_card, metadata.measure_urn) + + entry_to_delete = object_storage_service.delete_file(algorithm.organization_id, algorithm_id, ulid) + measure_task.files.remove(entry_to_delete) + await algorithms_service.update(algorithm) + + return templates.Redirect(request, "") diff --git a/amt/core/config.py b/amt/core/config.py index 28f662df..b47b6253 100644 --- a/amt/core/config.py +++ b/amt/core/config.py @@ -63,6 +63,11 @@ class Settings(BaseSettings): TASK_REGISTRY_URL: str = "https://task-registry.apps.digilab.network" + OBJECT_STORE_URL: str = "localhost:9000" + OBJECT_STORE_USER: str = "berry" + OBJECT_STORE_PASSWORD: str = "berryberry" + OBJECT_STORE_BUCKET_NAME: str = "amt" + @computed_field def SQLALCHEMY_ECHO(self) -> bool: return self.DEBUG diff --git a/amt/core/exceptions.py b/amt/core/exceptions.py index 286035e1..9f5e18fe 100644 --- a/amt/core/exceptions.py +++ b/amt/core/exceptions.py @@ -77,3 +77,9 @@ class AMTAuthorizationFlowError(AMTHTTPException): def __init__(self) -> None: self.detail: str = _("Something went wrong during the authorization flow. Please try again later.") super().__init__(status.HTTP_401_UNAUTHORIZED, self.detail) + + +class AMTStorageError(AMTHTTPException): + def __init__(self) -> None: + self.detail: str = _("Something went wrong storing your file. PLease try again later.") + super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, self.detail) diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 5945846d..ece37261 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -198,6 +198,36 @@ msgstr "" msgid "Organization" msgstr "" +#: amt/api/forms/measure.py:17 +msgid "Status" +msgstr "" + +#: amt/api/forms/measure.py:32 +msgid "Information on how this measure is implemented" +msgstr "" + +#: amt/api/forms/measure.py:33 +msgid "" +"Describe how the measure has been implemented, including challenges and " +"solutions." +msgstr "" + +#: amt/api/forms/measure.py:40 +msgid "Add files" +msgstr "" + +#: amt/api/forms/measure.py:45 +msgid "Add URI" +msgstr "" + +#: amt/api/forms/measure.py:48 +msgid "Add links to documents" +msgstr "" + +#: amt/api/forms/measure.py:49 +msgid "URI" +msgstr "" + #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 @@ -210,30 +240,30 @@ msgstr "" msgid "Name of the organization" msgstr "" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:32 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" -#: amt/api/forms/organization.py:32 +#: amt/api/forms/organization.py:33 #: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "" -#: amt/api/forms/organization.py:33 +#: amt/api/forms/organization.py:34 msgid "The slug for this organization" msgstr "" -#: amt/api/forms/organization.py:38 +#: amt/api/forms/organization.py:40 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "" -#: amt/api/forms/organization.py:39 +#: amt/api/forms/organization.py:41 msgid "Search for a person..." msgstr "" -#: amt/api/forms/organization.py:45 +#: amt/api/forms/organization.py:47 msgid "Add organization" msgstr "" @@ -297,6 +327,10 @@ msgid "" "later." msgstr "" +#: amt/core/exceptions.py:84 +msgid "Something went wrong storing your file. PLease try again later." +msgstr "" + #: amt/site/templates/algorithms/details_base.html.j2:19 msgid "Delete algoritmic system" msgstr "" @@ -313,12 +347,14 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:153 +#: amt/site/templates/macros/form_macros.html.j2:165 #: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:163 +#: amt/site/templates/macros/form_macros.html.j2:170 #: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "" @@ -456,41 +492,12 @@ msgstr "" msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:38 -msgid "Status" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:45 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:47 -msgid "to do" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:50 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:52 -msgid "in progress" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:55 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:57 -msgid "done" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 -msgid "Information on how this measure is implemented" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:74 -msgid "" -"Describe how the measure has been implemented, including challenges and " -"solutions." -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:84 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:40 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:88 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:44 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -500,7 +507,7 @@ msgstr "" msgid "measures executed" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:55 +#: amt/site/templates/algorithms/details_requirements.html.j2:59 #: amt/site/templates/macros/editable.html.j2:24 #: amt/site/templates/macros/editable.html.j2:27 msgid "Edit" @@ -632,22 +639,34 @@ msgstr "" msgid "Algorithmic Management Toolkit (AMT)" msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid "Are you sure you want to remove " msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid " from this organization? " msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:55 +#: amt/site/templates/macros/form_macros.html.j2:61 msgid "Delete" msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:56 +#: amt/site/templates/macros/form_macros.html.j2:62 msgid "Delete member" msgstr "" +#: amt/site/templates/macros/form_macros.html.j2:139 +msgid "Saved files" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:150 +msgid "Delete file" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:157 +msgid "Are you sure you want to delete" +msgstr "" + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index c95527f3..235f9fe0 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -199,6 +199,36 @@ msgstr "" msgid "Organization" msgstr "" +#: amt/api/forms/measure.py:17 +msgid "Status" +msgstr "" + +#: amt/api/forms/measure.py:32 +msgid "Information on how this measure is implemented" +msgstr "" + +#: amt/api/forms/measure.py:33 +msgid "" +"Describe how the measure has been implemented, including challenges and " +"solutions." +msgstr "" + +#: amt/api/forms/measure.py:40 +msgid "Add files" +msgstr "" + +#: amt/api/forms/measure.py:45 +msgid "Add URI" +msgstr "" + +#: amt/api/forms/measure.py:48 +msgid "Add links to documents" +msgstr "" + +#: amt/api/forms/measure.py:49 +msgid "URI" +msgstr "" + #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 @@ -211,30 +241,30 @@ msgstr "" msgid "Name of the organization" msgstr "" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:32 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" -#: amt/api/forms/organization.py:32 +#: amt/api/forms/organization.py:33 #: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "" -#: amt/api/forms/organization.py:33 +#: amt/api/forms/organization.py:34 msgid "The slug for this organization" msgstr "" -#: amt/api/forms/organization.py:38 +#: amt/api/forms/organization.py:40 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "" -#: amt/api/forms/organization.py:39 +#: amt/api/forms/organization.py:41 msgid "Search for a person..." msgstr "" -#: amt/api/forms/organization.py:45 +#: amt/api/forms/organization.py:47 msgid "Add organization" msgstr "" @@ -298,6 +328,10 @@ msgid "" "later." msgstr "" +#: amt/core/exceptions.py:84 +msgid "Something went wrong storing your file. PLease try again later." +msgstr "" + #: amt/site/templates/algorithms/details_base.html.j2:19 msgid "Delete algoritmic system" msgstr "" @@ -314,12 +348,14 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:153 +#: amt/site/templates/macros/form_macros.html.j2:165 #: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:163 +#: amt/site/templates/macros/form_macros.html.j2:170 #: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "" @@ -457,41 +493,12 @@ msgstr "" msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:38 -msgid "Status" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:45 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:47 -msgid "to do" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:50 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:52 -msgid "in progress" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:55 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:57 -msgid "done" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 -msgid "Information on how this measure is implemented" -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:74 -msgid "" -"Describe how the measure has been implemented, including challenges and " -"solutions." -msgstr "" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:84 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:40 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:88 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:44 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -501,7 +508,7 @@ msgstr "" msgid "measures executed" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:55 +#: amt/site/templates/algorithms/details_requirements.html.j2:59 #: amt/site/templates/macros/editable.html.j2:24 #: amt/site/templates/macros/editable.html.j2:27 msgid "Edit" @@ -633,22 +640,34 @@ msgstr "" msgid "Algorithmic Management Toolkit (AMT)" msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid "Are you sure you want to remove " msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid " from this organization? " msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:55 +#: amt/site/templates/macros/form_macros.html.j2:61 msgid "Delete" msgstr "" -#: amt/site/templates/macros/form_macros.html.j2:56 +#: amt/site/templates/macros/form_macros.html.j2:62 msgid "Delete member" msgstr "" +#: amt/site/templates/macros/form_macros.html.j2:139 +msgid "Saved files" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:150 +msgid "Delete file" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:157 +msgid "Are you sure you want to delete" +msgstr "" + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "" diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index 2911f520..e2e0c364 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -201,6 +201,38 @@ msgstr "Selecteer organisatie" msgid "Organization" msgstr "Organisatie" +#: amt/api/forms/measure.py:17 +msgid "Status" +msgstr "Status" + +#: amt/api/forms/measure.py:32 +msgid "Information on how this measure is implemented" +msgstr "Informatie over hoe deze maatregel is geïmplementeerd" + +#: amt/api/forms/measure.py:33 +msgid "" +"Describe how the measure has been implemented, including challenges and " +"solutions." +msgstr "" +"Beschrijf hoe de maatregel is geimplementeerd, inclusief uitdagingen en " +"oplossingen." + +#: amt/api/forms/measure.py:40 +msgid "Add files" +msgstr "Bestanden toevoegen" + +#: amt/api/forms/measure.py:45 +msgid "Add URI" +msgstr "Voeg URI toe" + +#: amt/api/forms/measure.py:48 +msgid "Add links to documents" +msgstr "Voeg link naar bestanden toe" + +#: amt/api/forms/measure.py:49 +msgid "URI" +msgstr "URI" + #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 @@ -213,32 +245,32 @@ msgstr "Naam" msgid "Name of the organization" msgstr "Naam van de organisatie" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:32 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" "Een slug is het pad in het webadres, zoals /organizations/mijn-" "organisatie-naam" -#: amt/api/forms/organization.py:32 +#: amt/api/forms/organization.py:33 #: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "Slug" -#: amt/api/forms/organization.py:33 +#: amt/api/forms/organization.py:34 msgid "The slug for this organization" msgstr "Het web-pad (slug) voor deze organisatie" -#: amt/api/forms/organization.py:38 +#: amt/api/forms/organization.py:40 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "Voeg personen toe" -#: amt/api/forms/organization.py:39 +#: amt/api/forms/organization.py:41 msgid "Search for a person..." msgstr "Zoek een persoon..." -#: amt/api/forms/organization.py:45 +#: amt/api/forms/organization.py:47 msgid "Add organization" msgstr "Organisatie toevoegen" @@ -310,6 +342,12 @@ msgstr "" "Er is iets fout gegaan tijdens de autorisatiestroom. Probeer het later " "opnieuw" +#: amt/core/exceptions.py:84 +msgid "Something went wrong storing your file. PLease try again later." +msgstr "" +"Er is iets fout gegaan tijdens het opslaan van uw bestand. Probeer het " +"later opnieuw" + #: amt/site/templates/algorithms/details_base.html.j2:19 msgid "Delete algoritmic system" msgstr "Verwijder algoritme" @@ -328,12 +366,14 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:153 +#: amt/site/templates/macros/form_macros.html.j2:165 #: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "Ja" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:163 +#: amt/site/templates/macros/form_macros.html.j2:170 #: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "Nee" @@ -471,43 +511,12 @@ msgstr "Referenties" msgid "Read more on the algoritmekader" msgstr "Lees meer op het algoritmekader" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:38 -msgid "Status" -msgstr "Status" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:45 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:47 -msgid "to do" -msgstr "Te doen" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:50 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:52 -msgid "in progress" -msgstr "Onderhanden" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:55 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:57 -msgid "done" -msgstr "Afgerond" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 -msgid "Information on how this measure is implemented" -msgstr "Informatie over hoe deze maatregel is geïmplementeerd" - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:74 -msgid "" -"Describe how the measure has been implemented, including challenges and " -"solutions." -msgstr "" -"Beschrijf hoe de maatregel is geimplementeerd, inclusief uitdagingen " -"enoplossingen." - -#: amt/site/templates/algorithms/details_measure_modal.html.j2:84 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:40 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "Opslaan" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:88 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:44 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -517,7 +526,7 @@ msgstr "Annuleren" msgid "measures executed" msgstr "maatregelen uitgevoerd" -#: amt/site/templates/algorithms/details_requirements.html.j2:55 +#: amt/site/templates/algorithms/details_requirements.html.j2:59 #: amt/site/templates/macros/editable.html.j2:24 #: amt/site/templates/macros/editable.html.j2:27 msgid "Edit" @@ -654,22 +663,34 @@ msgstr "Er is één fout:" msgid "Algorithmic Management Toolkit (AMT)" msgstr "Algoritme Management Toolkit" -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid "Are you sure you want to remove " msgstr "Weet u zeker dat u uw algoritmische systeem wilt verwijderen " -#: amt/site/templates/macros/form_macros.html.j2:52 +#: amt/site/templates/macros/form_macros.html.j2:58 msgid " from this organization? " msgstr "Het web-pad (slug) voor deze organisatie" -#: amt/site/templates/macros/form_macros.html.j2:55 +#: amt/site/templates/macros/form_macros.html.j2:61 msgid "Delete" msgstr "Verwijder" -#: amt/site/templates/macros/form_macros.html.j2:56 +#: amt/site/templates/macros/form_macros.html.j2:62 msgid "Delete member" msgstr "Voeg personen toe" +#: amt/site/templates/macros/form_macros.html.j2:139 +msgid "Saved files" +msgstr "Opgeslagen bestanden" + +#: amt/site/templates/macros/form_macros.html.j2:150 +msgid "Delete file" +msgstr "Verwijder bestand" + +#: amt/site/templates/macros/form_macros.html.j2:157 +msgid "Are you sure you want to delete" +msgstr "Weet u zeker dat u het bestand wilt verwijderen " + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "geleden" diff --git a/amt/schema/measure.py b/amt/schema/measure.py index 2d7f396d..c55afd61 100644 --- a/amt/schema/measure.py +++ b/amt/schema/measure.py @@ -10,8 +10,28 @@ class MeasureBase(BaseModel): class MeasureTask(MeasureBase): state: str = Field(default="") value: str = Field(default="") + links: list[str] = Field(default=[]) + files: list[str] = Field(default=[]) version: str + def update( + self, + state: str | None, + value: str | None, + links: list[str] | None, + new_files: list[str] | None, + ) -> None: + if state: + self.state = state + + if value: + self.value = value + + self.links = [link for link in links if link] if links else [] + + if new_files: + self.files.extend(new_files) + class Measure(MeasureBase): name: str diff --git a/amt/schema/webform.py b/amt/schema/webform.py index 9c58ee5b..4645cce8 100644 --- a/amt/schema/webform.py +++ b/amt/schema/webform.py @@ -5,6 +5,8 @@ class WebFormFieldType(Enum): HIDDEN = "hidden" TEXT = "text" + TEXT_CLONEABLE = "text_cloneable" + FILE = "file" RADIO = "radio" SELECT = "select" TEXTAREA = "textarea" @@ -38,11 +40,12 @@ def __init__(self, type: WebFormFieldType, name: str, label: str, group: str | N class WebFormField(WebFormBaseField): placeholder: str | None - default_value: str | WebFormOption | None + default_value: str | list[str] | WebFormOption | list[tuple[str, str]] | None options: list[WebFormOption] | None validators: list[Any] description: str | None attributes: dict[str, str] | None + required: bool def __init__( self, @@ -50,11 +53,12 @@ def __init__( name: str, label: str, placeholder: str | None = None, - default_value: str | WebFormOption | None = None, + default_value: str | list[str] | list[tuple[str, str]] | WebFormOption | None = None, options: list[WebFormOption] | None = None, attributes: dict[str, str] | None = None, description: str | None = None, group: str | None = None, + required: bool = False, ) -> None: super().__init__(type=type, name=name, label=label, group=group) self.placeholder = placeholder @@ -62,6 +66,7 @@ def __init__( self.options = options self.attributes = attributes self.description = description + self.required = required class WebFormSearchField(WebFormField): @@ -75,7 +80,7 @@ def __init__( name: str, label: str, placeholder: str | None = None, - default_value: str | None | WebFormOption = None, + default_value: str | list[str] | list[tuple[str, str]] | WebFormOption | None = None, options: list[WebFormOption] | None = None, attributes: dict[str, str] | None = None, group: str | None = None, @@ -96,6 +101,35 @@ def __init__( self.query_var_name = query_var_name +class WebFormTextCloneableField(WebFormField): + clone_button_name: str + + def __init__( + self, + clone_button_name: str, + name: str, + label: str, + placeholder: str | None = None, + default_value: str | list[str] | list[tuple[str, str]] | None = None, + options: list[WebFormOption] | None = None, + attributes: dict[str, str] | None = None, + group: str | None = None, + description: str | None = None, + ) -> None: + super().__init__( + type=WebFormFieldType.TEXT_CLONEABLE, + name=name, + label=label, + placeholder=placeholder, + default_value=default_value, + options=options, + attributes=attributes, + group=group, + description=description, + ) + self.clone_button_name = clone_button_name + + class WebForm: id: str legend: str | None diff --git a/amt/services/object_storage.py b/amt/services/object_storage.py new file mode 100644 index 00000000..6234d6ca --- /dev/null +++ b/amt/services/object_storage.py @@ -0,0 +1,195 @@ +import logging +import os +from collections.abc import Iterable +from datetime import UTC, datetime + +from fastapi import UploadFile +from minio import Minio +from minio.datatypes import Object +from ulid import ULID +from urllib3 import BaseHTTPResponse + +from amt.core.config import get_settings +from amt.core.exceptions import AMTStorageError +from amt.schema.shared import BaseModel + +logger = logging.getLogger(__name__) + + +class ObjectMetadata(BaseModel): + """ + Metadata for files in the object storage. + """ + + algorithm_id: str + user_id: str + measure_urn: str + timestamp: str + filename: str + ext: str + + +class MinioMetadataExtractor: + @staticmethod + def from_file_upload(filename: str, algorithm_id: str, measure_urn: str, user_id: str) -> ObjectMetadata: + """ + Extract metadata for a file. + """ + filename, ext = os.path.splitext(filename) if filename else ("", "") + ext = ext.replace(".", "") + return ObjectMetadata( + algorithm_id=algorithm_id, + user_id=user_id, + measure_urn=measure_urn, + timestamp=datetime.now(UTC).isoformat(), + filename=filename, + ext=ext, + ) + + @staticmethod + def from_object(object: Object) -> ObjectMetadata: + """ + Extract metadata from a Minio object. + """ + return ObjectMetadata( + algorithm_id=object.metadata["X-Amz-Meta-Algorithm_id"] if object.metadata else "", + user_id=object.metadata["X-Amz-Meta-User_id"] if object.metadata else "", + measure_urn=object.metadata["X-Amz-Meta-Measure_urn"] if object.metadata else "", + timestamp=object.metadata["X-Amz-Meta-Timestamp"] if object.metadata else "", + filename=object.metadata["X-Amz-Meta-Filename"] if object.metadata else "", + ext=object.metadata["X-Amz-Meta-Ext"] if object.metadata else "", + ) + + +class ObjectStorageService: + def __init__( + self, + minio_client: Minio, + metadata_extractor: MinioMetadataExtractor, + bucket_name: str, + ) -> None: + self.client = minio_client + self.bucket_name = bucket_name + self._ensure_bucket_exists() + self.metadata_extractor = metadata_extractor + + def _ensure_bucket_exists(self) -> None: + """ + Validate bucket existence, raising an error if not found. + """ + if not self.client.bucket_exists(self.bucket_name): + logger.exception("Bucket in object storage does not exist.") + raise AMTStorageError() + + def _generate_destination_path( + self, organization_id: str | int, algorithm_id: str | int, ulid: ULID | None = None + ) -> str: + """ + Generate a unique destination path for file to upload if no ULID is passed. + Returns the destination path of a file with given ULID if a ULID is passed. + """ + ulid = ulid if ulid else ULID() + return f"uploads/org/{organization_id}/algorithm/{algorithm_id}/{ulid}" + + def get_file_metadata_from_object_name(self, object_name: str) -> ObjectMetadata: + """ + Gets the object metadata for a file stored under object_name. + """ + try: + stats = self.client.stat_object(self.bucket_name, object_name) + except Exception as err: + logger.exception("Could not retrieve file metadata from object store") + raise AMTStorageError() from err + + return self.metadata_extractor.from_object(stats) + + def get_file_metadata(self, organization_id: str | int, algorithm_id: str | int, ulid: ULID) -> ObjectMetadata: + """ + Gets the object metadata for a file with given organization_id, algorithm_id and ULID. + """ + path = self._generate_destination_path(organization_id, algorithm_id, ulid) + return self.get_file_metadata_from_object_name(path) + + def upload_file( + self, + organization_id: str | int, + algorithm_id: str | int, + measure_urn: str, + user_id: str, + file: UploadFile, + ) -> str: + """ + Uploads a single file to the object storage and returns the path (object name) + where the file is stored. + """ + if not file.size: + logger.exception("User provided upload file is empty.") + raise AMTStorageError() + + destination_path = self._generate_destination_path(organization_id, algorithm_id) + filename = file.filename if file.filename else "unnamed.unknown" + metadata = self.metadata_extractor.from_file_upload(filename, str(algorithm_id), measure_urn, user_id) + + try: + self.client.put_object( + self.bucket_name, destination_path, file.file, file.size, metadata=metadata.model_dump() + ) + except Exception as err: + logger.exception("Cannot upload file to object storage.") + raise AMTStorageError() from err + + return destination_path + + def upload_files( + self, + organization_id: str | int, + algorithm_id: str | int, + measure_urn: str, + user_id: str, + files: Iterable[UploadFile], + ) -> list[str]: + """ + Uploads multiple files to the object storage and returns the paths (object names) + where the files are stored. + """ + return [self.upload_file(organization_id, algorithm_id, measure_urn, user_id, file) for file in files] + + def get_file(self, organization_id: str | int, algorithm_id: str | int, ulid: ULID) -> BaseHTTPResponse: + """ + Gets a file from the object storage. + """ + path = self._generate_destination_path(organization_id, algorithm_id, ulid) + try: + file = self.client.get_object(self.bucket_name, path) + except Exception as err: + logger.exception("Cannot get file from object storage.") + raise AMTStorageError() from err + return file + + def delete_file(self, organization_id: str | int, algorithm_id: str | int, ulid: ULID) -> str: + """ + Deletes a file from the object storage. + """ + + path = self._generate_destination_path(organization_id, algorithm_id, ulid) + try: + self.client.remove_object(self.bucket_name, path) + except Exception as err: + logger.exception("Cannot delete file from object storage.") + raise AMTStorageError() from err + return path + + +def create_object_storage_service( + url: str = get_settings().OBJECT_STORE_URL, + username: str = get_settings().OBJECT_STORE_USER, + password: str = get_settings().OBJECT_STORE_PASSWORD, + bucket_name: str = get_settings().OBJECT_STORE_BUCKET_NAME, + secure: bool = False, +) -> ObjectStorageService: + """ + Creates an instance of the ObjectStorageService. + """ + metadata_extractor = MinioMetadataExtractor() + client = Minio(endpoint=url, access_key=username, secret_key=password, secure=secure) + return ObjectStorageService(client, metadata_extractor, bucket_name=bucket_name) diff --git a/amt/services/task_registry.py b/amt/services/task_registry.py index 83489d39..4d909e56 100644 --- a/amt/services/task_registry.py +++ b/amt/services/task_registry.py @@ -67,7 +67,9 @@ async def get_requirements_and_measures( for measure_urn in requirement.links: if measure_urn not in measure_urns: measure = await measure_service.fetch_measures(measure_urn) - applicable_measures.append(MeasureTask(urn=measure_urn, version=measure[0].schema_version)) + applicable_measures.append( + MeasureTask(urn=measure_urn, state="to do", version=measure[0].schema_version) + ) measure_urns.add(measure_urn) return applicable_requirements, applicable_measures diff --git a/amt/site/static/ts/amt.ts b/amt/site/static/ts/amt.ts index 2354ec4c..429ed072 100644 --- a/amt/site/static/ts/amt.ts +++ b/amt/site/static/ts/amt.ts @@ -388,3 +388,41 @@ export function add_field_on_enter(id: string) { show_form_search_options(id); } } + +export function createLinkComponent(template_id: string, element_id: string) { + const template = document.getElementById(template_id) as HTMLTemplateElement; + const item = document.getElementById(element_id); + const link = template?.content.cloneNode(true); + if (link) { + item?.appendChild(link); + } +} + +export function getFiles(element: HTMLInputElement, target_id: string) { + if (element.files) { + const list = document.getElementById(target_id) as HTMLElement; + list.innerHTML = ""; + for (const file of element.files) { + const li = document.createElement("li"); + li.className = "rvo-item-list__item"; + + const container = document.createElement("div"); + container.className = "rvo-layout-row rvo-layout-gap--sm"; + + const icon_el = document.createElement("span"); + icon_el.className = + "utrecht-icon rvo-icon rvo-icon-document-met-lijnen rvo-icon--md rvo-icon--hemelblauw"; + icon_el.role = "img"; + icon_el.ariaLabel = "File"; + container.appendChild(icon_el); + + const text_el = document.createElement("span"); + text_el.textContent = file.name; + container.appendChild(text_el); + + li.appendChild(container); + + list.appendChild(li); + } + } +} diff --git a/amt/site/templates/algorithms/details_measure_modal.html.j2 b/amt/site/templates/algorithms/details_measure_modal.html.j2 index 9b93cd98..c192111d 100644 --- a/amt/site/templates/algorithms/details_measure_modal.html.j2 +++ b/amt/site/templates/algorithms/details_measure_modal.html.j2 @@ -1,8 +1,9 @@ +{% import "macros/form_macros.html.j2" as macros with context %}

{{ measure.name }}

-
- {# Description #}
@@ -30,51 +30,7 @@
- {# Status#} -
-
- -
-
- -
-
- {# Implementation#} -
-
- -
- -
+ {% for form_field in form.fields %}{{ macros.form_field(form.id, form_field) }}{% endfor %}

diff --git a/amt/site/templates/algorithms/details_requirements.html.j2 b/amt/site/templates/algorithms/details_requirements.html.j2 index 1ca02273..485f90d9 100644 --- a/amt/site/templates/algorithms/details_requirements.html.j2 +++ b/amt/site/templates/algorithms/details_requirements.html.j2 @@ -31,11 +31,15 @@

{% if measure.state == "to do" %} -
+
{% elif measure.state == "in progress" %} +
+ {% elif measure.state == "in review" %}
{% elif measure.state == "done" %}
+ {% elif measure.state == "not implemented" %} +
{% else %} {# This should not happen, red could be for overdue.#}
diff --git a/amt/site/templates/macros/form_macros.html.j2 b/amt/site/templates/macros/form_macros.html.j2 index 0857f49b..4e3aeb54 100644 --- a/amt/site/templates/macros/form_macros.html.j2 +++ b/amt/site/templates/macros/form_macros.html.j2 @@ -22,6 +22,12 @@ {% macro form_field(form, field) %} {% if field.type == WebFormFieldType.TEXT %} {{ form_field_text(form, field) }} + {% elif field.type == WebFormFieldType.FILE %} + {{ form_field_file(form, field) }} + {% elif field.type == WebFormFieldType.TEXT_CLONEABLE %} + {{ form_field_text_cloneable(form, field) }} + {% elif field.type == WebFormFieldType.TEXTAREA %} + {{ form_field_textarea(form, field) }} {% elif field.type == WebFormFieldType.SELECT %} {{ form_field_select(form, field) }} {% elif field.type == WebFormFieldType.SEARCH_SELECT %} @@ -87,7 +93,7 @@
+ class="rvo-label {% if field.required %}rvo-label--required{% endif %}">{{ field.label }} {% if field.description %}
{{ field.description }}
@@ -109,6 +115,88 @@
{% endmacro %} +{% macro form_field_file(prefix, field) %} +
+
+ + {% if field.description %}
{{ field.description }}
{% endif %} +
+
+ + {% if field.default_value %} +
+
+ +
    + {% for ulid, file in field.default_value %} + +
  • +
    +
    + + {{ file }} +
    + + + +
    +
  • + {% endfor %} +
+
+
+ {% endif %} +
+{% endmacro %} {% macro form_field_text(prefix, field) %}
+ class="rvo-label {% if field.required %}rvo-label--required{% endif %}">{{ field.label }} {% if field.description %}
{{ field.description }}
{% endif %}
- +
{% endmacro %} -{% macro form_field_search(prefix, field) %} +{% macro form_field_text_cloneable(prefix, field) %}
+ for="{{ prefix }}{{ field.name }}" + class="rvo-label {% if field.required %}rvo-label--required{% endif %}">{{ field.label }} {% if field.description %}
{{ field.description }}
{% endif %}
+
+ {% if isinstance(field.default_value, 'list') %} + {% for value in field.default_value %} +
+
+ + + + +
+
+ {% endfor %} + {% endif %} + +
+
+ + {{ field.clone_button_name }} +
+
+{% endmacro %} +{% macro form_field_textarea(prefix, field) %} +
+
+ + {% if field.description %}
{{ field.description }}
{% endif %} +
+
+ +
+{% endmacro %} +{% macro form_field_search(prefix, field) %} +
+
+ + {% if field.description %} +
{{ field.description }}
+ {% endif %} +
+
=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -728,6 +738,17 @@ "@csstools/css-tokenizer": "^3.0.3" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1460,26 +1481,6 @@ "@parcel/watcher-win32-x64": "2.4.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", - "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", @@ -1500,66 +1501,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", - "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", - "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", - "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-linux-arm64-glibc": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", @@ -1600,106 +1541,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", - "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", - "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", - "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", - "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", - "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1903,6 +1744,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1919,17 +1766,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1944,25 +1791,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MITClause", "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4" }, "engines": { @@ -1973,23 +1816,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2000,14 +1839,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2019,18 +1858,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", "dev": true, "license": "MIT", "engines": { @@ -2042,14 +1877,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2064,10 +1899,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2097,16 +1930,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2116,22 +1949,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/types": "8.18.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2534,6 +2363,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2742,6 +2577,26 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/build": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/build/-/build-0.1.4.tgz", + "integrity": "sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==", + "dependencies": { + "cssmin": "0.3.x", + "jsmin": "1.x", + "jxLoader": "*", + "moo-server": "*", + "promised-io": "*", + "timespan": "2.x", + "uglify-js": "1.x", + "walker": "1.x", + "winston": "*", + "wrench": "1.3.x" + }, + "engines": { + "node": ">v0.4.12" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3020,6 +2875,16 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3035,8 +2900,32 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/colord": { "version": "2.9.3", @@ -3050,6 +2939,16 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -3235,6 +3134,14 @@ "node": ">=4" } }, + "node_modules/cssmin": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssmin/-/cssmin-0.3.2.tgz", + "integrity": "sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==", + "bin": { + "cssmin": "bin/cssmin" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3458,6 +3365,12 @@ "node": ">= 4" } }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -3863,6 +3776,12 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3931,6 +3850,12 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4521,8 +4446,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -4666,7 +4590,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -5340,6 +5263,18 @@ "node": ">=4" } }, + "node_modules/jsmin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsmin/-/jsmin-1.0.1.tgz", + "integrity": "sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==", + "license": "Doug Crockford's license that allows this module to be used for Good but not for Evil", + "bin": { + "jsmin": "bin/jsmin" + }, + "engines": { + "node": ">=0.1.93" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5376,6 +5311,29 @@ "node": ">=6" } }, + "node_modules/jxLoader": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jxLoader/-/jxLoader-0.1.1.tgz", + "integrity": "sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==", + "dependencies": { + "js-yaml": "0.3.x", + "moo-server": "1.3.x", + "promised-io": "*", + "walker": "1.x" + }, + "engines": { + "node": ">v0.4.10" + } + }, + "node_modules/jxLoader/node_modules/js-yaml": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-0.3.7.tgz", + "integrity": "sha512-/7PsVDNP2tVe2Z1cF9kTEkjamIwz4aooDpRKmN1+g/9eePCgcxsv4QDvEbxO0EH+gdDD7MLyDoR6BASo3hH51g==", + "license": "MIT", + "engines": { + "node": "> 0.4.11" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5409,6 +5367,12 @@ "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5808,6 +5772,23 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -5851,7 +5832,6 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, "dependencies": { "tmpl": "1.0.5" } @@ -6003,11 +5983,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/moo-server": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz", + "integrity": "sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==", + "engines": { + "node": ">v0.4.10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -6119,6 +6106,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -6662,6 +6658,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/promised-io": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/promised-io/-/promised-io-0.3.6.tgz", + "integrity": "sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6735,6 +6736,20 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", @@ -6966,7 +6981,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -6982,6 +6996,15 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/sass": { "version": "1.81.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", @@ -7155,6 +7178,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -7237,6 +7275,15 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7258,6 +7305,15 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -7598,13 +7654,13 @@ "dev": true }, "node_modules/stylelint/node_modules/css-tree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz", - "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.1", + "mdn-data": "2.12.2", "source-map-js": "^1.0.1" }, "engines": { @@ -7697,9 +7753,9 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/mdn-data": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz", - "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true, "license": "CC0-1.0" }, @@ -8042,11 +8098,24 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/timespan": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz", + "integrity": "sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==", + "engines": { + "node": ">= 0.2.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -8069,6 +8138,15 @@ "node": ">=8.0" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -8208,15 +8286,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8226,12 +8304,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/uglify-js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz", + "integrity": "sha512-YPX1DjKtom8l9XslmPFQnqWzTBkvI4N0pbkzLuPZZ4QTyig0uQqvZz9NgUdfEV+qccJzi7fVcGWdESvRIjWptQ==", + "bin": { + "uglifyjs": "bin/uglifyjs" } }, "node_modules/undici-types": { @@ -8282,8 +8364,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utila": { "version": "0.4.0", @@ -8315,7 +8396,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, "dependencies": { "makeerror": "1.0.12" } @@ -8517,6 +8597,42 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8588,6 +8704,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/wrench": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz", + "integrity": "sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==", + "deprecated": "wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years.", + "engines": { + "node": ">=0.1.97" + } + }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", diff --git a/package.json b/package.json index b9236b7a..f83eb2d1 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@nl-rvo/assets": "1.0.0-alpha.360", "@nl-rvo/component-library-css": "2.1.0", "@nl-rvo/design-tokens": "1.4.3", + "build": "^0.1.4", "htmx.org": "^1.9.12", "hyperscript.org": "^0.9.13", "reset-css": "^5.0.2", diff --git a/poetry.lock b/poetry.lock index 41123842..0885554c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -68,6 +68,63 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + [[package]] name = "async-lru" version = "2.0.4" @@ -1132,6 +1189,24 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "minio" +version = "7.2.12" +description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" +optional = false +python-versions = ">=3.9" +files = [ + {file = "minio-7.2.12-py3-none-any.whl", hash = "sha256:4b63370ca83f82c23e6fb0a094a1e2b08b275884ae43f6a90c4388a45633e3f5"}, + {file = "minio-7.2.12.tar.gz", hash = "sha256:2a3fcf4ab753824de8ae3ffeb14da33d6ad416f83a7e82363a27b34da8e91f27"}, +] + +[package.dependencies] +argon2-cffi = "*" +certifi = "*" +pycryptodome = "*" +typing-extensions = "*" +urllib3 = "*" + [[package]] name = "multidict" version = "6.1.0" @@ -1551,6 +1626,47 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pycryptodome" +version = "3.21.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +] + [[package]] name = "pydantic" version = "2.9.2" @@ -1806,6 +1922,26 @@ pytest = "==8.*" [package.extras] testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==5.*)"] +[[package]] +name = "pytest-minio-mock" +version = "0.4.16" +description = "A pytest plugin for mocking Minio S3 interactions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_minio_mock-0.4.16-py3-none-any.whl", hash = "sha256:f97c9262b0efc572a1efbc0ed03c988880b87f4387f964140565b0d5237f1cc2"}, + {file = "pytest_minio_mock-0.4.16.tar.gz", hash = "sha256:37e406edc6384243b703f85c15040f427efa519bf35a7cec19cd8ac92bca4370"}, +] + +[package.dependencies] +minio = "*" +pytest = ">=5.0.0" +pytest-mock = "*" +validators = "*" + +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -1868,6 +2004,17 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-multipart" +version = "0.0.19" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, + {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, +] + [[package]] name = "python-slugify" version = "8.0.4" @@ -1896,6 +2043,9 @@ files = [ {file = "python_ulid-3.0.0.tar.gz", hash = "sha256:e50296a47dc8209d28629a22fc81ca26c00982c78934bd7766377ba37ea49a9f"}, ] +[package.dependencies] +pydantic = {version = ">=2.0", optional = true, markers = "extra == \"pydantic\""} + [package.extras] pydantic = ["pydantic (>=2.0)"] @@ -2498,6 +2648,20 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "validators" +version = "0.34.0" +description = "Python Data Validation for Humans™" +optional = false +python-versions = ">=3.8" +files = [ + {file = "validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321"}, + {file = "validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f"}, +] + +[package.extras] +crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] + [[package]] name = "vcrpy" version = "6.0.2" @@ -2875,4 +3039,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "1ca5edd8d5216cbd1a01877d8f90769a8f3542146048813b0c373cae5c7c2aa3" +content-hash = "b5d1556f42ae97f95795689bb58a0ecde019e36e2d869ff926c40b004802f657" diff --git a/pyproject.toml b/pyproject.toml index 8cefeab5..c0eaaa40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ babel = "^2.15.0" httpx = "^0.27.2" pyyaml-include = "^2.2" click = "^8.1.7" -python-ulid = "^3.0.0" +python-ulid = {extras = ["pydantic"], version = "^3.0.0"} fastapi-csrf-protect = "^0.3.4" sqlalchemy = {extras = ["asyncio"], version = "^2.0.36"} sqlalchemy-utils = "^0.41.2" @@ -44,6 +44,8 @@ aiosqlite = "^0.20.0" asyncpg = "^0.30.0" async-lru = "^2.0.4" jinja2-base64-filters = "^0.1.4" +python-multipart = "^0.0.19" +minio = "^7.2.12" [tool.poetry.group.test.dependencies] @@ -57,6 +59,7 @@ pytest-playwright = "^0.5.2" pytest-httpx = "^0.34.0" freezegun = "^1.5.1" vcrpy = "^6.0.2" +pytest-minio-mock = "^0.4.16" [tool.poetry.group.dev.dependencies] diff --git a/tests/api/routes/test_algorithm.py b/tests/api/routes/test_algorithm.py index d537fb4c..d091a6da 100644 --- a/tests/api/routes/test_algorithm.py +++ b/tests/api/routes/test_algorithm.py @@ -3,7 +3,6 @@ import pytest import vcr # type: ignore from amt.api.routes.algorithm import ( - MeasureUpdate, find_measure_task, find_requirement_task, find_requirement_tasks_by_measure_urn, @@ -11,10 +10,13 @@ get_algorithm_or_error, set_path, ) +from amt.core.config import get_settings from amt.core.exceptions import AMTNotFound, AMTRepositoryError from amt.models import Algorithm from amt.schema.task import MovedTask from httpx import AsyncClient +from minio import Minio +from pytest_minio_mock.plugin import MockMinioClient from pytest_mock import MockFixture from tests.api.routes.test_algorithms import MockRequest @@ -547,10 +549,19 @@ async def test_find_requirement_tasks_by_measure_urn() -> None: @pytest.mark.asyncio -async def test_get_measure(client: AsyncClient, db: DatabaseTestUtils) -> None: +async def test_get_measure(minio_mock: MockMinioClient, client: AsyncClient, db: DatabaseTestUtils) -> None: # given await db.given([default_user(), default_algorithm_with_system_card("testalgorithm1")]) + # Need to make bucket in object store. The Minio class is mocked by minio_mock. + storage_client = Minio( + endpoint=get_settings().OBJECT_STORE_URL, + access_key=get_settings().OBJECT_STORE_USER, + secret_key=get_settings().OBJECT_STORE_PASSWORD, + secure=False, + ) + storage_client.make_bucket(get_settings().OBJECT_STORE_BUCKET_NAME) + # when response = await client.get("/algorithm/1/measure/urn:nl:ak:mtr:dat-01") @@ -561,16 +572,34 @@ async def test_get_measure(client: AsyncClient, db: DatabaseTestUtils) -> None: @pytest.mark.asyncio -async def test_update_measure_value(client: AsyncClient, mocker: MockFixture, db: DatabaseTestUtils) -> None: +async def test_update_measure_value( + minio_mock: MockMinioClient, client: AsyncClient, mocker: MockFixture, db: DatabaseTestUtils +) -> None: # given await db.given([default_user(), default_algorithm_with_system_card("testalgorithm1")]) client.cookies["fastapi-csrf-token"] = "1" mocker.patch("fastapi_csrf_protect.CsrfProtect.validate_csrf", new_callable=mocker.AsyncMock) + mocker.patch("amt.api.routes.algorithm.get_user_id_or_error", return_value=default_user().id) + + # Need to make bucket in object store. The Minio class is mocked by minio_mock. + storage_client = Minio( + endpoint=get_settings().OBJECT_STORE_URL, + access_key=get_settings().OBJECT_STORE_USER, + secret_key=get_settings().OBJECT_STORE_PASSWORD, + secure=False, + ) + storage_client.make_bucket(get_settings().OBJECT_STORE_BUCKET_NAME) # happy flow response = await client.post( "/algorithm/1/measure/urn:nl:ak:mtr:dat-01", - json={"measure_update": MeasureUpdate(measure_state="done", measure_value="something").model_dump()}, + data={ + "measure_state": "done", + "measure_value": "something", + "measure_links": [], + "existing_file_names": [], + "measure_files": [], + }, headers={"X-CSRF-Token": "1"}, ) assert response.status_code == 200