def test_storing_and_receiving(clean_test_db_engine):
    with mock.patch(
        "hetdesrun.persistence.dbservice.revision.Session",
        sessionmaker(clean_test_db_engine),
    ):
        tr_uuid = get_uuid_from_seed("test_storing_and_receiving")

        tr_object = TransformationRevision(
            id=tr_uuid,
            revision_group_id=tr_uuid,
            name="Test",
            description="Test description",
            version_tag="1.0.0",
            category="Test category",
            state=State.DRAFT,
            type=Type.COMPONENT,
            content="code",
            io_interface=IOInterface(),
            test_wiring=WorkflowWiring(),
            documentation="",
        )

        store_single_transformation_revision(tr_object)

        received_tr_object = read_single_transformation_revision(tr_uuid)

        assert tr_object == received_tr_object

        # non-existent object
        wrong_tr_uuid = get_uuid_from_seed("wrong id")
        with pytest.raises(DBNotFoundError):
            received_tr_object = read_single_transformation_revision(wrong_tr_uuid)
def test_deleting(clean_test_db_engine):
    with mock.patch(
        "hetdesrun.persistence.dbservice.revision.Session",
        sessionmaker(clean_test_db_engine),
    ):
        tr_draft_uuid = get_uuid_from_seed("draft")

        tr_draft_object = TransformationRevision(
            id=tr_draft_uuid,
            revision_group_id=tr_draft_uuid,
            name="Test",
            description="Test description",
            version_tag="1.0.0",
            category="Test category",
            state=State.DRAFT,
            type=Type.COMPONENT,
            content="code",
            io_interface=IOInterface(),
            test_wiring=WorkflowWiring(),
            documentation="",
        )

        tr_released_uuid = get_uuid_from_seed("released")

        tr_released_object = TransformationRevision(
            id=tr_released_uuid,
            revision_group_id=tr_released_uuid,
            name="Test",
            description="Test description",
            version_tag="1.0.0",
            category="Test category",
            released_timestamp="2021-12-24 00:00",
            state=State.RELEASED,
            type=Type.COMPONENT,
            content="code",
            io_interface=IOInterface(),
            test_wiring=WorkflowWiring(),
            documentation="",
        )

        store_single_transformation_revision(tr_draft_object)
        store_single_transformation_revision(tr_released_object)

        delete_single_transformation_revision(tr_draft_uuid)

        with pytest.raises(DBNotFoundError):
            read_single_transformation_revision(tr_draft_uuid)

        with pytest.raises(DBBadRequestError):
            delete_single_transformation_revision(tr_released_uuid)
