Skip to content

Commit

Permalink
✨ Adds custom project's metadata in the wbserver API (#4421)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Jul 7, 2023
1 parent afc1eb0 commit 9e9e234
Show file tree
Hide file tree
Showing 29 changed files with 996 additions and 65 deletions.
93 changes: 93 additions & 0 deletions api/specs/webserver/openapi-projects-metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
paths:
/projects/{project_id}/metadata:
get:
tags:
- project
summary: Get Project Metadata
operationId: get_project_metadata
parameters:
- required: true
schema:
type: string
format: uuid
title: Project Id
name: project_id
in: path
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope_ProjectMetadataGet_'
patch:
tags:
- project
summary: Update Project Metadata
operationId: update_project_metadata
parameters:
- required: true
schema:
type: string
format: uuid
title: Project Id
name: project_id
in: path
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectMetadataUpdate'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope_ProjectMetadataGet_'
components:
schemas:
Envelope_ProjectMetadataGet_:
properties:
data:
$ref: '#/components/schemas/ProjectMetadataGet'
error:
title: Error
type: object
title: Envelope[ProjectMetadataGet]
ProjectMetadataGet:
properties:
projectUuid:
type: string
format: uuid
title: Projectuuid
custom:
additionalProperties:
anyOf:
- type: boolean
- type: integer
- type: number
- type: string
type: object
title: Custom
description: Custom key-value map
type: object
required:
- projectUuid
title: ProjectMetadataGet
ProjectMetadataUpdate:
properties:
custom:
additionalProperties:
anyOf:
- type: boolean
- type: integer
- type: number
- type: string
type: object
title: Custom
type: object
required:
- custom
title: ProjectMetadataUpdate
5 changes: 4 additions & 1 deletion api/specs/webserver/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: "osparc-simcore web API"
version: 0.23.0
version: 0.24.0
description: "API designed for the front-end app"
contact:
name: IT'IS Foundation
Expand Down Expand Up @@ -256,6 +256,9 @@ paths:
/projects/{project_id}/outputs:
$ref: "./openapi-projects-ports.yaml#/paths/~1projects~1{project_id}~1outputs"

/projects/{project_id}/metadata:
$ref: "./openapi-projects-metadata.yaml#/paths/~1projects~1{project_id}~1metadata"

/projects/{project_id}/metadata/ports:
$ref: "./openapi-projects-ports.yaml#/paths/~1projects~1{project_id}~1metadata~1ports"

Expand Down
63 changes: 63 additions & 0 deletions api/specs/webserver/scripts/openapi_projects_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
""" Helper script to automatically generate OAS
This OAS are the source of truth
"""

# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=unused-variable
# pylint: disable=too-many-arguments


from enum import Enum
from typing import Annotated

from _common import CURRENT_DIR, create_openapi_specs
from fastapi import Depends, FastAPI, status
from models_library.api_schemas_webserver.projects_metadata import (
ProjectMetadataGet,
ProjectMetadataUpdate,
)
from models_library.generics import Envelope
from simcore_service_webserver.projects._metadata_handlers import (
ProjectMetadataGet,
ProjectPathParams,
)

app = FastAPI(redoc_url=None)

TAGS: list[str | Enum] = ["project"]


#
# API entrypoints
#


@app.get(
"/projects/{project_id}/metadata",
response_model=Envelope[ProjectMetadataGet],
tags=TAGS,
operation_id="get_project_metadata",
status_code=status.HTTP_200_OK,
)
async def get_project_metadata(_params: Annotated[ProjectPathParams, Depends()]):
...


@app.patch(
"/projects/{project_id}/metadata",
response_model=Envelope[ProjectMetadataGet],
tags=TAGS,
operation_id="update_project_metadata",
status_code=status.HTTP_200_OK,
)
async def update_project_metadata(
_params: Annotated[ProjectPathParams, Depends()], _body: ProjectMetadataUpdate
):
...


if __name__ == "__main__":

create_openapi_specs(app, CURRENT_DIR.parent / "openapi-projects-metadata.yaml")
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import TypeAlias

