Skip to content

Commit 9d62136

Browse files
authored
✨ api-server: listing of all solver jobs (#7566)
1 parent bc131b7 commit 9d62136

File tree

34 files changed

+1478
-579
lines changed

34 files changed

+1478
-579
lines changed

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

+23-5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ProjectJobRpcGet(BaseModel):
2929
workbench: NodesDict
3030

3131
# timestamps
32-
creation_at: datetime
32+
created_at: datetime
3333
modified_at: datetime
3434

3535
# Specific to jobs
@@ -43,12 +43,30 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
4343
"examples": [
4444
{
4545
"uuid": "12345678-1234-5678-1234-123456789012",
46-
"name": "My project",
47-
"description": "My project description",
46+
"name": "A solver job",
47+
"description": "A description of a solver job with a single node",
4848
"workbench": {f"{uuid4()}": n for n in nodes_examples[2:3]},
49-
"creation_at": "2023-01-01T00:00:00Z",
49+
"created_at": "2023-01-01T00:00:00Z",
5050
"modified_at": "2023-01-01T00:00:00Z",
51-
"job_parent_resource_name": "solvers/foo/release/1.2.3",
51+
"job_parent_resource_name": "solvers/simcore%2Fservices%2Fcomp%2Fitis%2Fsleeper/releases/2.0.2",
52+
},
53+
{
54+
"uuid": "00000000-1234-5678-1234-123456789012",
55+
"name": "A study job",
56+
"description": "A description of a study job with many node",
57+
"workbench": {f"{uuid4()}": n for n in nodes_examples},
58+
"created_at": "2023-02-01T00:00:00Z",
59+
"modified_at": "2023-02-01T00:00:00Z",
60+
"job_parent_resource_name": "studies/96642f2a-a72c-11ef-8776-02420a00087d",
61+
},
62+
{
63+
"uuid": "00000000-0000-5678-1234-123456789012",
64+
"name": "A program job",
65+
"description": "A program of a solver job with a single node",
66+
"workbench": {f"{uuid4()}": n for n in nodes_examples[2:3]},
67+
"created_at": "2023-03-01T00:00:00Z",
68+
"modified_at": "2023-03-01T00:00:00Z",
69+
"job_parent_resource_name": "program/simcore%2Fservices%2Fdynamic%2Fjupyter/releases/5.0.2",
5270
},
5371
]
5472
}