Exemple #3
0
async def delete_documentation(
    # pylint: disable=redefined-builtin
    id: UUID, ) -> None:
    """Change the documentation of a transformation revision in the data base to "".

    This endpoint is deprecated and will be removed soon,
    use PUT /api/transformations/{id} instead
    """

    logger.info("delete documentation %s", id)

    try:
        transformation_revision = read_single_transformation_revision(id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    transformation_revision.documentation = ""

    try:
        update_or_create_single_transformation_revision(
            transformation_revision)
        logger.info("deleted documentation {id}")
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e
Exemple #4
0
async def get_component_revision_by_id(
    # pylint: disable=redefined-builtin
    id: UUID = Path(
        ...,
        example=UUID("123e4567-e89b-12d3-a456-426614174000"),
    ),
) -> DocumentationFrontendDto:
    """Get the documentation of a transformation revision by its id from the data base.

    This endpoint is deprecated and will be removed soon,
    use GET /api/transformations/{id} instead.
    """

    logger.info("get documentation %s", id)

    try:
        transformation_revision = read_single_transformation_revision(id)
        logger.info("found documentation with id %s", id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    documentation_dto = DocumentationFrontendDto.from_transformation_revision(
        transformation_revision)
    logger.debug(documentation_dto.json())

    return documentation_dto
async def get_component_revision_by_id(
    # pylint: disable=redefined-builtin
    id: UUID = Path(
        ...,
        example=UUID("123e4567-e89b-12d3-a456-426614174000"),
    ),
) -> ComponentRevisionFrontendDto:
    """Get a single transformation revision of type component from the data base.

    This endpoint is deprecated and will be removed soon,
    use GET /api/transformations/{id} instead.
    """

    logger.info("get component %s", id)

    try:
        transformation_revision = read_single_transformation_revision(id)
        logger.info("found component with id %s", id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    if transformation_revision.type != Type.COMPONENT:
        msg = f"DB entry for id {id} does not have type {Type.COMPONENT}"
        logger.error(msg)
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=msg)

    component_dto = ComponentRevisionFrontendDto.from_transformation_revision(
        transformation_revision)
    logger.debug(component_dto.json())

    return component_dto
async def create_transformation_revision(
    transformation_revision: TransformationRevision,
) -> TransformationRevision:
    """Store a transformation revision in the data base."""
    logger.info("create transformation revision %s", transformation_revision.id)

    if transformation_revision.type == Type.COMPONENT:
        logger.debug("transformation revision has type %s", Type.COMPONENT)
        transformation_revision.content = update_code(transformation_revision)
        logger.debug("generated code:\n%s", transformation_revision.content)

    try:
        store_single_transformation_revision(transformation_revision)
        logger.info("created transformation revision")
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e

    try:
        persisted_transformation_revision = read_single_transformation_revision(
            transformation_revision.id
        )
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    logger.debug(persisted_transformation_revision.json())

    return persisted_transformation_revision
async def test_deprecate_base_item_of_type_component(async_test_client,
                                                     clean_test_db_engine):
    with mock.patch(
            "hetdesrun.persistence.dbservice.revision.Session",
            sessionmaker(clean_test_db_engine),
    ):
        store_single_transformation_revision(
            TransformationRevisionFrontendDto(
                **tr_dto_json_component_2).to_transformation_revision())

        async with async_test_client as ac:
            response = await ac.put(
                "/api/base-items/" + str(tr_dto_json_component_2["id"]),
                json=tr_dto_json_component_2_deprecate,
            )

        assert response.status_code == 201
        assert response.json()["state"] == "DISABLED"
        assert response.json()["name"] != "new name"
        assert response.json()["category"] != "Test"
        assert len(response.json()["inputs"]) == 0

        tr_component_2 = read_single_transformation_revision(
            tr_dto_json_component_2["id"])

        assert tr_component_2.state.value == "DISABLED"
        assert tr_component_2.name != "new name"
        assert "disabled_timestamp" in tr_component_2.content
        assert "released_timestamp" in tr_component_2.content
Exemple #8
0
async def test_put_component_transformation_without_update_code(
        async_test_client, clean_test_db_engine):
    patched_session = sessionmaker(clean_test_db_engine)
    with mock.patch(
            "hetdesrun.persistence.dbservice.revision.Session",
            patched_session,
    ):

        path = "./tests/data/components/alerts-from-score_100_38f168ef-cb06-d89c-79b3-0cd823f32e9d.json"
        example_component_tr_json = load_json(path)

        async with async_test_client as ac:
            response = await ac.put(
                posix_urljoin("/api/transformations/",
                              example_component_tr_json["id"]) +
                "?update_component_code=False",
                json=example_component_tr_json,
            )

        component_tr_in_db = read_single_transformation_revision(
            example_component_tr_json["id"])

        assert response.status_code == 201
        assert "COMPONENT_INFO" not in response.json()["content"]
        assert "COMPONENT_INFO" not in component_tr_in_db.content
        assert "register" in response.json()["content"]
        assert "register" in component_tr_in_db.content
Exemple #9
0
def prepare_execution_input(
        exec_by_id_input: ExecByIdInput) -> WorkflowExecutionInput:
    """Loads trafo revision and prepares execution input from it.

    Loads the trafo revision specified by id and prepares
    an workflow execution input object which can be executed by the runtime
    -- either code or by calling runtime rest endpoint for running
    workflows.

    Note that trafo revisions of type components will be wrapped in
    an ad-hoc workflow structure for execution.
    """
    try:
        transformation_revision = read_single_transformation_revision(
            exec_by_id_input.id)
        logger.info("found transformation revision with id %s",
                    str(exec_by_id_input.id))
    except DBNotFoundError as e:
        raise TrafoExecutionNotFoundError() from e

    if transformation_revision.type == Type.COMPONENT:
        tr_workflow = transformation_revision.wrap_component_in_tr_workflow()
        assert isinstance(tr_workflow.content,
                          WorkflowContent)  # hint for mypy
        nested_transformations = {
            tr_workflow.content.operators[0].id: transformation_revision
        }
    else:
        tr_workflow = transformation_revision
        nested_transformations = get_all_nested_transformation_revisions(
            tr_workflow)

    nested_components = {
        tr.id: tr
        for tr in nested_transformations.values() if tr.type == Type.COMPONENT
    }
    workflow_node = tr_workflow.to_workflow_node(
        uuid4(), nested_nodes(tr_workflow, nested_transformations))

    execution_input = WorkflowExecutionInput(
        code_modules=[
            tr_component.to_code_module()
            for tr_component in nested_components.values()
        ],
        components=[
            component.to_component_revision()
            for component in nested_components.values()
        ],
        workflow=workflow_node,
        configuration=ConfigurationInput(
            name=str(tr_workflow.id),
            run_pure_plot_operators=exec_by_id_input.run_pure_plot_operators,
        ),
        workflow_wiring=exec_by_id_input.wiring,
        job_id=exec_by_id_input.job_id,
    )
    return execution_input
Exemple #10
0
async def test_execute_for_full_workflow_dto(async_test_client,
                                             clean_test_db_engine):
    patched_session = sessionmaker(clean_test_db_engine)
    with mock.patch(
            "hetdesrun.persistence.dbservice.nesting.Session",
            patched_session,
    ):
        with mock.patch(
                "hetdesrun.persistence.dbservice.revision.Session",
                patched_session,
        ):
            async with async_test_client as ac:

                json_files = [
                    "./transformations/components/connectors/pass-through-integer_100_57eea09f-d28e-89af-4e81-2027697a3f0f.json",
                    "./transformations/components/connectors/pass-through-series_100_bfa27afc-dea8-b8aa-4b15-94402f0739b6.json",
                    "./transformations/components/connectors/pass-through-string_100_2b1b474f-ddf5-1f4d-fec4-17ef9122112b.json",
                    "./transformations/components/remaining-useful-life/univariate-linear-rul-regression_100_8d61a267-3a71-51cd-2817-48c320469d6b.json",
                    "./transformations/components/visualization/univariate-linear-rul-regression-result-plot_100_9c3f88ce-1311-241e-18b7-acf7d3f5a051.json",
                    "./transformations/components/arithmetic/consecutive-differences_100_ce801dcb-8ce1-14ad-029d-a14796dcac92.json",
                    "./transformations/components/basic/filter_100_18260aab-bdd6-af5c-cac1-7bafde85188f.json",
                    "./transformations/components/basic/greater-or-equal_100_f759e4c0-1468-0f2e-9740-41302b860193.json",
                    "./transformations/components/basic/last-datetime-index_100_c8e3bc64-b214-6486-31db-92a8888d8991.json",
                    "./transformations/components/basic/restrict-to-time-interval_100_bf469c0a-d17c-ca6f-59ac-9838b2ff67ac.json",
                    "./transformations/components/connectors/pass-through-float_100_2f511674-f766-748d-2de3-ad5e62e10a1a.json",
                    "./transformations/components/visualization/single-timeseries-plot_100_8fba9b51-a0f1-6c6c-a6d4-e224103b819c.json",
                    "./transformations/workflows/examples/data-from-last-positive-step_100_2cbb87e7-ea99-4404-abe1-be550f22763f.json",
                    "./transformations/workflows/examples/univariate-linear-rul-regression-example_100_806df1b9-2fc8-4463-943f-3d258c569663.json",
                    "./transformations/workflows/examples/linear-rul-from-last-positive-step_100_3d504361-e351-4d52-8734-391aa47e8f24.json",
                ]

                for file in json_files:
                    tr_json = load_json(file)

                    response = await ac.put(
                        posix_urljoin(
                            get_config().hd_backend_api_url,
                            "transformations",
                            tr_json["id"],
                        ) + "?allow_overwrite_released=True",
                        json=tr_json,
                    )

                workflow_id = UUID("3d504361-e351-4d52-8734-391aa47e8f24")
                tr_workflow = read_single_transformation_revision(workflow_id)
                wiring_dto = WiringFrontendDto.from_wiring(
                    tr_workflow.test_wiring, workflow_id)

                response = await ac.post(
                    "/api/workflows/" + str(workflow_id) + "/execute",
                    json=json.loads(wiring_dto.json(by_alias=True)),
                )

                assert response.status_code == 200
                assert "output_types_by_output_name" in response.json()
async def bind_wiring_to_component_revision(
    # pylint: disable=redefined-builtin
    id: UUID,
    wiring_dto: WiringFrontendDto,
) -> ComponentRevisionFrontendDto:
    """Store or update the test wiring of a transformation revision of type component.

    This endpoint is deprecated and will be removed soon,
    use PUT /api/transformations/{id} instead.
    """

    logger.info("bind wiring to component %s", id)

    try:
        transformation_revision = read_single_transformation_revision(id)
        logger.info("found component with id %s", id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    if transformation_revision.type != Type.COMPONENT:
        msg = f"DB entry for id {id} does not have type {Type.COMPONENT}"
        logger.error(msg)
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=msg)

    wiring = wiring_dto.to_wiring()
    transformation_revision.test_wiring = wiring

    try:
        persisted_transformation_revision = (
            update_or_create_single_transformation_revision(
                transformation_revision))
        logger.info("bound wiring to component %s", id)
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    persisted_component_dto = ComponentRevisionFrontendDto.from_transformation_revision(
        persisted_transformation_revision)
    logger.debug(persisted_component_dto.json())

    return persisted_component_dto
Exemple #12
0
async def test_update_transformation_revision_with_non_existing_workflow(
        async_test_client, clean_test_db_engine):
    with mock.patch(
            "hetdesrun.persistence.dbservice.revision.Session",
            sessionmaker(clean_test_db_engine),
    ):
        async with async_test_client as ac:
            response = await ac.put(
                posix_urljoin("/api/transformations/",
                              str(get_uuid_from_seed("workflow 2"))),
                json=tr_json_workflow_2_update,
            )

        workflow_tr_in_db = read_single_transformation_revision(
            get_uuid_from_seed("workflow 2"))

        assert response.status_code == 201
        assert response.json()["name"] == "new name"
        assert len(workflow_tr_in_db.content.links) == 2
async def get_transformation_revision_by_id(
    # pylint: disable=redefined-builtin
    id: UUID = Path(
        ...,
        example=UUID("123e4567-e89b-12d3-a456-426614174000"),
    ),
) -> TransformationRevision:

    logger.info("get transformation revision %s", id)

    try:
        transformation_revision = read_single_transformation_revision(id)
        logger.info("found transformation revision with id %s", id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    logger.debug(transformation_revision.json())

    return transformation_revision
async def create_component_revision(
    component_dto: ComponentRevisionFrontendDto,
) -> ComponentRevisionFrontendDto:
    """Store a transformation revision of type component in the data base.

    This endpoint is deprecated and will be removed soon,
    use POST /api/transformations/ instead.
    """

    logger.info("create new component")

    try:
        transformation_revision = component_dto.to_transformation_revision(
            documentation=("# New Component/Workflow\n"
                           "## Description\n"
                           "## Inputs\n"
                           "## Outputs\n"
                           "## Details\n"
                           "## Examples\n"))
    except ValidationError as e:
        raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY,
                            detail=str(e)) from e

    logger.debug("generate code")
    transformation_revision.content = update_code(transformation_revision)
    logger.debug("generated code:\n%s", component_dto.code)

    try:
        store_single_transformation_revision(transformation_revision)
        logger.info("created new component")
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e

    persisted_transformation_revision = read_single_transformation_revision(
        transformation_revision.id)

    persisted_component_dto = ComponentRevisionFrontendDto.from_transformation_revision(
        persisted_transformation_revision)
    logger.debug(persisted_component_dto.json())

    return persisted_component_dto
Exemple #15
0
async def update_documentation(
    # pylint: disable=redefined-builtin
    id: UUID,
    documentation_dto: DocumentationFrontendDto,
) -> DocumentationFrontendDto:
    """Update or store the documentation of a transformation revision in the data base.

    This endpoint is deprecated and will be removed soon,
    use PUT /api/transformations/{id} instead
    """

    logger.info("update documentation %s", id)

    if id != documentation_dto.id:
        msg = (f"The id {id} does not match "
               f"the id of the documentation DTO {documentation_dto.id}")
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    try:
        transformation_revision = read_single_transformation_revision(id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    transformation_revision.documentation = documentation_dto.document

    try:
        persisted_transformation_revision = (
            update_or_create_single_transformation_revision(
                transformation_revision))
        logger.info("updated documentation {id}")
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    persisted_documentation_dto = DocumentationFrontendDto.from_transformation_revision(
        persisted_transformation_revision)
    logger.debug(persisted_documentation_dto.json())

    return persisted_documentation_dto
Exemple #16
0
async def create_workflow_revision(
    workflow_dto: WorkflowRevisionFrontendDto,
) -> WorkflowRevisionFrontendDto:
    """Store a transformation revision of type workflow in the data base.

    This endpoint is deprecated and will be removed soon,
    use POST /api/transformations/ instead.
    """

    logger.info("create a new workflow")

    try:
        transformation_revision = workflow_dto.to_transformation_revision(
            documentation=("# New Component/Workflow\n"
                           "## Description\n"
                           "## Inputs\n"
                           "## Outputs\n"
                           "## Details\n"
                           "## Examples\n"))
    except ValidationError as e:
        raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY,
                            detail=str(e)) from e

    try:
        store_single_transformation_revision(transformation_revision)
        logger.info("created new workflow")
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e

    try:
        persisted_transformation_revision = read_single_transformation_revision(
            transformation_revision.id)
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    persisted_workflow_dto = WorkflowRevisionFrontendDto.from_transformation_revision(
        persisted_transformation_revision)
    logger.debug(persisted_workflow_dto.json())

    return persisted_workflow_dto
def contains_deprecated(transformation_id: UUID) -> bool:
    logger.info(
        "check if transformation revision %s contains deprecated operators",
        str(transformation_id),
    )
    transformation_revision = read_single_transformation_revision(transformation_id)

    if transformation_revision.type is not Type.WORKFLOW:
        msg = f"transformation revision {id} is not a workflow!"
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    assert isinstance(transformation_revision.content, WorkflowContent)  # hint for mypy

    is_disabled = []
    for operator in transformation_revision.content.operators:
        logger.info(
            "operator with transformation id %s has status %s",
            str(operator.transformation_id),
            operator.state,
        )
        is_disabled.append(operator.state == State.DISABLED)

    return any(is_disabled)
async def update_transformation_revision(
    # pylint: disable=redefined-builtin
    id: UUID,
    updated_transformation_revision: TransformationRevision,
    allow_overwrite_released: bool = Query(
        False, description="Only set to True for deployment"
    ),
    update_component_code: bool = Query(
        True, description="Only set to False for deployment"
    ),
) -> TransformationRevision:
    """Update or store a transformation revision in the data base.

    If no DB entry with the provided id is found, it will be created.

    Updating a transformation revision is only possible if it is in state DRAFT
    or to change the state from RELEASED to DISABLED.

    Unset attributes of the json sent in the request body will be set to default values,
    possibly changing the attribute saved in the DB.
    """

    logger.info("update transformation revision %s", id)

    if id != updated_transformation_revision.id:
        msg = (
            f"The id {id} does not match the id of the provided "
            f"transformation revision DTO {updated_transformation_revision.id}"
        )
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    existing_transformation_revision: Optional[TransformationRevision] = None

    try:
        existing_transformation_revision = read_single_transformation_revision(
            id, log_error=False
        )
        logger.info("found transformation revision %s", id)
    except DBNotFoundError:
        # base/example workflow deployment needs to be able to put
        # with an id and either create or update the transformation revision
        pass

    modifiable, msg = is_modifiable(
        existing_transformation_revision,
        updated_transformation_revision,
        allow_overwrite_released,
    )
    if not modifiable:
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    updated_transformation_revision = if_applicable_release_or_deprecate(
        existing_transformation_revision, updated_transformation_revision
    )

    if updated_transformation_revision.type == Type.WORKFLOW or update_component_code:
        updated_transformation_revision = update_content(
            existing_transformation_revision, updated_transformation_revision
        )

    try:
        persisted_transformation_revision = (
            update_or_create_single_transformation_revision(
                updated_transformation_revision
            )
        )
        logger.info("updated transformation revision %s", id)
    except DBIntegrityError as e:
        logger.error(
            "integrity error in DB when trying to access entry for id %s\n%s", id, e
        )
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    logger.debug(persisted_transformation_revision.json())

    return persisted_transformation_revision
async def update_component_revision(
    # pylint: disable=redefined-builtin
    id: UUID,
    updated_component_dto: ComponentRevisionFrontendDto,
) -> ComponentRevisionFrontendDto:
    """Update or store a transformation revision of type component in the data base.

    If no DB entry with the provided id is found, it will be created.

    Updating a transformation revision is only possible if it is in state DRAFT
    or to change the state from RELEASED to DISABLED.

    This endpoint is deprecated and will be removed soon,
    use PUT /api/transformations/{id} instead.
    """

    logger.info("update component %s", id)

    if id != updated_component_dto.id:
        msg = (
            f"The id {id} does not match "
            f"the id of the component revision DTO {updated_component_dto.id}")
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    try:
        updated_transformation_revision = (
            updated_component_dto.to_transformation_revision())
    except ValidationError as e:
        logger.error("The following validation error occured:\n%s", str(e))
        raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY,
                            detail=str(e)) from e

    existing_transformation_revision: Optional[TransformationRevision] = None

    try:
        existing_transformation_revision = read_single_transformation_revision(
            id, log_error=False)
        logger.info("found transformation revision %s", id)
    except DBNotFoundError:
        # base/example workflow deployment needs to be able to put
        # with an id and either create or update the component revision
        pass

    modifiable, msg = is_modifiable(
        existing_transformation_revision,
        updated_transformation_revision,
    )
    if not modifiable:
        logger.error(msg)
        raise HTTPException(status.HTTP_403_FORBIDDEN, detail=msg)

    if existing_transformation_revision is not None:
        updated_transformation_revision.documentation = (
            existing_transformation_revision.documentation)
        updated_transformation_revision.test_wiring = (
            existing_transformation_revision.test_wiring)
        updated_transformation_revision.released_timestamp = (
            existing_transformation_revision.released_timestamp)

    updated_transformation_revision = if_applicable_release_or_deprecate(
        existing_transformation_revision, updated_transformation_revision)

    updated_transformation_revision = update_content(
        existing_transformation_revision, updated_transformation_revision)

    try:
        persisted_transformation_revision = (
            update_or_create_single_transformation_revision(
                updated_transformation_revision))
        logger.info("updated component %s", id)
    except DBIntegrityError as e:
        raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR,
                            detail=str(e)) from e
    except DBNotFoundError as e:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) from e

    persisted_component_dto = ComponentRevisionFrontendDto.from_transformation_revision(
        persisted_transformation_revision)
    logger.debug(persisted_component_dto.json())

    return persisted_component_dto