Skip to content

Commit 8f23e60

Browse files
authored
Merge branch 'master' into pr-osparc-fix-ports-issues
2 parents 56abd95 + c01b1d3 commit 8f23e60

File tree

25 files changed

+635
-134
lines changed

25 files changed

+635
-134
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The SIM-CORE, named **o<sup>2</sup>S<sup>2</sup>PARC** – **O**pen **O**nline *
3333
The aim of o<sup>2</sup>S<sup>2</sup>PARC is to establish a comprehensive, freely accessible, intuitive, and interactive online platform for simulating peripheral nerve system neuromodulation/ stimulation and its impact on organ physiology in a precise and predictive manner.
3434
To achieve this, the platform will comprise both state-of-the art and highly detailed animal and human anatomical models with realistic tissue property distributions that make it possible to perform simulations ranging from the molecular scale up to the complexity of the human body.
3535

36+
3637
## Getting Started
3738

3839
A production instance of **o<sup>2</sup>S<sup>2</sup>PARC** is running at [oSPARC.io](https://osparc.io).
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Prompt
2+
3+
```
4+
Please convert all pydantic model fields that use `Field()` with default values to use the Annotated pattern instead.
5+
Follow these guidelines:
6+
7+
1. Move default values outside of `Field()` like this: `field_name: Annotated[field_type, Field(description="")] = default_value`.
8+
2. Keep all other parameters like validation_alias and descriptions inside `Field()`.
9+
3. For fields using default_factory, keep that parameter as is in the `Field()` constructor, but set the default value outside to DEFAULT_FACTORY from common_library.basic_types. Example: `field_name: Annotated[dict_type, Field(default_factory=dict)] = DEFAULT_FACTORY`.
10+
4. Add the import: `from common_library.basic_types import DEFAULT_FACTORY` if it's not already present.
11+
5. If `Field()` has no parameters (empty), don't use Annotated at all. Just use: `field_name: field_type = default_value`.
12+
6. Leave any model validations, `model_config` settings, and `field_validators` untouched.
13+
```
14+
## Examples
15+
16+
### Before:
17+
18+
```python
19+
from pydantic import BaseModel, Field
20+
21+
class UserModel(BaseModel):
22+
name: str = Field(default="Anonymous", description="User's display name")
23+
age: int = Field(default=18, ge=0, lt=120)
24+
tags: list[str] = Field(default_factory=list, description="User tags")
25+
metadata: dict[str, str] = Field(default_factory=dict)
26+
is_active: bool = Field(default=True)
27+
```
28+
29+
- **After**
30+
31+
```python
32+
from typing import Annotated
33+
from pydantic import BaseModel, Field
34+
from common_library.basic_types import DEFAULT_FACTORY
35+
36+
class UserModel(BaseModel):
37+
name: Annotated[str, Field(description="User's display name")] = "Anonymous"
38+
age: Annotated[int, Field(ge=0, lt=120)] = 18
39+
tags: Annotated[list[str], Field(default_factory=list, description="User tags")] = DEFAULT_FACTORY
40+
metadata: Annotated[dict[str, str], Field(default_factory=dict)] = DEFAULT_FACTORY
41+
is_active: bool = True
42+
```
43+
44+
## Another Example with Complex Fields
45+
46+
### Before:
47+
48+
```python
49+
from pydantic import BaseModel, Field, field_validator
50+
from datetime import datetime
51+
52+
class ProjectModel(BaseModel):
53+
id: str = Field(default_factory=uuid.uuid4, description="Unique project identifier")
54+
name: str = Field(default="Untitled Project", min_length=3, max_length=50)
55+
created_at: datetime = Field(default_factory=datetime.now)
56+
config: dict = Field(default={"version": "1.0", "theme": "default"})
57+
58+
@field_validator("name")
59+
def validate_name(cls, v):
60+
if v.isdigit():
61+
raise ValueError("Name cannot be only digits")
62+
return v
63+
```
64+
65+
### After:
66+
67+
```python
68+
from typing import Annotated
69+
from pydantic import BaseModel, Field, field_validator
70+
from datetime import datetime
71+
from common_library.basic_types import DEFAULT_FACTORY
72+
73+
class ProjectModel(BaseModel):
74+
id: Annotated[str, Field(default_factory=uuid.uuid4, description="Unique project identifier")] = DEFAULT_FACTORY
75+
name: Annotated[str, Field(min_length=3, max_length=50)] = "Untitled Project"
76+
created_at: Annotated[datetime, Field(default_factory=datetime.now)] = DEFAULT_FACTORY
77+
config: dict = {"version": "1.0", "theme": "default"}
78+
79+
@field_validator("name")
80+
def validate_name(cls, v):
81+
if v.isdigit():
82+
raise ValueError("Name cannot be only digits")
83+
return v
84+
```

packages/models-library/src/models_library/api_schemas_api_server/functions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
FunctionJobCollection,
1616
FunctionJobCollectionID,
1717
FunctionJobCollectionIDNotFoundError,
18+
FunctionJobCollectionsListFilters,
1819
FunctionJobCollectionStatus,
1920
FunctionJobID,
2021
FunctionJobIDNotFoundError,
@@ -53,6 +54,7 @@
5354
"FunctionJobCollectionID",
5455
"FunctionJobCollectionIDNotFoundError",
5556
"FunctionJobCollectionStatus",
57+
"FunctionJobCollectionsListFilters",
5658
"FunctionJobID",
5759
"FunctionJobIDNotFoundError",
5860
"FunctionJobStatus",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
FunctionJobCollection,
1919
FunctionJobCollectionID,
2020
FunctionJobCollectionIDNotFoundError,
21+
FunctionJobCollectionsListFilters,
2122
FunctionJobCollectionStatus,
2223
FunctionJobID,
2324
FunctionJobIDNotFoundError,
@@ -70,6 +71,7 @@
7071
"FunctionJobCollectionIDNotFoundError",
7172
"FunctionJobCollectionStatus",
7273
"FunctionJobCollectionStatus",
74+
"FunctionJobCollectionsListFilters",
7375
"FunctionJobID",
7476
"FunctionJobID",
7577
"FunctionJobIDNotFoundError",

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

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from re import Pattern
44
from typing import Annotated, ClassVar, Final, TypeAlias
55

6+
import annotated_types
67
from common_library.basic_types import BootModeEnum, BuildTargetEnum, LogLevel
78
from pydantic import Field, HttpUrl, PositiveInt, StringConstraints
89
from pydantic_core import core_schema
@@ -13,15 +14,16 @@
1314
SIMPLE_VERSION_RE,
1415
UUID_RE,
1516
)
17+
from .utils.common_validators import trim_string_before
1618

