Skip to content

Commit a0310f5

Browse files
committed
🎨 Add version_display field to solver responses and update API documentation
1 parent 720fef3 commit a0310f5

File tree

2 files changed

+104
-107
lines changed

2 files changed

+104
-107
lines changed

services/api-server/src/simcore_service_api_server/api/routes/solvers.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ..dependencies.services import get_catalog_service, get_solver_service
2626
from ..dependencies.webserver_http import AuthSession, get_webserver_session
2727
from ._constants import (
28+
FMSG_CHANGELOG_ADDED_IN_VERSION,
2829
FMSG_CHANGELOG_NEW_IN_VERSION,
2930
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT,
3031
create_route_description,
@@ -67,8 +68,6 @@ async def list_solvers(
6768
catalog_service: Annotated[CatalogService, Depends(get_catalog_service)],
6869
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
6970
):
70-
"""Lists all available solvers (latest version)"""
71-
7271
services, _ = await catalog_service.list_latest_releases(
7372
filters=ServiceListFilters(service_type=ServiceType.COMPUTATIONAL),
7473
)
@@ -167,21 +166,25 @@ async def list_solvers_releases(
167166
@router.get(
168167
"/{solver_key:path}/latest",
169168
response_model=Solver,
170-
summary="Get Latest Release of a Solver",
171169
responses=_SOLVER_STATUS_CODES,
170+
description=create_route_description(
171+
base="Gets latest release of a solver",
172+
changelog=[
173+
FMSG_CHANGELOG_ADDED_IN_VERSION.format(
174+
"0.7.1", "`version_display` field in the response"
175+
),
176+
],
177+
),
172178
)
173179
async def get_solver(
174180
solver_key: SolverKeyId,
175181
solver_service: Annotated[SolverService, Depends(get_solver_service)],
176182
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
177183
):
178-
"""Gets latest release of a solver"""
179184
# IMPORTANT: by adding /latest, we avoid changing the order of this entry in the router list
180185
# otherwise, {solver_key:path} will override and consume any of the paths that follow.
181186
try:
182-
solver = await solver_service.get_latest_release(
183-
solver_key=solver_key,
184-
)
187+
solver = await solver_service.get_latest_release(solver_key=solver_key)
185188
solver.url = url_for(
186189
"get_solver_release", solver_key=solver.id, version=solver.version
187190
)
@@ -199,16 +202,20 @@ async def get_solver(
199202
"/{solver_key:path}/releases",
200203
response_model=list[Solver],
201204
responses=_SOLVER_STATUS_CODES,
205+
description=create_route_description(
206+
base="Lists all releases of a given (one) solver",
207+
changelog=[
208+
FMSG_CHANGELOG_ADDED_IN_VERSION.format(
209+
"0.7.1", "`version_display` field in the response"
210+
),
211+
],
212+
),
202213
)
203214
async def list_solver_releases(
204215
solver_key: SolverKeyId,
205216
solver_service: Annotated[SolverService, Depends(get_solver_service)],
206217
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
207218
):
208-
"""Lists all releases of a given (one) solver
209-
210-
SEE get_solver_releases_page for a paginated version of this function
211-
"""
212219
all_releases: list[Solver] = []
213220
for page_params in iter_pagination_params(limit=DEFAULT_PAGINATION_LIMIT):
214221
solvers, page_meta = await solver_service.solver_release_history(
@@ -267,14 +274,21 @@ async def get_solver_releases_page(
267274
"/{solver_key:path}/releases/{version}",
268275
response_model=Solver,
269276
responses=_SOLVER_STATUS_CODES,
277+
description=create_route_description(
278+
base="Gets a specific release of a solver",
279+
changelog=[
280+
FMSG_CHANGELOG_ADDED_IN_VERSION.format(
281+
"0.7.1", "`version_display` field in the response"
282+
),
283+
],
284+
),
270285
)
271286
async def get_solver_release(
272287
solver_key: SolverKeyId,
273288
version: VersionStr,
274289
solver_service: Annotated[SolverService, Depends(get_solver_service)],
275290
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
276291
):
277-
"""Gets a specific release of a solver"""
278292
try:
279293
solver: Solver = await solver_service.get_solver(
280294
solver_key=solver_key,
@@ -302,8 +316,15 @@ async def get_solver_release(
302316
"/{solver_key:path}/releases/{version}/ports",
303317
response_model=OnePage[SolverPort],
304318
responses=_SOLVER_STATUS_CODES,
305-
description="Lists inputs and outputs of a given solver\n\n"
306-
+ FMSG_CHANGELOG_NEW_IN_VERSION.format("0.5.0"),
319+
description=create_route_description(
320+
base="Lists inputs and outputs of a given solver",
321+
changelog=[
322+
FMSG_CHANGELOG_NEW_IN_VERSION.format("0.5.0"),
323+
FMSG_CHANGELOG_ADDED_IN_VERSION.format(
324+
"0.7.1", "`version_display` field in the response"
325+
),
326+
],
327+
),
307328
)
308329
async def list_solver_ports(
309330
solver_key: SolverKeyId,
@@ -322,9 +343,16 @@ async def list_solver_ports(
322343
@router.get(
323344
"/{solver_key:path}/releases/{version}/pricing_plan",
324345
response_model=ServicePricingPlanGetLegacy,
325-
description="Gets solver pricing plan\n\n"
326-
+ FMSG_CHANGELOG_NEW_IN_VERSION.format("0.7"),
327346
responses=_SOLVER_STATUS_CODES,
347+
description=create_route_description(
348+
base="Gets solver pricing plan",
349+
changelog=[
350+
FMSG_CHANGELOG_NEW_IN_VERSION.format("0.7"),
351+
FMSG_CHANGELOG_ADDED_IN_VERSION.format(
352+
"0.7.1", "`version_display` field in the response"
353+
),
354+
],
355+
),
328356
)
329357
async def get_solver_pricing_plan(
330358
solver_key: SolverKeyId,

services/api-server/tests/unit/api_solvers/test_api_routers_solvers.py

Lines changed: 60 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,84 @@
66

77

88
import httpx
9-
import pytest
10-
import simcore_service_api_server.api.routes.solvers
9+
from api_solvers.conftest import solver_version
1110
from pydantic import TypeAdapter
12-
from pytest_mock import MockFixture
11+
from pytest_mock import MockType
1312
from simcore_service_api_server._meta import API_VTAG
1413
from simcore_service_api_server.models.pagination import OnePage
1514
from simcore_service_api_server.models.schemas.solvers import Solver, SolverPort
1615
from starlette import status
1716

1817

19-
@pytest.mark.skip(reason="Still under development. Currently using fake implementation")
20-
async def test_list_solvers(
18+
async def test_list_all_solvers(
19+
mocked_catalog_rpc_api: dict[str, MockType],
2120
client: httpx.AsyncClient,
22-
mocker: MockFixture,
21+
auth: httpx.BasicAuth,
2322
):
24-
warn = mocker.patch.object(
25-
simcore_service_api_server.api.routes.solvers._logger, "warning"
26-
)
23+
response = await client.get(f"/{API_VTAG}/solvers", auth=auth)
24+
assert response.status_code == status.HTTP_200_OK
2725

28-
# list solvers latest releases
29-
resp = await client.get("/v0/solvers")
30-
assert resp.status_code == status.HTTP_200_OK
3126

32-
# No warnings for ValidationError with the fixture
33-
assert (
34-
not warn.called
35-
), f"No warnings expected in this fixture, got {warn.call_args!s}"
27+
async def test_list_all_solvers_paginated(
28+
mocked_catalog_rpc_api: dict[str, MockType],
29+
client: httpx.AsyncClient,
30+
auth: httpx.BasicAuth,
31+
):
32+
response = await client.get(f"/{API_VTAG}/solvers/page", auth=auth)
33+
assert response.status_code == status.HTTP_200_OK
34+
assert len(response.json()["items"]) == response.json()["total"]
3635

37-
data = resp.json()
38-
assert len(data) == 2
3936

40-
for item in data:
41-
solver = Solver(**item)
42-
print(solver.model_dump_json(indent=1, exclude_unset=True))
37+
async def test_list_all_solvers_releases(
38+
mocked_catalog_rpc_api: dict[str, MockType],
39+
client: httpx.AsyncClient,
40+
auth: httpx.BasicAuth,
41+
):
42+
response = await client.get(f"/{API_VTAG}/solvers/releases", auth=auth)
43+
assert response.status_code == status.HTTP_200_OK
4344

44-
# use link to get the same solver
45-
assert solver.url
46-
assert solver.url.host == "api.testserver.io" # cli.base_url
47-
assert solver.url.path
4845

49-
# get_solver_latest_version_by_name
50-
resp0 = await client.get(solver.url.path)
51-
assert resp0.status_code == status.HTTP_501_NOT_IMPLEMENTED
52-
assert f"GET solver {solver.id}" in resp0.json()["errors"][0]
53-
# get_solver
54-
resp1 = await client.get(f"/v0/solvers/{solver.id}")
55-
assert resp1.status_code == status.HTTP_501_NOT_IMPLEMENTED
56-
assert f"GET solver {solver.id}" in resp1.json()["errors"][0]
46+
async def test_list_all_solvers_releases_paginated(
47+
mocked_catalog_rpc_api: dict[str, MockType],
48+
client: httpx.AsyncClient,
49+
auth: httpx.BasicAuth,
50+
):
51+
solver_key = "simcore/services/comp/itis/sleeper"
52+
response = await client.get(
53+
f"/{API_VTAG}/solvers/{solver_key}/releases/page", auth=auth
54+
)
55+
assert response.status_code == status.HTTP_200_OK
56+
assert len(response.json()["items"]) == response.json()["total"]
57+
58+
59+
async def test_list_solver_releases(
60+
mocked_catalog_rpc_api: dict[str, MockType],
61+
client: httpx.AsyncClient,
62+
auth: httpx.BasicAuth,
63+
):
64+
solver_key = "simcore/services/comp/itis/sleeper"
65+
response = await client.get(f"/{API_VTAG}/solvers/{solver_key}/releases", auth=auth)
66+
assert response.status_code == status.HTTP_200_OK
67+
5768

58-
# get_solver_latest_version_by_name
59-
resp2 = await client.get(f"/v0/solvers/{solver.id}/latest")
69+
async def test_get_solver_release(
70+
mocked_catalog_rpc_api: dict[str, MockType],
71+
client: httpx.AsyncClient,
72+
auth: httpx.BasicAuth,
73+
):
74+
solver_key = "simcore/services/comp/itis/sleeper"
75+
solver_version = "2.2.1"
76+
response = await client.get(
77+
f"/{API_VTAG}/solvers/{solver_key}/releases/{solver_version}", auth=auth
78+
)
79+
assert response.status_code == status.HTTP_200_OK
6080

61-
assert resp2.status_code == status.HTTP_501_NOT_IMPLEMENTED
62-
assert f"GET latest {solver.id}" in resp2.json()["errors"][0]
81+
solver = Solver.model_validate(response.json())
82+
assert solver.version_display == "2 Xtreme"
6383

6484

6585
async def test_list_solver_ports(
66-
mocked_catalog_rpc_api: dict,
86+
mocked_catalog_rpc_api: dict[str, MockType],
6787
client: httpx.AsyncClient,
6888
auth: httpx.BasicAuth,
6989
):
@@ -100,60 +120,9 @@ async def test_list_solver_ports(
100120
}
101121

102122

103-
async def test_list_solvers_with_mocked_catalog(
104-
client: httpx.AsyncClient,
105-
mocked_catalog_rpc_api: dict,
106-
auth: httpx.BasicAuth,
107-
):
108-
response = await client.get(f"/{API_VTAG}/solvers", auth=auth)
109-
assert response.status_code == status.HTTP_200_OK
110-
111-
112-
async def test_list_releases_with_mocked_catalog(
113-
client: httpx.AsyncClient,
114-
mocked_catalog_rpc_api: dict,
115-
auth: httpx.BasicAuth,
116-
):
117-
response = await client.get(f"/{API_VTAG}/solvers/releases", auth=auth)
118-
assert response.status_code == status.HTTP_200_OK
119-
120-
121-
async def test_list_solver_page_with_mocked_catalog(
122-
client: httpx.AsyncClient,
123-
mocked_catalog_rpc_api: dict,
124-
auth: httpx.BasicAuth,
125-
):
126-
response = await client.get(f"/{API_VTAG}/solvers/page", auth=auth)
127-
assert response.status_code == status.HTTP_200_OK
128-
assert len(response.json()["items"]) == response.json()["total"]
129-
130-
131-
async def test_list_solver_releases_page_with_mocked_catalog(
132-
client: httpx.AsyncClient,
133-
mocked_catalog_rpc_api: dict,
134-
auth: httpx.BasicAuth,
135-
):
136-
solver_key = "simcore/services/comp/itis/sleeper"
137-
response = await client.get(
138-
f"/{API_VTAG}/solvers/{solver_key}/releases/page", auth=auth
139-
)
140-
assert response.status_code == status.HTTP_200_OK
141-
assert len(response.json()["items"]) == response.json()["total"]
142-
143-
144-
async def test_list_solver_releases_with_mocked_catalog(
145-
client: httpx.AsyncClient,
146-
mocked_catalog_rpc_api: dict,
147-
auth: httpx.BasicAuth,
148-
):
149-
solver_key = "simcore/services/comp/itis/sleeper"
150-
response = await client.get(f"/{API_VTAG}/solvers/{solver_key}/releases", auth=auth)
151-
assert response.status_code == status.HTTP_200_OK
152-
153-
154-
async def test_list_solver_ports_with_mocked_catalog(
123+
async def test_list_solver_ports_again(
124+
mocked_catalog_rpc_api: dict[str, MockType],
155125
client: httpx.AsyncClient,
156-
mocked_catalog_rpc_api: dict,
157126
auth: httpx.BasicAuth,
158127
):
159128
solver_key = "simcore/services/comp/itis/sleeper"

0 commit comments

Comments
 (0)