Skip to content

Commit d7e9263

Browse files
♻️ Use catalog rpc client in api-server (#7541)
1 parent 9a00aa7 commit d7e9263

29 files changed

+819
-349
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from models_library.products import ProductName
1313
from models_library.rest_pagination import PageOffsetInt
1414
from models_library.rpc_pagination import PageLimitInt, PageRpc
15+
from models_library.services_enums import ServiceType
1516
from models_library.services_history import ServiceRelease
17+
from models_library.services_regex import (
18+
COMPUTATIONAL_SERVICE_KEY_RE,
19+
DYNAMIC_SERVICE_KEY_RE,
20+
)
1621
from models_library.services_types import ServiceKey, ServiceVersion
1722
from models_library.users import UserID
1823
from pydantic import NonNegativeInt, TypeAdapter
@@ -65,6 +70,14 @@ async def get_service(
6570
got.version = service_version
6671
got.key = service_key
6772

73+
if DYNAMIC_SERVICE_KEY_RE.match(got.key):
74+
got.service_type = ServiceType.DYNAMIC
75+
elif COMPUTATIONAL_SERVICE_KEY_RE.match(got.key):
76+
got.service_type = ServiceType.COMPUTATIONAL
77+
else:
78+
msg = "Service type not recognized. Please extend the mock yourself"
79+
raise RuntimeError(msg)
80+
6881
return got
6982

7083
async def update_service(

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
class CapturedParameterSchema(BaseModel):
99
title: str | None = None
10-
type_: Literal["str", "int", "float", "bool"] | None = Field(None, alias="type")
10+
type_: Literal["str", "int", "float", "bool", "null"] | None = Field(
11+
None, alias="type"
12+
)
1113
pattern: str | None = None
1214
format_: Literal["uuid"] | None = Field(None, alias="format")
1315
exclusiveMinimum: bool | None = None

services/api-server/openapi.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@
223223
"files"
224224
],
225225
"summary": "List Files",
226-
"description": "Lists all files stored in the system\n\nSEE `get_files_page` for a paginated version of this function",
226+
"description": "\ud83d\udea8 **Deprecated**: This endpoint is deprecated and will be removed in a future release.\nPlease use `GET /v0/files/page` instead.\n\n\n\nLists all files stored in the system\n\nAdded in *version 0.5*: \n\nRemoved in *version 0.7*: This endpoint is deprecated and will be removed in a future version",
227227
"operationId": "list_files",
228228
"responses": {
229229
"200": {
@@ -1414,7 +1414,7 @@
14141414
"solvers"
14151415
],
14161416
"summary": "List Solvers",
1417-
"description": "Lists all available solvers (latest version)\n\nSEE get_solvers_page for paginated version of this function",
1417+
"description": "\ud83d\udea8 **Deprecated**: This endpoint is deprecated and will be removed in a future release.\nPlease use `GET /v0/solvers/page` instead.\n\n\n\nLists all available solvers (latest version)\n\nNew in *version 0.5.0*\n\nRemoved in *version 0.7*: This endpoint is deprecated and will be removed in a future version",
14181418
"operationId": "list_solvers",
14191419
"responses": {
14201420
"200": {
@@ -2356,7 +2356,7 @@
23562356
"solvers"
23572357
],
23582358
"summary": "List Jobs",
2359-
"description": "List of jobs in a specific released solver (limited to 20 jobs)\n\n- DEPRECATION: This implementation and returned values are deprecated and the will be replaced by that of get_jobs_page\n- SEE `get_jobs_page` for paginated version of this function",
2359+
"description": "\ud83d\udea8 **Deprecated**: This endpoint is deprecated and will be removed in a future release.\nPlease use `GET /{solver_key}/releases/{version}/jobs/page` instead.\n\n\n\nList of jobs in a specific released solver\n\nNew in *version 0.5*\n\nRemoved in *version 0.7*: This endpoint is deprecated and will be removed in a future version",
23602360
"operationId": "list_jobs",
23612361
"security": [
23622362
{

services/api-server/src/simcore_service_api_server/_service.py

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
from collections.abc import Callable
3+
from typing import Annotated
4+
5+
from fastapi import Depends
6+
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
7+
from models_library.projects import ProjectID
8+
from models_library.projects_nodes_io import NodeID
9+
from pydantic import HttpUrl
10+
from servicelib.fastapi.app_state import SingletonInAppStateMixin
11+
from servicelib.logging_utils import log_context
12+
13+
from .api.dependencies.webserver_http import get_webserver_session
14+
from .models.schemas.jobs import Job, JobInputs
15+
from .models.schemas.programs import Program
16+
from .models.schemas.solvers import Solver
17+
from .services_http.solver_job_models_converters import (
18+
create_job_from_project,
19+
create_new_project_for_job,
20+
)
21+
from .services_http.webserver import AuthSession
22+
23+
_logger = logging.getLogger(__name__)
24+
25+
26+
class JobService(SingletonInAppStateMixin):
27+
app_state_name = "JobService"
28+
_web_rest_api: AuthSession
29+
30+
def __init__(
31+
self, web_rest_api: Annotated[AuthSession, Depends(get_webserver_session)]
32+
):
33+
self._web_rest_api = web_rest_api
34+
35+
async def create_job(
36+
self,
37+
*,
38+
solver_or_program: Solver | Program,
39+
inputs: JobInputs,
40+
parent_project_uuid: ProjectID | None,
41+
parent_node_id: NodeID | None,
42+
url_for: Callable[..., HttpUrl],
43+
hidden: bool,
44+
) -> tuple[Job, ProjectGet]:
45+
# creates NEW job as prototype
46+
pre_job = Job.create_job_from_solver_or_program(
47+
solver_or_program_name=solver_or_program.name, inputs=inputs
48+
)
49+
with log_context(
50+
logger=_logger, level=logging.DEBUG, msg=f"Creating job {pre_job.name}"
51+
):
52+
project_in: ProjectCreateNew = create_new_project_for_job(
53+
solver_or_program, pre_job, inputs
54+
)
55+
new_project: ProjectGet = await self._web_rest_api.create_project(
56+
project_in,
57+
is_hidden=hidden,
58+
parent_project_uuid=parent_project_uuid,
59+
parent_node_id=parent_node_id,
60+
)
61+
62+
assert new_project # nosec
63+
assert new_project.uuid == pre_job.id # nosec
64+
65+
# for consistency, it rebuild job
66+
job = create_job_from_project(
67+
solver_or_program=solver_or_program, project=new_project, url_for=url_for
68+
)
69+
assert job.id == pre_job.id # nosec
70+
assert job.name == pre_job.name # nosec
71+
assert job.name == Job.compose_resource_name(
72+
parent_name=solver_or_program.resource_name,
73+
job_id=job.id,
74+
)
75+
return job, new_project
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Annotated
2+
3+
from fastapi import Depends
4+
from models_library.basic_types import VersionStr
5+
from models_library.services_enums import ServiceType
6+
7+
from .models.schemas.programs import Program, ProgramKeyId
8+
from .services_rpc.catalog import CatalogService
9+
10+
11+
class ProgramService:
12+
_catalog_service: CatalogService
13+
14+
def __init__(self, _catalog_service: Annotated[CatalogService, Depends()]):
15+
self._catalog_service = _catalog_service
16+
17+
async def get_program(
18+
self,
19+
*,
20+
user_id: int,
21+
name: ProgramKeyId,
22+
version: VersionStr,
23+
product_name: str,
24+
) -> Program:
25+
service = await self._catalog_service.get(
26+
user_id=user_id,
27+
name=name,
28+
version=version,
29+
product_name=product_name,
30+
)
31+
assert service.service_type == ServiceType.DYNAMIC # nosec
32+
33+
return Program.create_from_service(service)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Annotated
2+
3+
from common_library.pagination_tools import iter_pagination_params
4+
from fastapi import Depends
5+
from models_library.basic_types import VersionStr
6+
from models_library.products import ProductName
7+
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
8+
from models_library.services_enums import ServiceType
9+
from models_library.services_history import ServiceRelease
10+
from models_library.users import UserID
11+
from packaging.version import Version
12+
13+
from .models.schemas.solvers import Solver, SolverKeyId
14+
from .services_rpc.catalog import CatalogService
15+
16+
DEFAULT_PAGINATION_LIMIT = MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE - 1
17+
18+
19+
class SolverService:
20+
_catalog_service: CatalogService
21+
22+
def __init__(self, catalog_service: Annotated[CatalogService, Depends()]):
23+
self._catalog_service = catalog_service
24+
25+
async def get_solver(
26+
self,
27+
*,
28+
user_id: UserID,
29+
name: SolverKeyId,
30+
version: VersionStr,
31+
product_name: ProductName,
32+
) -> Solver:
33+
service = await self._catalog_service.get(
34+
user_id=user_id,
35+
name=name,
36+
version=version,
37+
product_name=product_name,
38+
)
39+
assert ( # nosec
40+
service.service_type == ServiceType.COMPUTATIONAL
41+
), "Expected by SolverName regex"
42+
43+
return Solver.create_from_service(service)
44+
45+
async def get_latest_release(
46+
self,
47+
*,
48+
user_id: int,
49+
solver_key: SolverKeyId,
50+
product_name: str,
51+
) -> Solver:
52+
service_releases: list[ServiceRelease] = []
53+
for page_params in iter_pagination_params(limit=DEFAULT_PAGINATION_LIMIT):
54+
releases, page_meta = await self._catalog_service.list_release_history(
55+
user_id=user_id,
56+
service_key=solver_key,
57+
product_name=product_name,
58+
offset=page_params.offset,
59+
limit=page_params.limit,
60+
)
61+
page_params.total_number_of_items = page_meta.total
62+
service_releases.extend(releases)
63+
64+
release = sorted(service_releases, key=lambda s: Version(s.version))[-1]
65+
service = await self._catalog_service.get(
66+
user_id=user_id,
67+
name=solver_key,
68+
version=release.version,
69+
product_name=product_name,
70+
)
71+
72+
return Solver.create_from_service(service)

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,41 @@
2020

2121
# removed on inputs/outputs in routes
2222
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT: Final[str] = "Removed in *version {}*: {}\n"
23+
24+
FMSG_DEPRECATED_ROUTE_NOTICE: Final[str] = (
25+
"🚨 **Deprecated**: This endpoint is deprecated and will be removed in a future release.\n"
26+
"Please use `{}` instead.\n\n"
27+
)
28+
29+
30+
def create_route_description(
31+
*,
32+
base: str = "",
33+
deprecated: bool = False,
34+
alternative: str | None = None, # alternative
35+
changelog: list[str] | None = None
36+
) -> str:
37+
"""
38+
Builds a consistent route description with optional deprecation and changelog information.
39+
40+
Args:
41+
base (str): Main route description.
42+
deprecated (tuple): (retirement_date, alternative_route) if deprecated.
43+
changelog (List[str]): List of formatted changelog strings.
44+
45+
Returns:
46+
str: Final description string.
47+
"""
48+
parts = []
49+
50+
if deprecated:
51+
assert alternative, "If deprecated, alternative must be provided" # nosec
52+
parts.append(FMSG_DEPRECATED_ROUTE_NOTICE.format(alternative))
53+
54+
if base:
55+
parts.append(base)
56+
57+
if changelog:
58+
parts.append("\n".join(changelog))
59+
60+
return "\n\n".join(parts)

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
get_upload_links_from_s3,
3030
)
3131
from simcore_sdk.node_ports_common.filemanager import upload_path as storage_upload_path
32+
from simcore_service_api_server.api.routes._constants import (
33+
FMSG_CHANGELOG_ADDED_IN_VERSION,
34+
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT,
35+
create_route_description,
36+
)
3237
from starlette.datastructures import URL
3338
from starlette.responses import RedirectResponse
3439

@@ -132,7 +137,23 @@ async def _create_domain_file(
132137
return file
133138

134139

135-
@router.get("", response_model=list[OutputFile], responses=_FILE_STATUS_CODES)
140+
@router.get(
141+
"",
142+
response_model=list[OutputFile],
143+
responses=_FILE_STATUS_CODES,
144+
description=create_route_description(
145+
base="Lists all files stored in the system",
146+
deprecated=True,
147+
alternative="GET /v0/files/page",
148+
changelog=[
149+
FMSG_CHANGELOG_ADDED_IN_VERSION.format("0.5", ""),
150+
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT.format(
151+
"0.7",
152+
"This endpoint is deprecated and will be removed in a future version",
153+
),
154+
],
155+
),
156+
)
136157
async def list_files(
137158
storage_client: Annotated[StorageApi, Depends(get_api_client(StorageApi))],
138159
user_id: Annotated[int, Depends(get_current_user_id)],

0 commit comments

Comments
 (0)