Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 Down Expand Up @@ -109,6 +110,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 +126,69 @@ async def test_function_workflow(
retrieved_function = TypeAdapter(RegisteredFunctionGet).validate_python(data)
assert retrieved_function.uid == returned_function.uid

# List existing functions (default)
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) == 2
assert returned_function_uid in [f.uid for f in retrieved_functions]
assert (
retrieved_functions[1].uid == returned_function_uid
) # ordered by modified_at by default

# List existing functions (ordered by created_at ascending)
url = client.app.router["list_functions"].url_for()
response = await client.get(
url,
params={"order_by": json_dumps({"field": "created_at", "direction": "asc"})},
)
data, error = await assert_status(response, expected_list)
if not error:
retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(
data
)
assert len(retrieved_functions) == 2
assert returned_function_uid in [f.uid for f in retrieved_functions]
assert retrieved_functions[0].uid == returned_function_uid

# List existing functions (searching for not existing)
url = client.app.router["list_functions"].url_for()
response = await client.get(
url, params={"search": "you_can_not_find_me_because_I_do_not_exist"}
)
data, error = await assert_status(response, expected_list)
if not error:
retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(
data
)
assert len(retrieved_functions) == 0

# List existing functions (searching for duplicate)
url = client.app.router["list_functions"].url_for()
response = await client.get(url, params={"search": "duplicate"})
data, error = await assert_status(response, expected_list)
if not error:
retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(
data
)
assert len(retrieved_functions) == 1

# List existing functions (searching by title)
url = client.app.router["list_functions"].url_for()
response = await client.get(
url, params={"filters": json_dumps({"search_by_title": "duplicate"})}
)
data, error = await assert_status(response, expected_list)
if not error:
retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python(
data
)
assert len(retrieved_functions) == 1

# 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 +226,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