1719
assert issubclass(LogLevel, Enum) # nosec
1820
assert issubclass(BootModeEnum, Enum) # nosec
1921
assert issubclass(BuildTargetEnum, Enum) # nosec
2022

2123
__all__: tuple[str, ...] = (
22-
"LogLevel",
2324
"BootModeEnum",
2425
"BuildTargetEnum",
26+
"LogLevel",
2527
)
2628

2729

@@ -70,12 +72,31 @@
7072
UUIDStr: TypeAlias = Annotated[str, StringConstraints(pattern=UUID_RE)]
7173

7274

75+
SafeQueryStr: TypeAlias = Annotated[
76+
str,
77+
StringConstraints(
78+
max_length=512, # Reasonable limit for query parameters to avoid overflows
79+
strip_whitespace=True,
80+
),
81+
annotated_types.doc(
82+
"""
83+
A string that is safe to use in URLs and query parameters.
84+
""",
85+
),
86+
]
87+
88+
7389
# non-empty bounded string used as identifier
7490
# e.g. "123" or "name_123" or "fa327c73-52d8-462a-9267-84eeaf0f90e3" but NOT ""
7591
_ELLIPSIS_CHAR: Final[str] = "..."
7692

7793

7894
class ConstrainedStr(str):
95+
"""Emulates pydantic's v1 constrained types
96+
97+
DEPRECATED: Use instead Annotated[str, StringConstraints(...)]
98+
"""
99+
79100
pattern: str | Pattern[str] | None = None
80101
min_length: int | None = None
81102
max_length: int | None = None
@@ -102,6 +123,11 @@ def __get_pydantic_core_schema__(cls, _source_type, _handler):
102123

103124

