Skip to content

Commit 9e9e234

Browse files
authored
✨ Adds custom project's metadata in the wbserver API (#4421)
1 parent afc1eb0 commit 9e9e234

File tree

29 files changed

+996
-65
lines changed

29 files changed

+996
-65
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
paths:
2+
/projects/{project_id}/metadata:
3+
get:
4+
tags:
5+
- project
6+
summary: Get Project Metadata
7+
operationId: get_project_metadata
8+
parameters:
9+
- required: true
10+
schema:
11+
type: string
12+
format: uuid
13+
title: Project Id
14+
name: project_id
15+
in: path
16+
responses:
17+
'200':
18+
description: Successful Response
19+
content:
20+
application/json:
21+
schema:
22+
$ref: '#/components/schemas/Envelope_ProjectMetadataGet_'
23+
patch:
24+
tags:
25+
- project
26+
summary: Update Project Metadata
27+
operationId: update_project_metadata
28+
parameters:
29+
- required: true
30+
schema:
31+
type: string
32+
format: uuid
33+
title: Project Id
34+
name: project_id
35+
in: path
36+
requestBody:
37+
content:
38+
application/json:
39+
schema:
40+
$ref: '#/components/schemas/ProjectMetadataUpdate'
41+
required: true
42+
responses:
43+
'200':
44+
description: Successful Response
45+
content:
46+
application/json:
47+
schema:
48+
$ref: '#/components/schemas/Envelope_ProjectMetadataGet_'
49+
components:
50+
schemas:
51+
Envelope_ProjectMetadataGet_:
52+
properties:
53+
data:
54+
$ref: '#/components/schemas/ProjectMetadataGet'
55+
error:
56+
title: Error
57+
type: object
58+
title: Envelope[ProjectMetadataGet]
59+
ProjectMetadataGet:
60+
properties:
61+
projectUuid:
62+
type: string
63+
format: uuid
64+
title: Projectuuid
65+
custom:
66+
additionalProperties:
67+
anyOf:
68+
- type: boolean
69+
- type: integer
70+
- type: number
71+
- type: string
72+
type: object
73+
title: Custom
74+
description: Custom key-value map
75+
type: object
76+
required:
77+
- projectUuid
78+
title: ProjectMetadataGet
79+
ProjectMetadataUpdate:
80+
properties:
81+
custom:
82+
additionalProperties:
83+
anyOf:
84+
- type: boolean
85+
- type: integer
86+
- type: number
87+
- type: string
88+
type: object
89+
title: Custom
90+
type: object
91+
required:
92+
- custom
93+
title: ProjectMetadataUpdate

api/specs/webserver/openapi.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.0
22
info:
33
title: "osparc-simcore web API"
4-
version: 0.23.0
4+
version: 0.24.0
55
description: "API designed for the front-end app"
66
contact:
77
name: IT'IS Foundation
@@ -256,6 +256,9 @@ paths:
256256
/projects/{project_id}/outputs:
257257
$ref: "./openapi-projects-ports.yaml#/paths/~1projects~1{project_id}~1outputs"
258258

