Skip to content

♻️ Use catalog rpc client in api-server #7541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5dfa550
_service.py -> service_jobs.py
bisgaard-itis Apr 14, 2025
e894814
start program service
bisgaard-itis Apr 14, 2025
9923b39
create program service
bisgaard-itis Apr 15, 2025
be7bff6
add unit test for getting program
bisgaard-itis Apr 15, 2025
ca5e159
cleanup function arguments
bisgaard-itis Apr 15, 2025
9e7bd41
introduce job_service and 'constuct' service layers in dependency inj…
bisgaard-itis Apr 15, 2025
03c5eb3
start adding test for creating job
bisgaard-itis Apr 15, 2025
0cf0dd1
minor changes
bisgaard-itis Apr 15, 2025
d94caa7
add legacy tasks to webserver
bisgaard-itis Apr 15, 2025
b744dd0
mock webserver part of job creation test
bisgaard-itis Apr 15, 2025
5c06e9c
make create job test pass
bisgaard-itis Apr 15, 2025
09c9507
use dependency indenction in class constructor
bisgaard-itis Apr 16, 2025
ec6a680
use dependency injection in JobService constructor
bisgaard-itis Apr 16, 2025
93b9966
minor changes
bisgaard-itis Apr 16, 2025
aac464c
fix tests
bisgaard-itis Apr 16, 2025
e6e6baa
remove get_solver method from http catalog client
bisgaard-itis Apr 16, 2025
65916c4
add test for getting latest solver
bisgaard-itis Apr 16, 2025
f51e7c7
Merge branch 'master' into 7525-use-catalog-rpc-client
bisgaard-itis Apr 16, 2025
d07461e
make pylint happy
bisgaard-itis Apr 16, 2025
52a97f2
fix webserver openapi test
bisgaard-itis Apr 16, 2025
105b498
Revert "fix webserver openapi test"
bisgaard-itis Apr 16, 2025
d67c526
Revert "add legacy tasks to webserver"
bisgaard-itis Apr 16, 2025
35f8097
add exception handling in CatalogService
bisgaard-itis Apr 16, 2025
b69c304
add error handling to CatalogService
bisgaard-itis Apr 16, 2025
27ec715
@pcrespov clean up JobService
bisgaard-itis Apr 16, 2025
086f9ea
Merge branch 'master' into 7525-use-catalog-rpc-client
bisgaard-itis Apr 17, 2025
fc9245f
add Deprecated note @pcrespov
bisgaard-itis Apr 17, 2025
7318dc1
add deprecation warnings to all listing endpoints
bisgaard-itis Apr 17, 2025
9f591b1
fixes to deprecation warnings
bisgaard-itis Apr 17, 2025
c6ee0c8
remove listing programs endpoint
bisgaard-itis Apr 17, 2025
06a98bc
Merge branch 'master' into 7525-use-catalog-rpc-client
bisgaard-itis Apr 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
from models_library.products import ProductName
from models_library.rest_pagination import PageOffsetInt
from models_library.rpc_pagination import PageLimitInt, PageRpc
from models_library.services_enums import ServiceType
from models_library.services_history import ServiceRelease
from models_library.services_regex import (
COMPUTATIONAL_SERVICE_KEY_RE,
DYNAMIC_SERVICE_KEY_RE,
)
from models_library.services_types import ServiceKey, ServiceVersion
from models_library.users import UserID
from pydantic import NonNegativeInt, TypeAdapter
Expand Down Expand Up @@ -65,6 +70,14 @@ async def get_service(
got.version = service_version
got.key = service_key

if DYNAMIC_SERVICE_KEY_RE.match(got.key):
got.service_type = ServiceType.DYNAMIC
elif COMPUTATIONAL_SERVICE_KEY_RE.match(got.key):
got.service_type = ServiceType.COMPUTATIONAL
else:
msg = "Service type not recognized. Please extend the mock yourself"
raise RuntimeError(msg)

return got

async def update_service(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

class CapturedParameterSchema(BaseModel):
title: str | None = None
type_: Literal["str", "int", "float", "bool"] | None = Field(None, alias="type")
type_: Literal["str", "int", "float", "bool", "null"] | None = Field(
None, alias="type"
)
pattern: str | None = None
format_: Literal["uuid"] | None = Field(None, alias="format")
exclusiveMinimum: bool | None = None
Expand Down
6 changes: 3 additions & 3 deletions services/api-server/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
"files"
],
"summary": "List Files",
"description": "Lists all files stored in the system\n\nSEE `get_files_page` for a paginated version of this function",
"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",
"operationId": "list_files",
"responses": {
"200": {
Expand Down Expand Up @@ -1414,7 +1414,7 @@
"solvers"
],
"summary": "List Solvers",
"description": "Lists all available solvers (latest version)\n\nSEE get_solvers_page for paginated version of this function",
"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",
"operationId": "list_solvers",
"responses": {
"200": {
Expand Down Expand Up @@ -2356,7 +2356,7 @@
"solvers"
],
"summary": "List Jobs",
"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",
"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",
"operationId": "list_jobs",
"security": [
{
Expand Down
58 changes: 0 additions & 58 deletions services/api-server/src/simcore_service_api_server/_service.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import logging
from collections.abc import Callable
from typing import Annotated

from fastapi import Depends
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
from models_library.projects import ProjectID
from models_library.projects_nodes_io import NodeID
from pydantic import HttpUrl
from servicelib.fastapi.app_state import SingletonInAppStateMixin
from servicelib.logging_utils import log_context

from .api.dependencies.webserver_http import get_webserver_session
from .models.schemas.jobs import Job, JobInputs
from .models.schemas.programs import Program
from .models.schemas.solvers import Solver
from .services_http.solver_job_models_converters import (
create_job_from_project,
create_new_project_for_job,
)
from .services_http.webserver import AuthSession

_logger = logging.getLogger(__name__)


class JobService(SingletonInAppStateMixin):
app_state_name = "JobService"
_web_rest_api: AuthSession

def __init__(
self, web_rest_api: Annotated[AuthSession, Depends(get_webserver_session)]
):
self._web_rest_api = web_rest_api

async def create_job(
self,
*,
solver_or_program: Solver | Program,
inputs: JobInputs,
parent_project_uuid: ProjectID | None,
parent_node_id: NodeID | None,
url_for: Callable[..., HttpUrl],
hidden: bool,
) -> tuple[Job, ProjectGet]:
# creates NEW job as prototype
pre_job = Job.create_job_from_solver_or_program(
solver_or_program_name=solver_or_program.name, inputs=inputs
)
with log_context(
logger=_logger, level=logging.DEBUG, msg=f"Creating job {pre_job.name}"
):
project_in: ProjectCreateNew = create_new_project_for_job(
solver_or_program, pre_job, inputs
)
new_project: ProjectGet = await self._web_rest_api.create_project(
project_in,
is_hidden=hidden,
parent_project_uuid=parent_project_uuid,
parent_node_id=parent_node_id,
)

assert new_project # nosec
assert new_project.uuid == pre_job.id # nosec

# for consistency, it rebuild job
job = create_job_from_project(
solver_or_program=solver_or_program, project=new_project, url_for=url_for
)
assert job.id == pre_job.id # nosec
assert job.name == pre_job.name # nosec
assert job.name == Job.compose_resource_name(
parent_name=solver_or_program.resource_name,
job_id=job.id,
)
return job, new_project
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Annotated

from fastapi import Depends
from models_library.basic_types import VersionStr
from models_library.services_enums import ServiceType

from .models.schemas.programs import Program, ProgramKeyId
from .services_rpc.catalog import CatalogService


class ProgramService:
_catalog_service: CatalogService

def __init__(self, _catalog_service: Annotated[CatalogService, Depends()]):
self._catalog_service = _catalog_service

async def get_program(
self,
*,
user_id: int,
name: ProgramKeyId,
version: VersionStr,
product_name: str,
) -> Program:
service = await self._catalog_service.get(
user_id=user_id,
name=name,
version=version,
product_name=product_name,
)
assert service.service_type == ServiceType.DYNAMIC # nosec

return Program.create_from_service(service)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Annotated

from common_library.pagination_tools import iter_pagination_params
from fastapi import Depends
from models_library.basic_types import VersionStr
from models_library.products import ProductName
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
from models_library.services_enums import ServiceType
from models_library.services_history import ServiceRelease
from models_library.users import UserID
from packaging.version import Version

from .models.schemas.solvers import Solver, SolverKeyId
from .services_rpc.catalog import CatalogService

DEFAULT_PAGINATION_LIMIT = MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE - 1


class SolverService:
_catalog_service: CatalogService

def __init__(self, catalog_service: Annotated[CatalogService, Depends()]):
self._catalog_service = catalog_service

async def get_solver(
self,
*,
user_id: UserID,
name: SolverKeyId,
version: VersionStr,
product_name: ProductName,
) -> Solver:
service = await self._catalog_service.get(
user_id=user_id,
name=name,
version=version,
product_name=product_name,
)
assert ( # nosec
service.service_type == ServiceType.COMPUTATIONAL
), "Expected by SolverName regex"

return Solver.create_from_service(service)

async def get_latest_release(
self,
*,
user_id: int,
solver_key: SolverKeyId,
product_name: str,
) -> Solver:
service_releases: list[ServiceRelease] = []
for page_params in iter_pagination_params(limit=DEFAULT_PAGINATION_LIMIT):
releases, page_meta = await self._catalog_service.list_release_history(
user_id=user_id,
service_key=solver_key,
product_name=product_name,
offset=page_params.offset,
limit=page_params.limit,
)
page_params.total_number_of_items = page_meta.total
service_releases.extend(releases)

release = sorted(service_releases, key=lambda s: Version(s.version))[-1]
service = await self._catalog_service.get(
user_id=user_id,
name=solver_key,
version=release.version,
product_name=product_name,
)

return Solver.create_from_service(service)
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,41 @@

# removed on inputs/outputs in routes
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT: Final[str] = "Removed in *version {}*: {}\n"

FMSG_DEPRECATED_ROUTE_NOTICE: Final[str] = (
"🚨 **Deprecated**: This endpoint is deprecated and will be removed in a future release.\n"
"Please use `{}` instead.\n\n"
)


def create_route_description(
*,
base: str = "",
deprecated: bool = False,
alternative: str | None = None, # alternative
changelog: list[str] | None = None
) -> str:
"""
Builds a consistent route description with optional deprecation and changelog information.

Args:
base (str): Main route description.
deprecated (tuple): (retirement_date, alternative_route) if deprecated.
changelog (List[str]): List of formatted changelog strings.

Returns:
str: Final description string.
"""
parts = []

if deprecated:
assert alternative, "If deprecated, alternative must be provided" # nosec
parts.append(FMSG_DEPRECATED_ROUTE_NOTICE.format(alternative))

if base:
parts.append(base)

if changelog:
parts.append("\n".join(changelog))

return "\n\n".join(parts)
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
get_upload_links_from_s3,
)
from simcore_sdk.node_ports_common.filemanager import upload_path as storage_upload_path
from simcore_service_api_server.api.routes._constants import (
FMSG_CHANGELOG_ADDED_IN_VERSION,
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT,
create_route_description,
)
from starlette.datastructures import URL
from starlette.responses import RedirectResponse

Expand Down Expand Up @@ -132,7 +137,23 @@ async def _create_domain_file(
return file


@router.get("", response_model=list[OutputFile], responses=_FILE_STATUS_CODES)
@router.get(
"",
response_model=list[OutputFile],
responses=_FILE_STATUS_CODES,
description=create_route_description(
base="Lists all files stored in the system",
deprecated=True,
alternative="GET /v0/files/page",
changelog=[
FMSG_CHANGELOG_ADDED_IN_VERSION.format("0.5", ""),
FMSG_CHANGELOG_REMOVED_IN_VERSION_FORMAT.format(
"0.7",
"This endpoint is deprecated and will be removed in a future version",
),
],
),
)
async def list_files(
storage_client: Annotated[StorageApi, Depends(get_api_client(StorageApi))],
user_id: Annotated[int, Depends(get_current_user_id)],
Expand Down
Loading
Loading