104125
class IDStr(ConstrainedStr):
126+
"""Non-empty bounded string used as identifier
127+
128+
DEPRECATED: Use instead Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=100)]
129+
"""
130+
105131
strip_whitespace = True
106132
min_length = 1
107133
max_length = 100
@@ -125,21 +151,36 @@ def concatenate(*args: "IDStr", link_char: str = " ") -> "IDStr":
125151
return IDStr(result)
126152

127153

128-
class ShortTruncatedStr(ConstrainedStr):
129-
# NOTE: Use to input e.g. titles or display names
130-
# A truncated string:
131-
# - Strips whitespaces and truncate strings that exceed the specified characters limit (curtail_length).
132-
# - Ensures that the **input** data length to the API is controlled and prevents exceeding large inputs silently, i.e. without raising errors.
133-
# SEE https://github.com/ITISFoundation/osparc-simcore/pull/5989#discussion_r1650506583
134-
strip_whitespace = True
135-
curtail_length = 600
136-
154+
_SHORT_TRUNCATED_STR_MAX_LENGTH: Final[int] = 600
155+
ShortTruncatedStr: TypeAlias = Annotated[
156+
str,
157+
StringConstraints(strip_whitespace=True),
158+
trim_string_before(max_length=_SHORT_TRUNCATED_STR_MAX_LENGTH),
159+
annotated_types.doc(
160+
"""
161+
A truncated string used to input e.g. titles or display names.
162+
Strips whitespaces and truncate strings that exceed the specified characters limit (curtail_length).
163+
Ensures that the **input** data length to the API is controlled and prevents exceeding large inputs silently,
164+
i.e. without raising errors.
165+
"""
166+
# SEE https://github.com/ITISFoundation/osparc-simcore/pull/5989#discussion_r1650506583
167+
),
168+
]
137169

138-
class LongTruncatedStr(ConstrainedStr):
139-
# NOTE: Use to input e.g. descriptions or summaries
140-
# Analogous to ShortTruncatedStr
141-
strip_whitespace = True
142-
curtail_length = 65536 # same as github descripton
170+
_LONG_TRUNCATED_STR_MAX_LENGTH: Final[int] = 65536 # same as github description
171+
LongTruncatedStr: TypeAlias = Annotated[
172+
str,
173+
StringConstraints(strip_whitespace=True),
174+
trim_string_before(max_length=_LONG_TRUNCATED_STR_MAX_LENGTH),
175+
annotated_types.doc(
176+
"""
177+
A truncated string used to input e.g. descriptions or summaries.
178+
Strips whitespaces and truncate strings that exceed the specified characters limit (curtail_length).
179+
Ensures that the **input** data length to the API is controlled and prevents exceeding large inputs silently,
180+
i.e. without raising errors.
181+
"""
182+
),
183+
]
143184

144185

145186
# auto-incremented primary-key IDs

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from common_library.errors_classes import OsparcErrorMixin
77
from models_library import projects
8+
from models_library.basic_regex import UUID_RE_BASE
9+
from models_library.basic_types import ConstrainedStr
810
from models_library.services_types import ServiceKey, ServiceVersion
911
from pydantic import BaseModel, Field
1012

@@ -274,3 +276,13 @@ class FunctionJobCollectionDB(BaseModel):
274276

275277
class RegisteredFunctionJobCollectionDB(FunctionJobCollectionDB):
276278
uuid: FunctionJobCollectionID
279+
280+
281+
class FunctionIDString(ConstrainedStr):
282+
pattern = UUID_RE_BASE
283+
284+
285+
class FunctionJobCollectionsListFilters(BaseModel):
286+
"""Filters for listing function job collections"""
287+
288+
has_function_id: FunctionIDString | None = None

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
from models_library.basic_types import ConstrainedStr
1212
from models_library.folders import FolderID
1313
from models_library.workspaces import WorkspaceID
14-
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
14+
from pydantic import (
15+
BaseModel,
16+
ConfigDict,
17+
Field,
18+
HttpUrl,
19+
StringConstraints,
20+
field_validator,
21+
)
1522

1623
from .basic_regex import DATE_RE, UUID_RE_BASE
1724
from .emails import LowerCaseEmailStr
@@ -35,8 +42,7 @@
3542
_DATETIME_FORMAT: Final[str] = "%Y-%m-%dT%H:%M:%S.%fZ"
3643

3744

