Skip to content

Commit 36498e9

Browse files
committed
Add function endpoints to update title and description
1 parent 9c7927c commit 36498e9

File tree

8 files changed

+294
-0
lines changed

8 files changed

+294
-0
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,38 @@ async def list_function_job_collections(
170170
)
171171

172172

173+
@log_decorator(_logger, level=logging.DEBUG)
174+
async def update_function_title(
175+
rabbitmq_rpc_client: RabbitMQRPCClient,
176+
*,
177+
function_id: FunctionID,
178+
title: str,
179+
) -> RegisteredFunction:
180+
result = await rabbitmq_rpc_client.request(
181+
WEBSERVER_RPC_NAMESPACE,
182+
TypeAdapter(RPCMethodName).validate_python("update_function_title"),
183+
function_id=function_id,
184+
title=title,
185+
)
186+
return TypeAdapter(RegisteredFunction).validate_python(result)
187+
188+
189+
@log_decorator(_logger, level=logging.DEBUG)
190+
async def update_function_description(
191+
rabbitmq_rpc_client: RabbitMQRPCClient,
192+
*,
193+
function_id: FunctionID,
194+
description: str,
195+
) -> RegisteredFunction:
196+
result = await rabbitmq_rpc_client.request(
197+
WEBSERVER_RPC_NAMESPACE,
198+
TypeAdapter(RPCMethodName).validate_python("update_function_description"),
199+
function_id=function_id,
200+
description=description,
201+
)
202+
return TypeAdapter(RegisteredFunction).validate_python(result)
203+
204+
173205
@log_decorator(_logger, level=logging.DEBUG)
174206
async def run_function(
175207
rabbitmq_rpc_client: RabbitMQRPCClient,

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,46 @@ async def list_function_job_collections(
149149
)
150150

151151

152+
@function_router.patch(
153+
"/{function_id:uuid}/title",
154+
response_model=RegisteredFunction,
155+
responses={**_COMMON_FUNCTION_ERROR_RESPONSES},
156+
description="Update function",
157+
)
158+
async def update_function_title(
159+
function_id: FunctionID,
160+
wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)],
161+
title: str,
162+
) -> RegisteredFunction:
163+
returned_function = await wb_api_rpc.update_function_title(
164+
function_id=function_id, title=title
165+
)
166+
assert (
167+
returned_function.title == title
168+
), f"Function title was not updated. Expected {title} but got {returned_function.title}" # nosec
169+
return returned_function
170+
171+
172+
@function_router.patch(
173+
"/{function_id:uuid}/description",
174+
response_model=RegisteredFunction,
175+
responses={**_COMMON_FUNCTION_ERROR_RESPONSES},
176+
description="Update function",
177+
)
178+
async def update_function_description(
179+
function_id: FunctionID,
180+
wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)],
181+
description: str,
182+
) -> RegisteredFunction:
183+
returned_function = await wb_api_rpc.update_function_description(
184+
function_id=function_id, description=description
185+
)
186+
assert (
187+
returned_function.description == description
188+
), f"Function description was not updated. Expected {description} but got {returned_function.description}" # nosec
189+
return returned_function
190+
191+
152192
def _join_inputs(
153193
default_inputs: FunctionInputs | None,
154194
function_inputs: FunctionInputs | None,

services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,24 @@ async def get_function_job(
328328
function_job_id=function_job_id,
329329
)
330330

331+
async def update_function_title(
332+
self, *, function_id: FunctionID, title: str
333+
) -> RegisteredFunction:
334+
return await functions_rpc_interface.update_function_title(
335+
self._client,
336+
function_id=function_id,
337+
title=title,
338+
)
339+
340+
async def update_function_description(
341+
self, *, function_id: FunctionID, description: str
342+
) -> RegisteredFunction:
343+
return await functions_rpc_interface.update_function_description(
344+
self._client,
345+
function_id=function_id,
346+
description=description,
347+
)
348+
331349
async def delete_function_job(self, *, function_job_id: FunctionJobID) -> None:
332350
return await functions_rpc_interface.delete_function_job(
333351
self._client,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict
3+
from pytest_simcore.helpers.typing_env import EnvVarsDict
4+
5+
6+
@pytest.fixture
7+
def app_environment(
8+
app_environment: EnvVarsDict,
9+
monkeypatch: pytest.MonkeyPatch,
10+
):
11+
return setenvs_from_dict(
12+
monkeypatch,
13+
{
14+
**app_environment, # WARNING: AFTER env_devel_dict because HOST are set to 127.0.0.1 in here
15+
"WEBSERVER_DEV_FEATURES_ENABLED": "1",
16+
"WEBSERVER_FUNCTIONS": "1",
17+
},
18+
)

services/api-server/tests/unit/api_functions/test_api_routers_functions.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ async def delete_function_job_collection(
228228
status_code=404, detail="Function job collection not found"
229229
)
230230

231+
async def update_function_title(
232+
self, function_id: str, title: str
233+
) -> RegisteredFunction:
234+
# Mimic updating the title of a function
235+
if function_id not in self._functions:
236+
raise HTTPException(status_code=404, detail="Function not found")
237+
self._functions[function_id].title = title
238+
return self._functions[function_id]
239+
240+
async def update_function_description(
241+
self, function_id: str, description: str
242+
) -> RegisteredFunction:
243+
# Mimic updating the description of a function
244+
if function_id not in self._functions:
245+
raise HTTPException(status_code=404, detail="Function not found")
246+
self._functions[function_id].description = description
247+
return self._functions[function_id]
248+
231249

232250
def test_register_function(api_app) -> None:
233251
client = TestClient(api_app)
@@ -335,6 +353,62 @@ def test_list_functions(api_app: FastAPI) -> None:
335353
assert data[0]["title"] == sample_function["title"]
336354

337355

356+
def test_update_function_title(api_app: FastAPI) -> None:
357+
client = TestClient(api_app)
358+
project_id = str(uuid4())
359+
# Register a sample function
360+
sample_function = {
361+
"uid": None,
362+
"title": "example_function",
363+
"function_class": "project",
364+
"project_id": project_id,
365+
"description": "An example function",
366+
"input_schema": JSONFunctionInputSchema().model_dump(),
367+
"output_schema": JSONFunctionOutputSchema().model_dump(),
368+
"default_inputs": None,
369+
}
370+
post_response = client.post("/functions", json=sample_function)
371+
assert post_response.status_code == 200
372+
data = post_response.json()
373+
function_id = data["uid"]
374+
375+
# Update the function title
376+
updated_title = {"title": "updated_example_function"}
377+
response = client.patch(f"/functions/{function_id}/title", params=updated_title)
378+
assert response.status_code == 200
379+
data = response.json()
380+
assert data["title"] == updated_title["title"]
381+
382+
383+
def test_update_function_description(api_app: FastAPI) -> None:
384+
client = TestClient(api_app)
385+
project_id = str(uuid4())
386+
# Register a sample function
387+
sample_function = {
388+
"uid": None,
389+
"title": "example_function",
390+
"function_class": "project",
391+
"project_id": project_id,
392+
"description": "An example function",
393+
"input_schema": JSONFunctionInputSchema().model_dump(),
394+
"output_schema": JSONFunctionOutputSchema().model_dump(),
395+
"default_inputs": None,
396+
}
397+
post_response = client.post("/functions", json=sample_function)
398+
assert post_response.status_code == 200
399+
data = post_response.json()
400+
function_id = data["uid"]
401+
402+
# Update the function description
403+
updated_description = {"description": "updated_example_function"}
404+
response = client.patch(
405+
f"/functions/{function_id}/description", params=updated_description
406+
)
407+
assert response.status_code == 200
408+
data = response.json()
409+
assert data["description"] == updated_description["description"]
410+
411+
338412
def test_get_function_input_schema(api_app: FastAPI) -> None:
339413
client = TestClient(api_app)
340414
project_id = str(uuid4())

services/web/server/src/simcore_service_webserver/functions/_functions_controller_rpc.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,32 @@ async def delete_function_job_collection(
238238
)
239239

240240

241+
@router.expose()
242+
async def update_function_title(
243+
app: web.Application, *, function_id: FunctionID, title: str
244+
) -> RegisteredFunction:
245+
assert app
246+
updated_function = await _functions_repository.update_function_title(
247+
app=app,
248+
function_id=function_id,
249+
title=title,
250+
)
251+
return _decode_function(updated_function)
252+
253+
254+
@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,))
255+
async def update_function_description(
256+
app: web.Application, *, function_id: FunctionID, description: str
257+
) -> RegisteredFunction:
258+
assert app
259+
updated_function = await _functions_repository.update_function_description(
260+
app=app,
261+
function_id=function_id,
262+
description=description,
263+
)
264+
return _decode_function(updated_function)
265+
266+
241267
@router.expose()
242268
async def find_cached_function_job(
243269
app: web.Application, *, function_id: FunctionID, inputs: FunctionInputs

services/web/server/src/simcore_service_webserver/functions/_functions_repository.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,42 @@ async def delete_function(
331331
)
332332

333333

334+
async def update_function_title(
335+
app: web.Application, *, function_id: FunctionID, title: str
336+
) -> RegisteredFunctionDB:
337+
async with transaction_context(get_asyncpg_engine(app)) as conn:
338+
result = await conn.stream(
339+
functions_table.update()
340+
.where(functions_table.c.uuid == function_id)
341+
.values(title=title)
342+
.returning(*_FUNCTIONS_TABLE_COLS)
343+
)
344+
row = await result.first()
345+
346+
if row is None:
347+
raise FunctionIDNotFoundError(function_id=function_id)
348+
349+
return RegisteredFunctionDB.model_validate(dict(row))
350+
351+
352+
async def update_function_description(
353+
app: web.Application, *, function_id: FunctionID, description: str
354+
) -> RegisteredFunctionDB:
355+
async with transaction_context(get_asyncpg_engine(app)) as conn:
356+
result = await conn.stream(
357+
functions_table.update()
358+
.where(functions_table.c.uuid == function_id)
359+
.values(description=description)
360+
.returning(*_FUNCTIONS_TABLE_COLS)
361+
)
362+
row = await result.first()
363+
364+
if row is None:
365+
raise FunctionIDNotFoundError(function_id=function_id)
366+
367+
return RegisteredFunctionDB.model_validate(dict(row))
368+
369+
334370
async def get_function_job(
335371
app: web.Application,
336372
connection: AsyncConnection | None = None,

services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,56 @@ async def test_list_functions_with_pagination(
240240
assert page_info.total == TOTAL_FUNCTIONS
241241

242242

243+
async def test_update_function_title(
244+
client: TestClient, rpc_client: RabbitMQRPCClient, mock_function: ProjectFunction
245+
):
246+
assert client.app
247+
# Register the function first
248+
registered_function = await functions_rpc.register_function(
249+
rabbitmq_rpc_client=rpc_client, function=mock_function
250+
)
251+
assert registered_function.uid is not None
252+
253+
# Update the function's title
254+
updated_title = "Updated Function Title"
255+
registered_function.title = updated_title
256+
updated_function = await functions_rpc.update_function_title(
257+
rabbitmq_rpc_client=rpc_client,
258+
function_id=registered_function.uid,
259+
title=updated_title,
260+
)
261+
262+
assert isinstance(updated_function, ProjectFunction)
263+
assert updated_function.uid == registered_function.uid
264+
# Assert the updated function's title matches the new title
265+
assert updated_function.title == updated_title
266+
267+
268+
async def test_update_function_description(
269+
client: TestClient, rpc_client: RabbitMQRPCClient, mock_function: ProjectFunction
270+
):
271+
assert client.app
272+
# Register the function first
273+
registered_function = await functions_rpc.register_function(
274+
rabbitmq_rpc_client=rpc_client, function=mock_function
275+
)
276+
assert registered_function.uid is not None
277+
278+
# Update the function's description
279+
updated_description = "Updated Function Description"
280+
registered_function.description = updated_description
281+
updated_function = await functions_rpc.update_function_description(
282+
rabbitmq_rpc_client=rpc_client,
283+
function_id=registered_function.uid,
284+
description=updated_description,
285+
)
286+
287+
assert isinstance(updated_function, ProjectFunction)
288+
assert updated_function.uid == registered_function.uid
289+
# Assert the updated function's description matches the new description
290+
assert updated_function.description == updated_description
291+
292+
243293
async def test_get_function_input_schema(
244294
client: TestClient, rpc_client: RabbitMQRPCClient, mock_function: ProjectFunction
245295
):

0 commit comments

Comments
 (0)