Skip to content

Commit 18f20e7

Browse files
Merge branch 'master' into is8110/update-api-key-uniqueness-constraint
2 parents 070c32c + 87820ae commit 18f20e7

File tree

16 files changed

+364
-62
lines changed

16 files changed

+364
-62
lines changed

packages/aws-library/src/aws_library/s3/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
)
2323

2424
__all__: tuple[str, ...] = (
25-
"CopiedBytesTransferredCallback",
26-
"MultiPartUploadLinks",
2725
"PRESIGNED_LINK_MAX_SIZE",
2826
"S3_MAX_FILE_SIZE",
27+
"CopiedBytesTransferredCallback",
28+
"MultiPartUploadLinks",
2929
"S3AccessError",
3030
"S3BucketInvalidError",
3131
"S3DestinationNotEmptyError",
@@ -37,8 +37,8 @@
3737
"S3RuntimeError",
3838
"S3UploadNotFoundError",
3939
"SimcoreS3API",
40-
"UploadedBytesTransferredCallback",
4140
"UploadID",
41+
"UploadedBytesTransferredCallback",
4242
)
4343

4444
# nopycln: file

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class ProjectGet(OutputSchema):
126126

127127
# display
128128
name: str
129-
description: str
129+
description: Annotated[str, BeforeValidator(none_to_empty_str_pre_validator)]
130130
thumbnail: HttpUrl | Literal[""]
131131

132132
type: ProjectType
@@ -144,7 +144,7 @@ class ProjectGet(OutputSchema):
144144
trashed_at: datetime | None
145145
trashed_by: Annotated[
146146
GroupID | None, Field(description="The primary gid of the user who trashed")
147-
]
147+
] = None
148148

149149
# labeling
150150
tags: list[int]
@@ -166,10 +166,6 @@ class ProjectGet(OutputSchema):
166166
workspace_id: WorkspaceID | None
167167
folder_id: FolderID | None
168168