38-
class ProjectIDStr(ConstrainedStr):
39-
pattern = UUID_RE_BASE
45+
ProjectIDStr: TypeAlias = Annotated[str, StringConstraints(pattern=UUID_RE_BASE)]
4046

4147

4248
class DateTimeStr(ConstrainedStr):

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import Final
1+
from typing import Annotated, Final, TypeAlias
22

33
from models_library.basic_types import ConstrainedStr
4-
from pydantic import TypeAdapter
4+
from pydantic import StringConstraints, TypeAdapter
55

66
REGEX_RABBIT_QUEUE_ALLOWED_SYMBOLS: Final[str] = r"^[\w\-\.]*$"
77

@@ -21,7 +21,9 @@ def from_entries(cls, entries: dict[str, str]) -> "RPCNamespace":
2121
return TypeAdapter(cls).validate_python(composed_string)
2222

2323

24-
class RPCMethodName(ConstrainedStr):
25-
pattern = REGEX_RABBIT_QUEUE_ALLOWED_SYMBOLS
26-
min_length: int = 1
27-
max_length: int = 252
24+
RPCMethodName: TypeAlias = Annotated[
25+
str,
26+
StringConstraints(
27+
pattern=REGEX_RABBIT_QUEUE_ALLOWED_SYMBOLS, min_length=1, max_length=252
28+
),
29+
]

packages/models-library/tests/test_basic_types.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from models_library.basic_types import (
5+
_SHORT_TRUNCATED_STR_MAX_LENGTH,
56
EnvVarKey,
67
IDStr,
78
MD5Str,
@@ -76,16 +77,28 @@ def test_string_identifier_constraint_type():
7677

7778

7879
def test_short_truncated_string():
80+
curtail_length = _SHORT_TRUNCATED_STR_MAX_LENGTH
7981
assert (
80-
TypeAdapter(ShortTruncatedStr).validate_python(
81-
"X" * ShortTruncatedStr.curtail_length
82-
)
83-
== "X" * ShortTruncatedStr.curtail_length
84-
)
82+
TypeAdapter(ShortTruncatedStr).validate_python("X" * curtail_length)
83+
== "X" * curtail_length
84+
), "Max length string should remain intact"
8585

8686
assert (
87-
TypeAdapter(ShortTruncatedStr).validate_python(
88-
"X" * (ShortTruncatedStr.curtail_length + 1)
89-
)
90-
== "X" * ShortTruncatedStr.curtail_length
91-
)
87+
TypeAdapter(ShortTruncatedStr).validate_python("X" * (curtail_length + 1))
88+
== "X" * curtail_length
89+
), "Overlong string should be truncated exactly to max length"
90+
91+
assert (
92+
TypeAdapter(ShortTruncatedStr).validate_python("X" * (curtail_length + 100))
93+
== "X" * curtail_length
94+
), "Much longer string should still truncate to exact max length"
95+
96+
# below limit
97+
assert TypeAdapter(ShortTruncatedStr).validate_python(
98+
"X" * (curtail_length - 1)
99+
) == "X" * (curtail_length - 1), "Under-length string should not be modified"
100+
101+
# spaces are trimmed
102+
assert (
103+
TypeAdapter(ShortTruncatedStr).validate_python(" " * (curtail_length + 1)) == ""
104+
), "Only-whitespace string should become empty string"

packages/models-library/tests/test_projects.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
import pytest
99
from faker import Faker
10-
from models_library.api_schemas_webserver.projects import LongTruncatedStr, ProjectPatch
10+
from models_library.api_schemas_webserver.projects import ProjectPatch
11+
from models_library.basic_types import _LONG_TRUNCATED_STR_MAX_LENGTH
1112
from models_library.projects import Project
1213

1314

@@ -47,8 +48,7 @@ def test_project_with_thumbnail_as_empty_string(minimal_project: dict[str, Any])
4748

4849
def test_project_patch_truncates_description():
4950
# NOTE: checks https://github.com/ITISFoundation/osparc-simcore/issues/5988
50-
assert LongTruncatedStr.curtail_length
51-
len_truncated = int(LongTruncatedStr.curtail_length)
51+
len_truncated = _LONG_TRUNCATED_STR_MAX_LENGTH
5252

5353
long_description = "X" * (len_truncated + 10)
5454
assert len(long_description) > len_truncated

0 commit comments

Comments
 (0)