Skip to content
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

🎨 web-server api: ordering parameters and simplified openapi specs for complex query parameters #6737

Merged
merged 42 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a749f7c
new rest ordering
pcrespov Nov 14, 2024
fe15ff2
moving base rest models
pcrespov Nov 14, 2024
6e714bb
fixes imports
pcrespov Nov 14, 2024
2c988e0
new validator
pcrespov Nov 14, 2024
e738765
updates
pcrespov Nov 14, 2024
7a59727
test
pcrespov Nov 14, 2024
6919590
minor
pcrespov Nov 14, 2024
ada2b69
projects
pcrespov Nov 14, 2024
a78b254
ordering
pcrespov Nov 14, 2024
8633d49
aline
pcrespov Nov 14, 2024
725dbe5
resource-usage
pcrespov Nov 14, 2024
8b431fe
cleanup projects
pcrespov Nov 14, 2024
8cd5e86
wip
pcrespov Nov 15, 2024
67433b4
oas projects
pcrespov Nov 15, 2024
8dc5dad
oas folders
pcrespov Nov 15, 2024
3aa8713
cleanup
pcrespov Nov 15, 2024
e558dd1
resource usage
pcrespov Nov 15, 2024
c49afed
minor
pcrespov Nov 15, 2024
4b48d21
creates converter
pcrespov Nov 15, 2024
e4645e9
tunes ordering
pcrespov Nov 15, 2024
71b8405
refactor ordering factory
pcrespov Nov 15, 2024
76581e2
refactor using factory
pcrespov Nov 15, 2024
4f3778e
adapts projects
pcrespov Nov 15, 2024
eda3161
adapts ruth
pcrespov Nov 15, 2024
7cdeac5
updates OAS
pcrespov Nov 15, 2024
3b3be47
mypy
pcrespov Nov 15, 2024
e2734d7
moving requestcontext
pcrespov Nov 15, 2024
ab57ca5
common requestcontext
pcrespov Nov 15, 2024
39c1267
minor
pcrespov Nov 15, 2024
97dc5b5
mypy
pcrespov Nov 15, 2024
f122993
fixes format json-string issue
pcrespov Nov 15, 2024
b33b108
fixing test
pcrespov Nov 15, 2024
cdb3677
fixes search
pcrespov Nov 15, 2024
f0a0637
fixes tests
pcrespov Nov 16, 2024
1eb477f
@sanderegg review: rename
pcrespov Nov 18, 2024
70d59c1
@sanderegg review: rename
pcrespov Nov 18, 2024
1118eff
@sanderegg review: doc
pcrespov Nov 18, 2024
1b236e2
@odeimaiz review: wrong field
pcrespov Nov 18, 2024
806c59a
@matusdrobuliak66 review: adds map
pcrespov Nov 18, 2024
3181710
fixes tests
pcrespov Nov 18, 2024
b553f24
fixes defaults
pcrespov Nov 18, 2024
d85312a
fixes mypy
pcrespov Nov 18, 2024
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
66 changes: 61 additions & 5 deletions api/specs/web-server/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,68 @@
from typing import Any, ClassVar, NamedTuple

import yaml
from fastapi import FastAPI
from fastapi import FastAPI, Query
from models_library.basic_types import LogLevel
from pydantic import BaseModel, Field
from models_library.utils.json_serialization import json_dumps
from pydantic import BaseModel, Field, create_model
from pydantic.fields import FieldInfo
from servicelib.fastapi.openapi import override_fastapi_openapi_method

CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent


def _create_json_type(**schema_extras):
class _Json(str):
__slots__ = ()

@classmethod
def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
# openapi.json schema is corrected here
field_schema.update(
type="string",
# format="json-string" NOTE: we need to get rid of openapi-core in web-server before using this!
)
if schema_extras:
field_schema.update(schema_extras)

return _Json


def as_query(model_class: type[BaseModel]) -> type[BaseModel]:
fields = {}
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
for field_name, model_field in model_class.__fields__.items():

field_type = model_field.type_
default_value = model_field.default

