Skip to content

Commit 4c549d5

Browse files
feat: enrich solver functions response
1 parent b1961b8 commit 4c549d5

File tree

9 files changed

+281
-95
lines changed

9 files changed

+281
-95
lines changed

services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py

Lines changed: 156 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
from typing import Any
2+
13
from aiohttp import web
24
from models_library.api_schemas_webserver.functions import (
35
Function,
46
FunctionToRegister,
57
RegisteredFunction,
68
RegisteredFunctionGet,
79
RegisteredFunctionUpdate,
8-
RegisteredProjectFunctionGet,
910
)
1011
from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet
11-
from models_library.functions import FunctionClass, RegisteredProjectFunction
12+
from models_library.functions import (
13+
FunctionClass,
14+
FunctionID,
15+
RegisteredProjectFunction,
16+
RegisteredSolverFunction,
17+
)
1218
from models_library.products import ProductName
19+
from models_library.projects import ProjectID
1320
from models_library.rest_pagination import Page
1421
from models_library.rest_pagination_utils import paginate_data
22+
from models_library.services_types import ServiceKey, ServiceVersion
1523
from models_library.users import UserID
1624
from pydantic import TypeAdapter
1725
from servicelib.aiohttp import status
@@ -29,6 +37,8 @@
2937
from ...security.decorators import permission_required
3038
from ...utils_aiohttp import create_json_response_from_page, envelope_json_response
3139
from .. import _functions_service
40+
from .._services_metadata import service as _services_metadata_service
41+
from .._services_metadata.service import ServiceMetadata
3242
from ._functions_rest_exceptions import handle_rest_requests_exceptions
3343
from ._functions_rest_schemas import (
3444
FunctionGetQueryParams,
@@ -39,30 +49,76 @@
3949
routes = web.RouteTableDef()
4050

4151

42-
async def _add_extras_to_project_function(
43-
function: RegisteredProjectFunction,
52+
async def _build_function_access_rights_dict(
4453
app: web.Application,
4554
user_id: UserID,
4655
product_name: ProductName,
47-
) -> dict:
48-
assert isinstance(function, RegisteredProjectFunction) # nosec
56+
function_id: FunctionID,
57+
) -> dict[str, Any]:
58+
access_rights = await _functions_service.get_function_user_permissions(
59+
app=app,
60+
user_id=user_id,
61+
product_name=product_name,
62+
function_id=function_id,
63+
)
64+
65+
return {
66+
"access_rights": access_rights.model_dump(),
67+
}
4968

69+
70+
async def _build_project_function_extras_dict(
71+
app: web.Application,
72+
*,
73+
user_id: UserID,
74+
function: RegisteredProjectFunction,
75+
) -> dict[str, Any]:
5076
project_dict = await _projects_service.get_project_for_user(
5177
app=app,
5278
project_uuid=f"{function.project_id}",
5379
user_id=user_id,
5480
)
5581

56-
function_with_extras = function.model_dump(mode="json") | {
57-
"access_rights": await _functions_service.get_function_user_permissions(
58-
app,
82+
return {
83+
"thumbnail": project_dict.get("thumbnail", None),
84+
}
85+
86+
87+
async def _build_function_extras(
88+
app: web.Application, user_id: UserID, *, function: RegisteredFunction
89+
) -> dict[str, Any]:
90+
extras: dict[str, Any] = {}
91+
if function.function_class == FunctionClass.PROJECT:
92+
assert isinstance(function, RegisteredProjectFunction)
93+
extras |= await _build_project_function_extras_dict(
94+
function=function,
95+
app=app,
5996
user_id=user_id,
60-
product_name=product_name,
61-
function_id=function.uid,
97+
)
98+
elif function.function_class == FunctionClass.SOLVER:
99+
assert isinstance(function, RegisteredSolverFunction)
100+
extras |= await _build_solver_function_extras_dict(
101+
app,
102+
function=function,
103+
)
104+
return extras
105+
106+
107+
async def _build_solver_function_extras_dict(
108+
app: web.Application,
109+
*,
110+
function: RegisteredSolverFunction,
111+
) -> dict[str, Any]:
112+
services_metadata = await _services_metadata_service.get_service_metadata(
113+
app,
114+
key=function.solver_key,
115+
version=function.solver_version,
116+
)
117+
return {
118+
"thumbnail": (
119+
f"{services_metadata.thumbnail}" if services_metadata.thumbnail else None
62120
),
63-
"thumbnail": project_dict.get("thumbnail", None),
64121
}
65-
return function_with_extras
66122

67123

68124
@routes.post(f"/{VTAG}/functions", name="register_function")
@@ -117,55 +173,76 @@ async def list_functions(request: web.Request) -> web.Response:
117173
)
118174

119175
chunk: list[RegisteredFunctionGet] = []
120-
projects_map: dict[str, ProjectDBGet | None] = (
176+
177+
projects_cache: dict[ProjectID, ProjectDBGet] = {}
178+
service_metadata_cache: dict[tuple[ServiceKey, ServiceVersion], ServiceMetadata] = (
121179
{}
122-
) # ProjectDBGet has to be renamed at some point!
180+
)
123181

124182
if query_params.include_extras:
125-
project_ids = []
126-
for function in functions:
127-
if function.function_class == FunctionClass.PROJECT:
128-
assert isinstance(function, RegisteredProjectFunction)
129-
project_ids.append(function.project_id)
130-
131-
projects_map = {
132-
f"{p.uuid}": p
133-
for p in await _projects_service.batch_get_projects(
183+
if any(
184+
function.function_class == FunctionClass.PROJECT for function in functions
185+
):
186+
project_uuids = [
187+
function.project_id
188+
for function in functions
189+
if function.function_class == FunctionClass.PROJECT
190+
]
191+
projects_cache = await _projects_service.batch_get_projects(
134192
request.app,
135-
project_uuids=project_ids,
193+
project_uuids=project_uuids,
136194
)
137-
}
138-
139-
for function in functions:
140-
if (
141-
query_params.include_extras
142-
and function.function_class == FunctionClass.PROJECT
195+
if any(
196+
function.function_class == FunctionClass.SOLVER for function in functions
143197
):
144-
assert isinstance(function, RegisteredProjectFunction) # nosec
145-
if project := projects_map.get(f"{function.project_id}"):
146-
chunk.append(
147-
TypeAdapter(RegisteredProjectFunctionGet).validate_python(
148-
function.model_dump(mode="json")
149-
| {
150-
"access_rights": await _functions_service.get_function_user_permissions(
151-
request.app,
152-
user_id=req_ctx.user_id,
153-
product_name=req_ctx.product_name,
154-
function_id=function.uid,
155-
),
156-
"thumbnail": (
157-
f"{project.thumbnail}" if project.thumbnail else None
158-
),
159-
}
160-
)
161-
)
162-
else:
163-
chunk.append(
164-
TypeAdapter(RegisteredFunctionGet).validate_python(
165-
function.model_dump(mode="json")
198+
service_keys_and_versions = [
199+
(function.solver_key, function.solver_version)
200+
for function in functions
201+
if function.function_class == FunctionClass.SOLVER
202+
]
203+
service_metadata_cache = (
204+
await _services_metadata_service.batch_get_service_metadata(
205+
app=request.app, keys_and_versions=service_keys_and_versions
166206
)
167207
)
168208

209+
for function in functions:
210+
access_rights = await _build_function_access_rights_dict(
211+
request.app,
212+
user_id=req_ctx.user_id,
213+
product_name=req_ctx.product_name,
214+
function_id=function.uid,
215+
)
216+
217+
extras: dict[str, Any] = {}
218+
if query_params.include_extras:
219+
if function.function_class == FunctionClass.PROJECT:
220+
assert isinstance(function, RegisteredProjectFunction) # nosec
221+
if project := projects_cache.get(function.project_id):
222+
extras = {
223+
"thumbnail": (
224+
f"{project.thumbnail}" if project.thumbnail else None
225+
),
226+
}
227+
elif function.function_class == FunctionClass.SOLVER:
228+
assert isinstance(function, RegisteredSolverFunction)
229+
if service_metadata := service_metadata_cache.get(
230+
(function.solver_key, function.solver_version)
231+
):
232+
extras = {
233+
"thumbnail": (
234+
f"{service_metadata.thumbnail}"
235+
if service_metadata.thumbnail
236+
else None
237+
),
238+
}
239+
240+
chunk.append(
241+
TypeAdapter(RegisteredFunctionGet).validate_python(
242+
function.model_dump() | access_rights | extras
243+
)
244+
)
245+
169246
page = Page[RegisteredFunctionGet].model_validate(
170247
paginate_data(
171248
chunk=chunk,
@@ -194,33 +271,29 @@ async def get_function(request: web.Request) -> web.Response:
194271
)
195272

196273
req_ctx = AuthenticatedRequestContext.model_validate(request)
197-
registered_function: RegisteredFunction = await _functions_service.get_function(
274+
function = await _functions_service.get_function(
198275
app=request.app,
199276
function_id=function_id,
200277
user_id=req_ctx.user_id,
201278
product_name=req_ctx.product_name,
202279
)
203280

204-
if (
205-
query_params.include_extras
206-
and registered_function.function_class == FunctionClass.PROJECT
207-
):
208-
function_with_extras = await _add_extras_to_project_function(
209-
function=registered_function,
210-
app=request.app,
211-
user_id=req_ctx.user_id,
212-
product_name=req_ctx.product_name,
213-
)
281+
access_rights = await _build_function_access_rights_dict(
282+
request.app,
283+
user_id=req_ctx.user_id,
284+
product_name=req_ctx.product_name,
285+
function_id=function_id,
286+
)
214287

215-
return envelope_json_response(
216-
TypeAdapter(RegisteredProjectFunctionGet).validate_python(
217-
function_with_extras
218-
)
219-
)
288+
extras = (
289+
await _build_function_extras(request.app, req_ctx.user_id, function=function)
290+
if query_params.include_extras
291+
else {}
292+
)
220293

221294
return envelope_json_response(
222295
TypeAdapter(RegisteredFunctionGet).validate_python(
223-
registered_function.model_dump(mode="json")
296+
function.model_dump() | access_rights | extras
224297
)
225298
)
226299

@@ -245,34 +318,30 @@ async def update_function(request: web.Request) -> web.Response:
245318
)
246319
req_ctx = AuthenticatedRequestContext.model_validate(request)
247320

248-
updated_function = await _functions_service.update_function(
321+
function = await _functions_service.update_function(
249322
request.app,
250323
user_id=req_ctx.user_id,
251324
product_name=req_ctx.product_name,
252325
function_id=function_id,
253326
function=function_update,
254327
)
255328

256-
if (
257-
query_params.include_extras
258-
and updated_function.function_class == FunctionClass.PROJECT
259-
):
260-
function_with_extras = await _add_extras_to_project_function(
261-
function=updated_function,
262-
app=request.app,
263-
user_id=req_ctx.user_id,
264-
product_name=req_ctx.product_name,
265-
)
329+
access_rights = await _build_function_access_rights_dict(
330+
request.app,
331+
user_id=req_ctx.user_id,
332+
product_name=req_ctx.product_name,
333+
function_id=function_id,
334+
)
266335

267-
return envelope_json_response(
268-
TypeAdapter(RegisteredProjectFunctionGet).validate_python(
269-
function_with_extras
270-
)
271-
)
336+
extras = (
337+
await _build_function_extras(request.app, req_ctx.user_id, function=function)
338+
if query_params.include_extras
339+
else {}
340+
)
272341

273342
return envelope_json_response(
274343
TypeAdapter(RegisteredFunctionGet).validate_python(
275-
updated_function.model_dump(mode="json")
344+
function.model_dump() | access_rights | extras
276345
)
277346
)
278347

services/web/server/src/simcore_service_webserver/functions/_services_metadata/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from common_library.errors_classes import OsparcErrorMixin
2+
3+
4+
class ServiceMetadataNotFoundError(OsparcErrorMixin, Exception):
5+
msg_template = "Service metadata for key {key} and version {version} not found"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from models_library.services_types import ServiceKey, ServiceVersion
2+
from pydantic import BaseModel, HttpUrl
3+
4+
5+
class ServiceMetadata(BaseModel):
6+
key: ServiceKey
7+
version: ServiceVersion
8+
thumbnail: HttpUrl | None

0 commit comments

Comments
 (0)