259+
/projects/{project_id}/metadata:
260+
$ref: "./openapi-projects-metadata.yaml#/paths/~1projects~1{project_id}~1metadata"
261+
259262
/projects/{project_id}/metadata/ports:
260263
$ref: "./openapi-projects-ports.yaml#/paths/~1projects~1{project_id}~1metadata~1ports"
261264

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
""" Helper script to automatically generate OAS
2+
3+
This OAS are the source of truth
4+
"""
5+
6+
# pylint: disable=redefined-outer-name
7+
# pylint: disable=unused-argument
8+
# pylint: disable=unused-variable
9+
# pylint: disable=too-many-arguments
10+
11+
12+
from enum import Enum
13+
from typing import Annotated
14+
15+
from _common import CURRENT_DIR, create_openapi_specs
16+
from fastapi import Depends, FastAPI, status
17+
from models_library.api_schemas_webserver.projects_metadata import (
18+
ProjectMetadataGet,
19+
ProjectMetadataUpdate,
20+
)
21+
from models_library.generics import Envelope
22+
from simcore_service_webserver.projects._metadata_handlers import (
23+
ProjectMetadataGet,
24+
ProjectPathParams,
25+
)
26+
27+
app = FastAPI(redoc_url=None)
28+
29+
TAGS: list[str | Enum] = ["project"]
30+
31+
32+
#
33+
# API entrypoints
34+
#
35+
36+
37+
@app.get(
38+
"/projects/{project_id}/metadata",
39+
response_model=Envelope[ProjectMetadataGet],
40+
tags=TAGS,
41+
operation_id="get_project_metadata",
42+
status_code=status.HTTP_200_OK,
43+
)
44+
async def get_project_metadata(_params: Annotated[ProjectPathParams, Depends()]):
45+
...
46+
47+
48+
@app.patch(
49+
"/projects/{project_id}/metadata",
50+
response_model=Envelope[ProjectMetadataGet],
51+
tags=TAGS,
52+
operation_id="update_project_metadata",
53+
status_code=status.HTTP_200_OK,
54+
)
55+
async def update_project_metadata(
56+
_params: Annotated[ProjectPathParams, Depends()], _body: ProjectMetadataUpdate
57+
):
58+
...
59+
60+
61+
if __name__ == "__main__":
62+
63+
create_openapi_specs(app, CURRENT_DIR.parent / "openapi-projects-metadata.yaml")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import TypeAlias
2+
3+
from pydantic import Field, StrictBool, StrictFloat, StrictInt
4+
5+
from ..projects import ProjectID
6+
from ._base import InputSchema, OutputSchema
7+
8+
# Limits metadata values
9+
MetaValueType: TypeAlias = StrictBool | StrictInt | StrictFloat | str
10+
MetadataDict: TypeAlias = dict[str, MetaValueType]
11+
12+
13+
class ProjectMetadataGet(OutputSchema):
14+
project_uuid: ProjectID
15+
custom: MetadataDict = Field(
16+
default_factory=dict, description="Custom key-value map"
17+
)
18+
19+
20+
class ProjectMetadataUpdate(InputSchema):
21+
custom: MetadataDict
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""new projects_metadata table
2+
3+
Revision ID: f3285aff5e84
4+
Revises: 58b24613c3f7
5+
Create Date: 2023-07-05 15:06:56.003418+00:00
6+
7+
"""
8+
from typing import Final
9+
10+
import sqlalchemy as sa
11+
from alembic import op
12+
from sqlalchemy.dialects import postgresql
13+
14+
# revision identifiers, used by Alembic.
15+
revision = "f3285aff5e84"
16+
down_revision = "58b24613c3f7"
17+
branch_labels = None
18+
depends_on = None
19+
20+
21+
# auto-update modified
22+
# TRIGGERS ------------------------
23+
_TABLE_NAME: Final[str] = "projects_metadata"
24+
_TRIGGER_NAME: Final[str] = "trigger_auto_update" # NOTE: scoped on table
25+
_PROCEDURE_NAME: Final[
26+
str
27+
] = f"{_TABLE_NAME}_auto_update_modified()" # NOTE: scoped on database
28+
modified_timestamp_trigger = sa.DDL(
29+
f"""
30+
DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};
31+
CREATE TRIGGER {_TRIGGER_NAME}
32+
BEFORE INSERT OR UPDATE ON {_TABLE_NAME}
33+
FOR EACH ROW EXECUTE PROCEDURE {_PROCEDURE_NAME};
34+
"""
35+
)
36+
37+
# PROCEDURES ------------------------
38+
update_modified_timestamp_procedure = sa.DDL(
39+
f"""
40+
CREATE OR REPLACE FUNCTION {_PROCEDURE_NAME}
41+
RETURNS TRIGGER AS $$
42+
BEGIN
43+
NEW.modified := current_timestamp;
44+
RETURN NEW;
45+
END;
46+
$$ LANGUAGE plpgsql;
47+
"""
48+
)
49+
50+
51+
def upgrade():
52+
# ### commands auto generated by Alembic - please adjust! ###
53+
op.create_table(
54+
"projects_metadata",
55+
sa.Column("project_uuid", sa.String(), nullable=False),
56+
sa.Column(
57+
"custom",
58+
postgresql.JSONB(astext_type=sa.Text()),
59+
server_default=sa.text("'{}'::jsonb"),
60+
nullable=False,
61+
),
62+
sa.Column(
63+
"created",
64+
sa.DateTime(timezone=True),
65+
server_default=sa.text("now()"),
66+
nullable=False,
67+
),
68+
sa.Column(
69+
"modified",
70+
sa.DateTime(timezone=True),
71+
server_default=sa.text("now()"),
72+
nullable=False,
73+
),
74+
sa.ForeignKeyConstraint(
75+
["project_uuid"],
76+
["projects.uuid"],
77+
name="fk_projects_metadata_project_uuid",
78+
onupdate="CASCADE",
79+
ondelete="CASCADE",
80+
),
81+
sa.PrimaryKeyConstraint("project_uuid"),
82+
)
83+
# ### end Alembic commands ###
84+
85+
# custom
86+
op.execute(update_modified_timestamp_procedure)
87+
op.execute(modified_timestamp_trigger)
88+
89+
90+
def downgrade():
91+
# custom
92+
op.execute(f"DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};")
93+
op.execute(f"DROP FUNCTION {_PROCEDURE_NAME};")
94+
95+
# ### commands auto generated by Alembic - please adjust! ###
96+
op.drop_table("projects_metadata")
97+
# ### end Alembic commands ###
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
These tables were designed to be controled by projects-plugin in
3+
the webserver's service
4+
"""
5+
6+
import sqlalchemy as sa
7+
from sqlalchemy.dialects.postgresql import JSONB
8+
9+
from ._common import (
10+
column_created_datetime,
11+
column_modified_datetime,
12+
register_modified_datetime_auto_update_trigger,
13+
)
14+
from .base import metadata
15+
from .projects import projects
16+
17+
projects_metadata = sa.Table(
18+
"projects_metadata",
19+
#
20+
# Keeps "third-party" metadata attached to a project
21+
#
22+
# These SHOULD NOT be actual properties of the project (e.g. uuid, name etc)
23+
# but rather information attached by third-parties that "decorate" or qualify
24+
# a project resource
25+
#
26+
# Things like 'stars', 'quality', 'classifiers', 'dev', etc (or any kind of stats)
27+
# should be moved here
28+
#
29+
metadata,
30+
sa.Column(
31+
"project_uuid",
32+
sa.String,
33+
sa.ForeignKey(
34+
projects.c.uuid,
35+
onupdate="CASCADE",
36+
ondelete="CASCADE",
37+
name="fk_projects_metadata_project_uuid",
38+
),
39+
nullable=False,
40+
primary_key=True,
41+
doc="The project unique identifier is also used to identify the associated job",
42+
),
43+
sa.Column(
44+
"custom",
45+
JSONB,
46+
nullable=False,
47+
server_default=sa.text("'{}'::jsonb"),
48+
doc="Reserved for the user to store custom metadata",
49+
),
50+
# TIME STAMPS ----ß
51+
column_created_datetime(timezone=True),
52+
column_modified_datetime(timezone=True),
53+
sa.PrimaryKeyConstraint("project_uuid"),
54+
)
55+
56+
57+
register_modified_datetime_auto_update_trigger(projects_metadata)

0 commit comments

Comments
 (0)