Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.74.0
0.75.0
2 changes: 1 addition & 1 deletion services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.74.0
current_version = 0.75.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: simcore-service-webserver
description: Main service with an interface (http-API & websockets) to the web front-end
version: 0.74.0
version: 0.75.0
servers:
- url: ''
description: webserver
Expand Down Expand Up @@ -3670,6 +3670,33 @@ paths:
type: boolean
default: false
title: Include Extras
- name: search
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Search
- name: filters
in: query
required: false
schema:
anyOf:
- type: string
contentMediaType: application/json
contentSchema: {}
- type: 'null'
title: Filters
- name: order_by
in: query
required: false
schema:
type: string
contentMediaType: application/json
contentSchema: {}
default: '{"field":"modified","direction":"desc"}'
title: Order By
- name: limit
in: query
required: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
)
from models_library.groups import GroupID
from models_library.products import ProductName
from models_library.rest_ordering import OrderBy
from models_library.rest_pagination import Page
from models_library.rest_pagination_utils import paginate_data
from models_library.users import UserID
Expand All @@ -43,6 +44,7 @@
from .._services_metadata.proxy import ServiceMetadata
from ._functions_rest_exceptions import handle_rest_requests_exceptions
from ._functions_rest_schemas import (
FunctionFilters,
FunctionGetQueryParams,
FunctionGroupPathParams,
FunctionPathParams,
Expand Down Expand Up @@ -172,13 +174,21 @@ async def list_functions(request: web.Request) -> web.Response:
FunctionsListQueryParams, request
)

if not query_params.filters:
query_params.filters = FunctionFilters()

assert query_params.filters # nosec

req_ctx = AuthenticatedRequestContext.model_validate(request)
functions, page_meta_info = await _functions_service.list_functions(
request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
pagination_limit=query_params.limit,
pagination_offset=query_params.offset,
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
search_by_function_title=query_params.filters.search_by_title,
search_by_multi_columns=query_params.search,
)

chunk: list[RegisteredFunctionGet] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from typing import Annotated

from models_library.basic_types import IDStr
from models_library.functions import FunctionID
from models_library.groups import GroupID
from models_library.rest_base import RequestParameters
from models_library.rest_filters import Filters, FiltersQueryParameters
from models_library.rest_ordering import (
OrderBy,
OrderDirection,
create_ordering_query_model_class,
)
from models_library.rest_pagination import PageQueryParameters
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, Field

from ...models import AuthenticatedRequestContext

Expand All @@ -17,14 +27,56 @@ class FunctionGroupPathParams(FunctionPathParams):
group_id: GroupID


class _FunctionQueryParams(BaseModel):
class FunctionQueryParams(BaseModel):
include_extras: bool = False


class FunctionGetQueryParams(_FunctionQueryParams): ...
class FunctionGetQueryParams(FunctionQueryParams): ...


class FunctionFilters(Filters):
search_by_title: Annotated[
str | None,
Field(
description="A search query to filter functions by their title. This field performs a case-insensitive partial match against the function title field.",
),
] = None


FunctionListOrderQueryParams: type[RequestParameters] = (
create_ordering_query_model_class(
ordering_fields={
"created_at",
"modified_at",
"name",
},
default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
ordering_fields_api_to_column_map={
"created_at": "created",
"modified_at": "modified",
},
)
)


class FunctionsListExtraQueryParams(RequestParameters):
search: Annotated[
str | None,
Field(
description="Multi column full text search",
max_length=100,
examples=["My Function"],
),
] = None


class FunctionsListQueryParams(PageQueryParameters, _FunctionQueryParams): ...
class FunctionsListQueryParams(
PageQueryParameters,
FunctionListOrderQueryParams, # type: ignore[misc, valid-type]
FiltersQueryParameters[FunctionFilters],
FunctionsListExtraQueryParams,
FunctionQueryParams,
): ...


