Skip to content

Commit 1295cff

Browse files
committed
✨ [Models] Update project and node models with improved field annotations and examples
1 parent 2d68a47 commit 1295cff

File tree

4 files changed

+184
-49
lines changed

4 files changed

+184
-49
lines changed

packages/models-library/src/models_library/projects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ class ProjectAtDB(BaseProjectModel):
113113

114114
published: Annotated[
115115
bool | None,
116-
Field(default=False, description="Defines if a study is available publicly"),
117-
]
116+
Field(description="Defines if a study is available publicly"),
117+
] = False
118118

119119
@field_validator("project_type", mode="before")
120120
@classmethod

packages/models-library/src/models_library/projects_nodes.py

Lines changed: 146 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Models Node as a central element in a project's pipeline
2+
Models Node as a central element in a project's pipeline
33
"""
44

55
from typing import Annotated, Any, TypeAlias, Union
@@ -17,6 +17,7 @@
1717
StringConstraints,
1818
field_validator,
1919
)
20+
from pydantic.config import JsonDict
2021

2122
from .basic_types import EnvVarKey, KeyIDStr
2223
from .projects_access import AccessEnum
@@ -71,24 +72,35 @@
7172

7273

7374
class NodeState(BaseModel):
74-
modified: bool = Field(
75-
default=True, description="true if the node's outputs need to be re-computed"
76-
)
77-
dependencies: set[NodeID] = Field(
78-
default_factory=set,
79-
description="contains the node inputs dependencies if they need to be computed first",
80-
)
81-
current_status: RunningState = Field(
82-
default=RunningState.NOT_STARTED,
83-
description="the node's current state",
84-
alias="currentStatus",
85-
)
86-
progress: float | None = Field(
87-
default=0,
88-
ge=0.0,
89-
le=1.0,
90-
description="current progress of the task if available (None if not started or not a computational task)",
91-
)
75+
modified: Annotated[
76+
bool,
77+
Field(
78+
description="true if the node's outputs need to be re-computed",
79+
),
80+
] = True
81+
dependencies: Annotated[
82+
set[NodeID],
83+
Field(
84+
default_factory=set,
85+
description="contains the node inputs dependencies if they need to be computed first",
86+
),
87+
] = DEFAULT_FACTORY
88+
current_status: Annotated[
89+
RunningState,
90+
Field(
91+
description="the node's current state",
92+
alias="currentStatus",
93+
),
94+
] = RunningState.NOT_STARTED
95+
progress: Annotated[
96+
float | None,
97+
Field(
98+
ge=0.0,
99+
le=1.0,
100+
description="current progress of the task if available (None if not started or not a computational task)",
101+
),
102+
] = 0
103+
92104
model_config = ConfigDict(
93105
extra="forbid",
94106
json_schema_extra={
@@ -114,23 +126,28 @@ class NodeState(BaseModel):
114126

115127

116128
class Node(BaseModel):
117-
key: ServiceKey = Field(
118-
...,
119-
description="distinctive name for the node based on the docker registry path",
120-
examples=[
121-
"simcore/services/comp/itis/sleeper",
122-
"simcore/services/dynamic/3dviewer",
123-
"simcore/services/frontend/file-picker",
124-
],
125-
)
126-
version: ServiceVersion = Field(
127-
...,
128-
description="semantic version number of the node",
129-
examples=["1.0.0", "0.0.1"],
130-
)
131-
label: str = Field(
132-
..., description="The short name of the node", examples=["JupyterLab"]
133-
)
129+
key: Annotated[
130+
ServiceKey,
131+
Field(
132+
description="distinctive name for the node based on the docker registry path",
133+
examples=[
134+
"simcore/services/comp/itis/sleeper",
135+
"simcore/services/dynamic/3dviewer",
136+
"simcore/services/frontend/file-picker",
137+
],
138+
),
139+
]
140+
version: Annotated[
141+
ServiceVersion,
142+
Field(
143+
description="semantic version number of the node",
144+
examples=["1.0.0", "0.0.1"],
145+
),
146+
]
147+
label: Annotated[
148+
str,
149+
Field(description="The short name of the node", examples=["JupyterLab"]),
150+
]
134151
progress: Annotated[
135152
float | None,
136153
Field(
@@ -204,9 +221,9 @@ class Node(BaseModel):
204221
Field(default_factory=dict, description="values of output properties"),
205222
] = DEFAULT_FACTORY
206223

207-
output_node: Annotated[
208-
bool | None, Field(deprecated=True, alias="outputNode")
209-
] = None
224+
output_node: Annotated[bool | None, Field(deprecated=True, alias="outputNode")] = (
225+
None
226+
)
210227

211228
output_nodes: Annotated[
212229
list[NodeID] | None,
@@ -270,9 +287,99 @@ def _convert_from_enum(cls, v):
270287
return NodeState(currentStatus=running_state_value)
271288
return v
272289

290+
@staticmethod
291+
def _update_json_schema_extra(schema: JsonDict) -> None:
292+
schema.update(
293+
{
294+
"examples": [
295+
# Minimal example with only required fields
296+
{
297+
"key": "simcore/services/comp/no_ports",
298+
"version": "1.0.0",
299+
"label": "Sleep",
300+
},
301+
# Complete example with optional fields
302+
{
303+
"key": "simcore/services/comp/only_inputs",
304+
"version": "1.0.0",
305+
"label": "Only INputs",
306+
"inputs": {
307+
"input_1": 1,
308+
"input_2": 2,
309+
"input_3": 3,
310+
},
311+
},
312+
# Complete example with optional fields
313+
{
314+
"key": "simcore/services/comp/only_outputs",
315+
"version": "1.0.0",
316+
"label": "Only Outputs",
317+
"outputs": {
318+
"output_1": 1,
319+
"output_2": 2,
320+
"output_3": 3,
321+
},
322+
},
323+
# Example with all possible input and output types
324+
{
325+
"key": "simcore/services/comp/itis/all-types",
326+
"version": "1.0.0",
327+
"label": "All Types Demo",
328+
"inputs": {
329+
"boolean_input": True,
330+
"integer_input": 42,
331+
"float_input": 3.14159,
332+
"string_input": "text value",
333+
"json_input": {"key": "value", "nested": {"data": 123}},
334+
"port_link_input": {
335+
"nodeUuid": "f2700a54-adcf-45d4-9881-01ec30fd75a2",
336+
"output": "out_1",
337+
},
338+
"simcore_file_link": {
339+
"store": "simcore.s3",
340+
"path": "123e4567-e89b-12d3-a456-426614174000/test.csv",
341+
},
342+
"datcore_file_link": {
343+
"store": "datcore",
344+
"dataset": "N:dataset:123",
345+
"path": "path/to/file.txt",
346+
},
347+
"download_link": {
348+
"downloadLink": "https://example.com/downloadable/file.txt"
349+
},
350+
"array_input": [1, 2, 3, 4, 5],
351+
"object_input": {"name": "test", "value": 42},
352+
},
353+
"outputs": {
354+
"boolean_output": False,
355+
"integer_output": 100,
356+
"float_output": 2.71828,
357+
"string_output": "result text",
358+
"json_output": {"status": "success", "data": [1, 2, 3]},
359+
"simcore_file_output": {
360+
"store": "simcore.s3",
361+
"path": "987e6543-e21b-12d3-a456-426614174000/result.csv",
362+
},
363+
"datcore_file_output": {
364+
"store": "datcore",
365+
"dataset": "N:dataset:456",
366+
"path": "results/output.txt",
367+
},
368+
"download_link_output": {
369+
"downloadLink": "https://example.com/results/download.txt"
370+
},
371+
"array_output": ["a", "b", "c", "d"],
372+
"object_output": {"status": "complete", "count": 42},
373+
},
374+
},
375+
],
376+
}
377+
)
378+
273379
model_config = ConfigDict(
274380
extra="forbid",
275381
populate_by_name=True,
382+
json_schema_extra=_update_json_schema_extra,
276383
)
277384

278385

packages/models-library/src/models_library/rpc/webserver/projects.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from datetime import datetime
22
from typing import Annotated, TypeAlias
3+
from uuid import uuid4
34

4-
from models_library.projects import ProjectID
5+
from models_library.projects import NodesDict, ProjectID
6+
from models_library.projects_nodes import Node
57
from models_library.rpc_pagination import PageRpc
68
from pydantic import BaseModel, ConfigDict, Field
9+
from pydantic.config import JsonDict
710

811

9-
class ProjectRpcGet(BaseModel):
12+
class ProjectJobRpcGet(BaseModel):
1013
"""
1114
Minimal information about a project that (for now) will fullfill
1215
the needs of the api-server. Specifically, the fields needed in
@@ -23,19 +26,44 @@ class ProjectRpcGet(BaseModel):
2326
]
2427
description: str
2528