kwargs = {
"alias": model_field.field_info.alias,
"title": model_field.field_info.title,
"description": model_field.field_info.description,
"gt": model_field.field_info.gt,
"ge": model_field.field_info.ge,
"lt": model_field.field_info.lt,
"le": model_field.field_info.le,
"min_length": model_field.field_info.min_length,
"max_length": model_field.field_info.max_length,
"regex": model_field.field_info.regex,
**model_field.field_info.extra,
}

if issubclass(field_type, BaseModel):
# Complex fields
field_type = _create_json_type(
description=kwargs["description"],
example=kwargs.get("example_json"),
)
default_value = json_dumps(default_value) if default_value else None

fields[field_name] = (field_type, Query(default=default_value, **kwargs))

new_model_name = f"{model_class.__name__}Query"
return create_model(new_model_name, **fields)


class Log(BaseModel):
level: LogLevel | None = Field("INFO", description="log level")
message: str = Field(
Expand Down Expand Up @@ -120,6 +173,9 @@ def assert_handler_signature_against_model(
for field in model_cls.__fields__.values()
]

assert {p.name for p in implemented_params}.issubset( # nosec
{p.name for p in specs_params}
), f"Entrypoint {handler} does not implement OAS"
implemented_names = {p.name for p in implemented_params}
specified_names = {p.name for p in specs_params}

if not implemented_names.issubset(specified_names):
msg = f"Entrypoint {handler} does not implement OAS: {implemented_names} not in {specified_names}"
raise AssertionError(msg)
57 changes: 20 additions & 37 deletions api/specs/web-server/_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@

from typing import Annotated

from fastapi import APIRouter, Depends, Query, status
from _common import as_query
from fastapi import APIRouter, Depends, status
from models_library.api_schemas_webserver.folders_v2 import (
CreateFolderBodyParams,
FolderGet,
PutFolderBodyParams,
)
from models_library.folders import FolderID
from models_library.generics import Envelope
from models_library.rest_pagination import PageQueryParameters
from models_library.workspaces import WorkspaceID
from pydantic import Json
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.folders._models import FolderFilters, FoldersPathParams
from simcore_service_webserver.folders._models import (
FolderSearchQueryParams,
FoldersListQueryParams,
FoldersPathParams,
)

router = APIRouter(
prefix=f"/{API_VTAG}",
Expand All @@ -36,7 +37,9 @@
response_model=Envelope[FolderGet],
status_code=status.HTTP_201_CREATED,
)
async def create_folder(_body: CreateFolderBodyParams):
async def create_folder(
_body: CreateFolderBodyParams,
):
...


Expand All @@ -45,20 +48,7 @@ async def create_folder(_body: CreateFolderBodyParams):
response_model=Envelope[list[FolderGet]],
)
async def list_folders(
params: Annotated[PageQueryParameters, Depends()],
folder_id: FolderID | None = None,
workspace_id: WorkspaceID | None = None,
order_by: Annotated[
Json,
Query(
description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
example='{"field": "name", "direction": "desc"}',
),
] = '{"field": "modified_at", "direction": "desc"}',
filters: Annotated[
Json | None,
Query(description=FolderFilters.schema_json(indent=1)),
] = None,
_query: Annotated[as_query(FoldersListQueryParams), Depends()],
):
...

Expand All @@ -68,19 +58,7 @@ async def list_folders(
response_model=Envelope[list[FolderGet]],
)
async def list_folders_full_search(
params: Annotated[PageQueryParameters, Depends()],
text: str | None = None,
order_by: Annotated[
Json,
Query(
description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
example='{"field": "name", "direction": "desc"}',
),
] = '{"field": "modified_at", "direction": "desc"}',
filters: Annotated[
Json | None,
Query(description=FolderFilters.schema_json(indent=1)),
] = None,
_query: Annotated[as_query(FolderSearchQueryParams), Depends()],
):
...

Expand All @@ -89,7 +67,9 @@ async def list_folders_full_search(
"/folders/{folder_id}",
response_model=Envelope[FolderGet],
)
async def get_folder(_path: Annotated[FoldersPathParams, Depends()]):
async def get_folder(
_path: Annotated[FoldersPathParams, Depends()],
):
...