__all__: tuple[str, ...] = ("AuthenticatedRequestContext",)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import pytest
from aiohttp.test_utils import TestClient
from common_library.json_serialization import json_dumps
from models_library.api_schemas_webserver.functions import (
FunctionClass,
JSONFunctionInputSchema,
Expand All @@ -27,6 +28,37 @@
pytest_simcore_core_services_selection = ["rabbit"]


async def _list_functions_and_validate(
client: TestClient,
expected_status: HTTPStatus,
expected_count: int | None = None,
params: dict[str, Any] | None = None,
expected_uid_in_results: str | None = None,
expected_uid_at_index: tuple[str, int] | None = None,
) -> list[RegisteredFunctionGet] | None:
"""Helper function to list functions and validate the response."""
url = client.app.router["list_functions"].url_for()
response = await client.get(url, params=params or {})
data, error = await assert_status(response, expected_status)

if error:
return None

retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(data)

if expected_count is not None:
assert len(retrieved_functions) == expected_count

if expected_uid_in_results is not None:
assert expected_uid_in_results in [f"{f.uid}" for f in retrieved_functions]

if expected_uid_at_index is not None:
expected_uid, index = expected_uid_at_index
assert f"{retrieved_functions[index].uid}" == expected_uid

return retrieved_functions


@pytest.fixture(params=[FunctionClass.PROJECT, FunctionClass.SOLVER])
def mocked_function(request) -> dict[str, Any]:
function_dict = {
Expand Down Expand Up @@ -109,6 +141,12 @@ async def test_function_workflow(
assert returned_function.uid is not None
returned_function_uid = returned_function.uid

# Register a new function (duplicate)
url = client.app.router["register_function"].url_for()
mocked_function.update(title=mocked_function["title"] + " (duplicate)")
response = await client.post(url, json=mocked_function)
await assert_status(response, expected_status_code=expected_register)

# Get the registered function
url = client.app.router["get_function"].url_for(
function_id=f"{returned_function_uid}"
Expand All @@ -119,6 +157,52 @@ async def test_function_workflow(
retrieved_function = TypeAdapter(RegisteredFunctionGet).validate_python(data)
assert retrieved_function.uid == returned_function.uid

# List existing functions (default)
await _list_functions_and_validate(
client,
expected_list,
expected_count=2,
expected_uid_in_results=f"{returned_function_uid}",
expected_uid_at_index=(
f"{returned_function_uid}",
1,
), # ordered by modified_at by default
)

# List existing functions (ordered by created_at ascending)
await _list_functions_and_validate(
client,
expected_list,
expected_count=2,
params={"order_by": json_dumps({"field": "created_at", "direction": "asc"})},
expected_uid_in_results=f"{returned_function_uid}",
expected_uid_at_index=(f"{returned_function_uid}", 0),
)

# List existing functions (searching for not existing)
await _list_functions_and_validate(
client,
expected_list,
expected_count=0,
params={"search": "you_can_not_find_me_because_I_do_not_exist"},
)

# List existing functions (searching for duplicate)
await _list_functions_and_validate(
client,
expected_list,
expected_count=1,
params={"search": "duplicate"},
)

# List existing functions (searching by title)
await _list_functions_and_validate(
client,
expected_list,
expected_count=1,
params={"filters": json_dumps({"search_by_title": "duplicate"})},
)

# Set group permissions for other user
new_group_id = other_logged_user["primary_gid"]
new_group_access_rights = {"read": True, "write": True, "execute": False}
Expand Down Expand Up @@ -156,17 +240,6 @@ async def test_function_workflow(
new_group_id: new_group_access_rights
}

# List existing functions
url = client.app.router["list_functions"].url_for()
response = await client.get(url)
data, error = await assert_status(response, expected_list)
if not error:
retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(
data
)
assert len(retrieved_functions) == 1
assert retrieved_functions[0].uid == returned_function_uid

# Update existing function
new_title = "Test Function (edited)"
new_description = "A test function (edited)"
Expand Down
Loading