from pydantic import Field, StrictBool, StrictFloat, StrictInt

from ..projects import ProjectID
from ._base import InputSchema, OutputSchema

# Limits metadata values
MetaValueType: TypeAlias = StrictBool | StrictInt | StrictFloat | str
MetadataDict: TypeAlias = dict[str, MetaValueType]


class ProjectMetadataGet(OutputSchema):
project_uuid: ProjectID
custom: MetadataDict = Field(
default_factory=dict, description="Custom key-value map"
)


class ProjectMetadataUpdate(InputSchema):
custom: MetadataDict
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""new projects_metadata table
Revision ID: f3285aff5e84
Revises: 58b24613c3f7
Create Date: 2023-07-05 15:06:56.003418+00:00
"""
from typing import Final

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "f3285aff5e84"
down_revision = "58b24613c3f7"
branch_labels = None
depends_on = None


# auto-update modified
# TRIGGERS ------------------------
_TABLE_NAME: Final[str] = "projects_metadata"
_TRIGGER_NAME: Final[str] = "trigger_auto_update" # NOTE: scoped on table
_PROCEDURE_NAME: Final[
str
] = f"{_TABLE_NAME}_auto_update_modified()" # NOTE: scoped on database
modified_timestamp_trigger = sa.DDL(
f"""
DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};
CREATE TRIGGER {_TRIGGER_NAME}
BEFORE INSERT OR UPDATE ON {_TABLE_NAME}
FOR EACH ROW EXECUTE PROCEDURE {_PROCEDURE_NAME};
"""
)

# PROCEDURES ------------------------
update_modified_timestamp_procedure = sa.DDL(
f"""
CREATE OR REPLACE FUNCTION {_PROCEDURE_NAME}
RETURNS TRIGGER AS $$
BEGIN
NEW.modified := current_timestamp;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"projects_metadata",
sa.Column("project_uuid", sa.String(), nullable=False),
sa.Column(
"custom",
postgresql.JSONB(astext_type=sa.Text()),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
),
sa.Column(
"created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"modified",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["project_uuid"],
["projects.uuid"],
name="fk_projects_metadata_project_uuid",
onupdate="CASCADE",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("project_uuid"),
)
# ### end Alembic commands ###

# custom
op.execute(update_modified_timestamp_procedure)
op.execute(modified_timestamp_trigger)


def downgrade():
# custom
op.execute(f"DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};")
op.execute(f"DROP FUNCTION {_PROCEDURE_NAME};")

# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("projects_metadata")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
These tables were designed to be controled by projects-plugin in
the webserver's service
"""

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB

from ._common import (
column_created_datetime,
column_modified_datetime,
register_modified_datetime_auto_update_trigger,
)
from .base import metadata
from .projects import projects

projects_metadata = sa.Table(
"projects_metadata",
#
# Keeps "third-party" metadata attached to a project
#
# These SHOULD NOT be actual properties of the project (e.g. uuid, name etc)
# but rather information attached by third-parties that "decorate" or qualify
# a project resource
#
# Things like 'stars', 'quality', 'classifiers', 'dev', etc (or any kind of stats)
# should be moved here
#
metadata,
sa.Column(
"project_uuid",
sa.String,
sa.ForeignKey(
projects.c.uuid,
onupdate="CASCADE",
ondelete="CASCADE",
name="fk_projects_metadata_project_uuid",
),
nullable=False,
primary_key=True,
doc="The project unique identifier is also used to identify the associated job",
),
sa.Column(
"custom",
JSONB,
nullable=False,
server_default=sa.text("'{}'::jsonb"),
doc="Reserved for the user to store custom metadata",
),
# TIME STAMPS ----ß
column_created_datetime(timezone=True),
column_modified_datetime(timezone=True),
sa.PrimaryKeyConstraint("project_uuid"),
)


register_modified_datetime_auto_update_trigger(projects_metadata)
Loading

0 comments on commit 9e9e234

Please sign in to comment.