29+
workbench: NodesDict
30+
2631
# timestamps
2732
creation_date: datetime
2833
last_change_date: datetime
2934

35+
# Specific to jobs
36+
job_parent_resource_name: str
37+
38+
@staticmethod
39+
def _update_json_schema_extra(schema: JsonDict) -> None:
40+
nodes_examples = Node.model_json_schema()["examples"]
41+
schema.update(
42+
{
43+
"examples": [
44+
{
45+
"uuid": "12345678-1234-5678-1234-123456789012",
46+
"name": "My project",
47+
"description": "My project description",
48+
"workbench": {f"{uuid4()}": n for n in nodes_examples[2:3]},
49+
"creation_date": "2023-01-01T00:00:00Z",
50+
"last_change_date": "2023-01-01T00:00:00Z",
51+
"job_parent_resource_name": "solvers/foo/release/1.2.3",
52+
},
53+
]
54+
}
55+
)
56+
3057
model_config = ConfigDict(
3158
extra="forbid",
3259
populate_by_name=True,
60+
json_schema_extra=_update_json_schema_extra,
3361
)
3462

3563

36-
PageRpcProjectRpcGet: TypeAlias = PageRpc[
64+
PageRpcProjectJobRpcGet: TypeAlias = PageRpc[
3765
# WARNING: keep this definition in models_library and not in the RPC interface
3866
# otherwise the metaclass PageRpc[*] will create *different* classes in server/client side
3967
# and will fail to serialize/deserialize these parameters when transmitted/received
40-
ProjectRpcGet
68+
ProjectJobRpcGet
4169
]

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/projects.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from models_library.projects import ProjectID
77
from models_library.rabbitmq_basic_types import RPCMethodName
88
from models_library.rest_pagination import PageOffsetInt
9-
from models_library.rpc.webserver.projects import PageRpcProjectRpcGet
9+
from models_library.rpc.webserver.projects import PageRpcProjectJobRpcGet
1010
from models_library.rpc_pagination import (
1111
DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
1212
PageLimitInt,
@@ -53,7 +53,7 @@ async def list_projects_marked_as_jobs(
5353
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
5454
# filters
5555
job_parent_resource_name_filter: str | None = None,
56-
) -> PageRpcProjectRpcGet:
56+
) -> PageRpcProjectJobRpcGet:
5757
result = await rpc_client.request(
5858
WEBSERVER_RPC_NAMESPACE,
5959
TypeAdapter(RPCMethodName).validate_python("list_projects_marked_as_jobs"),
@@ -63,5 +63,5 @@ async def list_projects_marked_as_jobs(
6363
limit=limit,
6464
job_parent_resource_name_filter=job_parent_resource_name_filter,
6565
)
66-
assert TypeAdapter(PageRpcProjectRpcGet).validate_python(result) # nosec
67-
return cast(PageRpcProjectRpcGet, result)
66+
assert TypeAdapter(PageRpcProjectJobRpcGet).validate_python(result) # nosec
67+
return cast(PageRpcProjectJobRpcGet, result)

0 commit comments

Comments
 (0)