packages/models-library/src/models_library/utils/common_validators.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" Reusable validators
1+
"""Reusable validators
22
33
Example:
44
@@ -22,10 +22,19 @@ class MyModel(BaseModel):
2222

2323
from common_library.json_serialization import json_loads
2424
from orjson import JSONDecodeError
25-
from pydantic import BaseModel
25+
from pydantic import BaseModel, BeforeValidator
2626
from pydantic.alias_generators import to_camel
2727

2828

29+
def trim_string_before(max_length: int) -> BeforeValidator:
30+
def _trim(value: str):
31+
if isinstance(value, str):
32+
return value[:max_length]
33+
return value
34+
35+
return BeforeValidator(_trim)
36+
37+
2938
def empty_str_to_none_pre_validator(value: Any):
3039
if isinstance(value, str) and value.strip() == "":
3140
return None

packages/models-library/tests/test_utils_common_validators.py

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from enum import Enum
2+
from typing import Annotated
23

34
import pytest
45
from models_library.utils.common_validators import (
56
create_enums_pre_validator,
67
empty_str_to_none_pre_validator,
78
none_to_empty_str_pre_validator,
89
null_or_none_str_to_none_validator,
10+
trim_string_before,
911
)
10-
from pydantic import BaseModel, ValidationError, field_validator
12+
from pydantic import BaseModel, StringConstraints, ValidationError, field_validator
1113

1214

1315
def test_enums_pre_validator():
@@ -89,3 +91,67 @@ class Model(BaseModel):
8991

9092
model = Model.model_validate({"message": ""})
9193
assert model == Model.model_validate({"message": ""})
94+
95+
96+
def test_trim_string_before():
97+
max_length = 10
98+
99+
class ModelWithTrim(BaseModel):
100+
text: Annotated[str, trim_string_before(max_length=max_length)]
101+
102+
# Test with string shorter than max_length
103+
short_text = "Short"
104+
model = ModelWithTrim(text=short_text)
105+
assert model.text == short_text
106+
107+
# Test with string equal to max_length
108+
exact_text = "1234567890" # 10 characters
109+
model = ModelWithTrim(text=exact_text)
110+
assert model.text == exact_text
111+
112+
# Test with string longer than max_length
113+
long_text = "This is a very long text that should be trimmed"
114+
model = ModelWithTrim(text=long_text)
115+
assert model.text == long_text[:max_length]
116+
assert len(model.text) == max_length
117+
118+
# Test with non-string value (should be left unchanged)
119+
class ModelWithTrimOptional(BaseModel):
120+
text: Annotated[str | None, trim_string_before(max_length=max_length)]
121+
122+
model = ModelWithTrimOptional(text=None)
123+
assert model.text is None
124+
125+
126+
def test_trim_string_before_with_string_constraints():
127+
max_length = 10
128+
129+
class ModelWithTrimAndConstraints(BaseModel):
130+
text: Annotated[
131+
str | None,
132+
StringConstraints(
133+
max_length=max_length
134+
), # NOTE: order does not matter for validation but has an effect in the openapi schema
135+
trim_string_before(max_length=max_length),
136+
]
137+
138+
# Check that the OpenAPI schema contains the string constraint
139+
schema = ModelWithTrimAndConstraints.model_json_schema()
140+
assert schema["properties"]["text"] == {
141+
"anyOf": [{"maxLength": max_length, "type": "string"}, {"type": "null"}],
142+
"title": "Text",
143+
}
144+
145+
# Test with string longer than max_length
146+
# This should pass because trim_string_before runs first and trims the input
147+
# before StringConstraints validation happens
148+
long_text = "This is a very long text that should be trimmed"
149+
model = ModelWithTrimAndConstraints(text=long_text)
150+
assert model.text is not None
151+
assert model.text == long_text[:max_length]
152+
assert len(model.text) == max_length
153+
154+
# Test with string exactly at max_length
155+
exact_text = "1234567890" # 10 characters
156+
model = ModelWithTrimAndConstraints(text=exact_text)
157+
assert model.text == exact_text

packages/pytest-simcore/src/pytest_simcore/helpers/webserver_rpc_server.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from models_library.products import ProductName
99
from models_library.projects import ProjectID
1010
from models_library.rest_pagination import PageOffsetInt
11-
from models_library.rpc.webserver.projects import PageRpcProjectJobRpcGet
11+
from models_library.rpc.webserver.projects import (
12+
PageRpcProjectJobRpcGet,
13+
ProjectJobRpcGet,
14+
)
1215
from models_library.rpc_pagination import (
1316
DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
1417
PageLimitInt,
@@ -54,19 +57,28 @@ async def list_projects_marked_as_jobs(
5457
offset: PageOffsetInt = 0,
5558
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
5659
# filters
57-
job_parent_resource_name_filter: str | None = None,
60+
job_parent_resource_name_prefix: str | None = None,
5861
) -> PageRpcProjectJobRpcGet:
5962
assert rpc_client
6063
assert product_name
6164
assert user_id
6265

63-
if job_parent_resource_name_filter:
64-
assert not job_parent_resource_name_filter.startswith("/")
66+
if job_parent_resource_name_prefix:
67+
assert not job_parent_resource_name_prefix.startswith("/")
68+
assert not job_parent_resource_name_prefix.endswith("%")
69+
assert not job_parent_resource_name_prefix.startswith("%")
6570

66-
items = PageRpcProjectJobRpcGet.model_json_schema()["examples"]
71+
items = [
72+
item
73+
for item in ProjectJobRpcGet.model_json_schema()["examples"]
74+
if job_parent_resource_name_prefix is None
75+
or item.get("job_parent_resource_name").startswith(
76+
job_parent_resource_name_prefix
77+
)
78+
]
6779

6880
return PageRpcProjectJobRpcGet.create(
69-
items[offset, : offset + limit],
81+
items[offset : offset + limit],
7082
total=len(items),
7183
limit=limit,
7284
offset=offset,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def list_projects_marked_as_jobs(
5252
offset: PageOffsetInt = 0,
5353
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
5454
# filters
55-
job_parent_resource_name_filter: str | None = None,
55+
job_parent_resource_name_prefix: str | None = None,
5656
) -> PageRpcProjectJobRpcGet:
5757
result = await rpc_client.request(
5858
WEBSERVER_RPC_NAMESPACE,
@@ -61,7 +61,7 @@ async def list_projects_marked_as_jobs(
6161
user_id=user_id,
6262
offset=offset,
6363
limit=limit,
64-
job_parent_resource_name_filter=job_parent_resource_name_filter,
64+
job_parent_resource_name_prefix=job_parent_resource_name_prefix,
6565
)
6666
assert TypeAdapter(PageRpcProjectJobRpcGet).validate_python(result) # nosec
6767
return cast(PageRpcProjectJobRpcGet, result)

services/api-server/docs/api-server.drawio.svg

+745
Loading

0 commit comments

Comments
 (0)