169-
_empty_description = field_validator("description", mode="before")(
170-
none_to_empty_str_pre_validator
171-
)
172-
173169
@staticmethod
174170
def _update_json_schema_extra(schema: JsonDict) -> None:
175171
schema.update(

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from common_library.basic_types import DEFAULT_FACTORY
1111
from pydantic import (
1212
BaseModel,
13+
BeforeValidator,
1314
ConfigDict,
1415
Field,
1516
HttpUrl,
@@ -85,13 +86,17 @@ class BaseProjectModel(BaseModel):
8586
]
8687
description: Annotated[
8788
str,
89+
BeforeValidator(none_to_empty_str_pre_validator),
8890
Field(
8991
description="longer one-line description about the project",
9092
examples=["Dabbling in temporal transitions ..."],
9193
),
9294
]
9395
thumbnail: Annotated[
9496
HttpUrl | None,
97+
BeforeValidator(
98+
empty_str_to_none_pre_validator,
99+
),
95100
Field(
96101
description="url of the project thumbnail",
97102
examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"],
@@ -104,15 +109,6 @@ class BaseProjectModel(BaseModel):
104109
# Pipeline of nodes (SEE projects_nodes.py)
105110
workbench: Annotated[NodesDict, Field(description="Project's pipeline")]
106111

107-
# validators
108-
_empty_thumbnail_is_none = field_validator("thumbnail", mode="before")(
109-
empty_str_to_none_pre_validator
110-
)
111-
112-
_none_description_is_empty = field_validator("description", mode="before")(
113-
none_to_empty_str_pre_validator
114-
)
115-
116112

117113
class ProjectAtDB(BaseProjectModel):
118114
# Model used to READ from database

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,22 @@ class NodeState(BaseModel):
177177

178178
model_config = ConfigDict(
179179
extra="forbid",
180-
populate_by_name=True,
180+
validate_by_alias=True,
181+
validate_by_name=True,
181182
json_schema_extra={
182183
"examples": [
184+
# example with alias name
183185
{
184186
"modified": True,
185187
"dependencies": [],
186188
"currentStatus": "NOT_STARTED",
187189
},
190+
# example with field name
191+
{
192+
"modified": True,
193+
"dependencies": [],
194+
"current_status": "NOT_STARTED",
195+
},
188196
{
189197
"modified": True,
190198
"dependencies": ["42838344-03de-4ce2-8d93-589a5dcdfd05"],
@@ -234,16 +242,19 @@ class Node(BaseModel):
234242
Field(
235243
ge=0,
236244
le=100,
237-
description="the node progress value (deprecated in DB, still used for API only)",
238-
deprecated=True, # <-- Think this is not true, it is still used by the File Picker (frontend nodes)
245+
description="the node progress value",
246+
deprecated=True, # NOTE: still used in the File Picker (frontend nodes) and must be removed first from there before retiring it here
247+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365
239248
),
240249
] = None
241250

242-
thumbnail: Annotated[ # <-- (DEPRECATED) Can be removed
251+
thumbnail: Annotated[
243252
str | HttpUrl | None,
244253
Field(
245254
description="url of the latest screenshot of the node",
246255
examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"],
256+
deprecated=True,
257+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365
247258
),
248259
] = None
249260

@@ -302,22 +313,31 @@ class Node(BaseModel):
302313
Field(default_factory=dict, description="values of output properties"),
303314
] = DEFAULT_FACTORY
304315

305-
output_node: Annotated[bool | None, Field(deprecated=True, alias="outputNode")] = (
306-
None # <-- (DEPRECATED) Can be removed
307-
)
316+
output_node: Annotated[
317+
bool | None,
318+
Field(
319+
deprecated=True,
320+
alias="outputNode",
321+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365
322+
),
323+
] = None
308324

309325
output_nodes: Annotated[ # <-- (DEPRECATED) Can be removed
310326
list[NodeID] | None,
311327
Field(
312328
description="Used in group-nodes. Node IDs of those connected to the output",
313329
alias="outputNodes",
330+
deprecated=True,
331+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365
314332
),
315333
] = None
316334

317-
parent: Annotated[ # <-- (DEPRECATED) Can be removed
335+
parent: Annotated[
318336
NodeID | None,
319337
Field(
320338
description="Parent's (group-nodes') node ID s. Used to group",
339+
deprecated=True,
340+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365
321341
),
322342
] = None
323343

@@ -334,6 +354,9 @@ class Node(BaseModel):
334354
Field(default_factory=NodeState, description="The node's state object"),
335355
] = DEFAULT_FACTORY
336356

357+
# NOTE: requested_resources should be here! WARNING: this model is used both in database and rest api!
358+
# Model for project_nodes table should NOT be Node but a different one !
359+
337360
boot_options: Annotated[
338361
dict[EnvVarKey, str] | None,
339362
Field(
@@ -453,12 +476,14 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
453476

454477
model_config = ConfigDict(
455478
extra="forbid",
456-
populate_by_name=True,
479+
validate_by_name=True,
480+
validate_by_alias=True,
457481
json_schema_extra=_update_json_schema_extra,
458482
)
459483

460484

461485
class PartialNode(Node):
462-
key: Annotated[ServiceKey, Field(default=None)]
463-
version: Annotated[ServiceVersion, Field(default=None)]
464-
label: Annotated[str, Field(default=None)]
486+
# NOTE: `type: ignore[assignment]` is needed because mypy gets confused when overriding the types by adding the Union with None
487+
key: ServiceKey | None = None # type: ignore[assignment]
488+
version: ServiceVersion | None = None # type: ignore[assignment]
489+
label: str | None = None # type: ignore[assignment]

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@
3030
UUID_RE,
3131
)
3232

33-
NodeID = UUID
34-
3533
UUIDStr: TypeAlias = Annotated[str, StringConstraints(pattern=UUID_RE)]
3634

35+
NodeID: TypeAlias = UUID
3736
NodeIDStr: TypeAlias = UUIDStr
3837

3938
LocationID: TypeAlias = int

packages/models-library/tests/test_services_types.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import pytest
22
from models_library.projects import ProjectID
33
from models_library.projects_nodes import NodeID
4-
from models_library.services_types import ServiceRunID
4+
from models_library.services_types import ServiceKey, ServiceRunID, ServiceVersion
55
from models_library.users import UserID
6-
from pydantic import PositiveInt
6+
from pydantic import PositiveInt, TypeAdapter
7+
from pytest_simcore.helpers.faker_factories import (
8+
random_service_key,
9+
random_service_version,
10+
)
711

812

913
@pytest.mark.parametrize(
@@ -38,3 +42,14 @@ def test_get_resource_tracking_run_id_for_dynamic():
3842
assert isinstance(
3943
ServiceRunID.get_resource_tracking_run_id_for_dynamic(), ServiceRunID
4044
)
45+
46+
47+
@pytest.mark.parametrize(
48+
"service_key, service_version",
49+
[(random_service_key(), random_service_version()) for _ in range(10)],
50+
)
51+
def test_faker_factory_service_key_and_version_are_in_sync(
52+
service_key: ServiceKey, service_version: ServiceVersion
53+
):
54+
TypeAdapter(ServiceKey).validate_python(service_key)
55+
TypeAdapter(ServiceVersion).validate_python(service_version)

packages/postgres-database/src/simcore_postgres_database/utils_projects.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sqlalchemy.ext.asyncio import AsyncConnection
88

99
from .models.projects import projects
10-
from .utils_repos import transaction_context
10+
from .utils_repos import pass_or_acquire_connection, transaction_context
1111

1212

1313
class DBBaseProjectError(OsparcErrorMixin, Exception):
@@ -22,6 +22,23 @@ class ProjectsRepo:
2222
def __init__(self, engine):
2323
self.engine = engine
2424

25+
async def exists(
26+
self,
27+
project_uuid: uuid.UUID,
28+
*,
29+
connection: AsyncConnection | None = None,
30+
) -> bool:
31+
async with pass_or_acquire_connection(self.engine, connection) as conn:
32+
return (
33+
await conn.scalar(
34+
sa.select(1)
35+
.select_from(projects)
36+
.where(projects.c.uuid == f"{project_uuid}")
37+
.limit(1)
38+
)
39+
is not None
40+
)
41+
2542
async def get_project_last_change_date(
2643
self,
2744
project_uuid: uuid.UUID,

packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
from typing import Annotated, Any
55

66
import asyncpg.exceptions # type: ignore[import-untyped]
7-
import sqlalchemy
87
import sqlalchemy.exc
98
from common_library.async_tools import maybe_await
109
from common_library.basic_types import DEFAULT_FACTORY
1110
from common_library.errors_classes import OsparcErrorMixin
1211
from pydantic import BaseModel, ConfigDict, Field
13-
from simcore_postgres_database.utils_aiosqlalchemy import map_db_exception
1412
from sqlalchemy.dialects.postgresql import insert as pg_insert
1513

1614
from ._protocols import DBConnection
1715
from .aiopg_errors import ForeignKeyViolation, UniqueViolation
1816
from .models.projects_node_to_pricing_unit import projects_node_to_pricing_unit
1917
from .models.projects_nodes import projects_nodes
18+
from .utils_aiosqlalchemy import map_db_exception
2019

2120

2221
#
@@ -59,6 +58,7 @@ class ProjectNodeCreate(BaseModel):
5958
input_access: dict[str, Any] | None = None
6059
input_nodes: list[str] | None = None
6160
inputs: dict[str, Any] | None = None
61+
inputs_required: list[str] | None = None
6262
inputs_units: dict[str, Any] | None = None
6363
output_nodes: list[str] | None = None
6464
outputs: dict[str, Any] | None = None
@@ -103,17 +103,18 @@ async def add(
103103
"""
104104
if not nodes:
105105
return []
106+
107+
values = [
108+
{
109+
"project_uuid": f"{self.project_uuid}",
110+
**node.model_dump(mode="json"),
111+
}
112+
for node in nodes
113+
]
114+
106115
insert_stmt = (
107116
projects_nodes.insert()
108-
.values(
109-
[
110-
{
111-
"project_uuid": f"{self.project_uuid}",
112-
**node.model_dump(exclude_unset=True, mode="json"),
113-
}
114-
for node in nodes
115-
]
116-
)
117+
.values(values)
117118
.returning(
118119
*[
119120
c
@@ -129,14 +130,17 @@ async def add(
129130
rows = await maybe_await(result.fetchall())
130131
assert isinstance(rows, list) # nosec
131132
return [ProjectNode.model_validate(r) for r in rows]
133+
132134
except ForeignKeyViolation as exc:
133135
# this happens when the project does not exist, as we first check the node exists
134136
raise ProjectNodesProjectNotFoundError(
135137
project_uuid=self.project_uuid
136138
) from exc
139+
137140
except UniqueViolation as exc:
138141
# this happens if the node already exists on creation
139142
raise ProjectNodesDuplicateNodeError from exc
143+
140144
except sqlalchemy.exc.IntegrityError as exc:
141145
raise map_db_exception(
142146
exc,

0 commit comments

Comments
 (0)