Expand All @@ -98,7 +78,8 @@ async def get_folder(_path: Annotated[FoldersPathParams, Depends()]):
response_model=Envelope[FolderGet],
)
async def replace_folder(
_path: Annotated[FoldersPathParams, Depends()], _body: PutFolderBodyParams
_path: Annotated[FoldersPathParams, Depends()],
_body: PutFolderBodyParams,
):
...

Expand All @@ -107,5 +88,7 @@ async def replace_folder(
"/folders/{folder_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_folder(_path: Annotated[FoldersPathParams, Depends()]):
async def delete_folder(
_path: Annotated[FoldersPathParams, Depends()],
):
...
28 changes: 14 additions & 14 deletions api/specs/web-server/_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async def list_groups():
response_model=Envelope[GroupGet],
status_code=status.HTTP_201_CREATED,
)
async def create_group(_b: GroupCreate):
async def create_group(_body: GroupCreate):
"""
Creates an organization group
"""
Expand All @@ -58,7 +58,7 @@ async def create_group(_b: GroupCreate):
"/groups/{gid}",
response_model=Envelope[GroupGet],
)
async def get_group(_p: Annotated[_GroupPathParams, Depends()]):
async def get_group(_path: Annotated[_GroupPathParams, Depends()]):
"""
Get an organization group
"""
Expand All @@ -69,8 +69,8 @@ async def get_group(_p: Annotated[_GroupPathParams, Depends()]):
response_model=Envelope[GroupGet],
)
async def update_group(
_p: Annotated[_GroupPathParams, Depends()],
_b: GroupUpdate,
_path: Annotated[_GroupPathParams, Depends()],
_body: GroupUpdate,
):
"""
Updates organization groups
Expand All @@ -81,7 +81,7 @@ async def update_group(
"/groups/{gid}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_group(_p: Annotated[_GroupPathParams, Depends()]):
async def delete_group(_path: Annotated[_GroupPathParams, Depends()]):
"""
Deletes organization groups
"""
Expand All @@ -91,7 +91,7 @@ async def delete_group(_p: Annotated[_GroupPathParams, Depends()]):
"/groups/{gid}/users",
response_model=Envelope[list[GroupUserGet]],
)
async def get_all_group_users(_p: Annotated[_GroupPathParams, Depends()]):
async def get_all_group_users(_path: Annotated[_GroupPathParams, Depends()]):
"""
Gets users in organization groups
"""
Expand All @@ -102,8 +102,8 @@ async def get_all_group_users(_p: Annotated[_GroupPathParams, Depends()]):
status_code=status.HTTP_204_NO_CONTENT,
)
async def add_group_user(
_p: Annotated[_GroupPathParams, Depends()],
_b: GroupUserAdd,
_path: Annotated[_GroupPathParams, Depends()],
_body: GroupUserAdd,
):
"""
Adds a user to an organization group
Expand All @@ -115,7 +115,7 @@ async def add_group_user(
response_model=Envelope[GroupUserGet],
)
async def get_group_user(
_p: Annotated[_GroupUserPathParams, Depends()],
_path: Annotated[_GroupUserPathParams, Depends()],
):
"""
Gets specific user in an organization group
Expand All @@ -127,8 +127,8 @@ async def get_group_user(
response_model=Envelope[GroupUserGet],
)
async def update_group_user(
_p: Annotated[_GroupUserPathParams, Depends()],
_b: GroupUserUpdate,
_path: Annotated[_GroupUserPathParams, Depends()],
_body: GroupUserUpdate,
):
"""
Updates user (access-rights) to an organization group
Expand All @@ -140,7 +140,7 @@ async def update_group_user(
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_group_user(
_p: Annotated[_GroupUserPathParams, Depends()],
_path: Annotated[_GroupUserPathParams, Depends()],
):
"""
Removes a user from an organization group
Expand All @@ -157,8 +157,8 @@ async def delete_group_user(
response_model=Envelope[dict[str, Any]],
)
async def get_group_classifiers(
_p: Annotated[_GroupPathParams, Depends()],
_q: Annotated[_ClassifiersQuery, Depends()],
_path: Annotated[_GroupPathParams, Depends()],
_query: Annotated[_ClassifiersQuery, Depends()],
):
...

Expand Down
Loading
Loading