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

✨ Link to dispatch study with file-only and 🐛 fixes download link #4043

Merged
merged 40 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d8b9e21
cleanup
pcrespov Mar 29, 2023
b84cd3a
the test
pcrespov Mar 29, 2023
edabecf
WIP
pcrespov Mar 30, 2023
77322b1
logs
pcrespov Apr 27, 2023
1c292fd
protected
pcrespov Apr 27, 2023
1975eba
moves compose to rest handlers
pcrespov Apr 27, 2023
f60aedd
cleanup
pcrespov Apr 27, 2023
da6ba43
get or create user
pcrespov Apr 27, 2023
4410a4c
organized three variants
pcrespov Apr 27, 2023
7fdd30b
file only projects
pcrespov Apr 27, 2023
4b422a4
froze page params
pcrespov Apr 27, 2023
b23ac53
renaming
pcrespov Apr 27, 2023
c76f164
cleanup
pcrespov Apr 27, 2023
ea856d5
cleanup messages
pcrespov Apr 27, 2023
cdfe5d1
added errors
pcrespov Apr 27, 2023
6675aa2
maps and helpers
pcrespov Apr 27, 2023
9c53ab8
adds max file size as option
pcrespov Apr 27, 2023
cf83cc6
rm warning
pcrespov Apr 27, 2023
305e33a
minor
pcrespov Apr 27, 2023
1d80915
cleanup
pcrespov Apr 27, 2023
d9b90dc
adds file validator
pcrespov May 2, 2023
4e720f6
add model tests
pcrespov May 2, 2023
22b33b5
cleanup
pcrespov May 2, 2023
4e87ea2
new query params
pcrespov May 2, 2023
ac50708
request validat can take now unions
pcrespov May 2, 2023
3f768b6
query-params
pcrespov May 2, 2023
ad46a46
clenaup
pcrespov May 2, 2023
9b15624
cleanup
pcrespov May 2, 2023
f0d2bca
query validation
pcrespov May 2, 2023
c0723ea
rm RedirectionQueryParams
pcrespov May 2, 2023
2674db0
fixes url
pcrespov May 2, 2023
0ff8fcc
@mrnicegyu11 review: typo
pcrespov May 3, 2023
e3f3a5f
@GitHK review: unused constant
pcrespov May 3, 2023
d50bddc
@mrnicegyu11 review: doc
pcrespov May 3, 2023
f48877c
@mrnicegyu11 review: doc
pcrespov May 3, 2023
f557b27
@GitHK review: mypy ignores
pcrespov May 3, 2023
93e641f
@GitHK review: parse_obj instead of cast
pcrespov May 3, 2023
628fb7f
fixes
pcrespov May 3, 2023
5af242f
fixes
pcrespov May 3, 2023
4fb47ca
fix test
pcrespov May 3, 2023
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
14 changes: 7 additions & 7 deletions api/specs/webserver/scripts/openapi_nih_sparc.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
""" Helper script to generate OAS automatically NIH-sparc portal API section
"""

# pylint: disable=protected-access
# pylint: disable=redefined-outer-name
# pylint: disable=too-many-arguments
# pylint: disable=unused-argument
# pylint: disable=unused-variable
# pylint: disable=too-many-arguments

from enum import Enum
from typing import Optional, Union

from fastapi import FastAPI, status
from fastapi.responses import RedirectResponse
from models_library.generics import Envelope
from models_library.projects import ProjectID
from models_library.services import ServiceKey, ServiceKeyVersion
from pydantic import HttpUrl, PositiveInt
from simcore_service_webserver.studies_dispatcher.handlers_rest import (
from simcore_service_webserver.studies_dispatcher._rest_handlers import (
ServiceGet,
Viewer,
)

app = FastAPI(redoc_url=None)

TAGS: list[Union[str, Enum]] = [
TAGS: list[str | Enum] = [
"nih-sparc",
]

Expand All @@ -43,7 +43,7 @@ async def list_services():
tags=TAGS,
operation_id="list_viewers",
)
async def list_viewers(file_type: Optional[str] = None):
async def list_viewers(file_type: str | None = None):
"""Lists all publically available viewers

Notice that this might contain multiple services for the same filetype
Expand All @@ -58,7 +58,7 @@ async def list_viewers(file_type: Optional[str] = None):
tags=TAGS,
operation_id="list_default_viewers",
)
async def list_default_viewers(file_type: Optional[str] = None):
async def list_default_viewers(file_type: str | None = None):
"""Lists the default viewer for each supported filetype

This was interfaced as a subcollection of viewers because it is a very common use-case
Expand All @@ -83,7 +83,7 @@ async def get_redirection_to_viewer(
viewer_version: ServiceKeyVersion,
file_size: PositiveInt,
download_link: HttpUrl,
file_name: Optional[str] = "unknown",
file_name: str | None = "unknown",
):
"""Opens a viewer in osparc for data in the NIH-sparc portal"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Optional, TypeVar

from pydantic import ValidationError
from pydantic.tools import parse_obj_as

T = TypeVar("T")


def parse_obj_or_none(type_: type[T], obj) -> Optional[T]:
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
try:
return parse_obj_as(type_, obj)
except ValidationError:
return None
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@

import json.decoder
from contextlib import contextmanager
from typing import Iterator, TypeVar, Union
from typing import Iterator, TypeAlias, TypeVar, Union

from aiohttp import web
from pydantic import BaseModel, ValidationError, parse_obj_as

from ..json_serialization import json_dumps
from ..mimetype_constants import MIMETYPE_APPLICATION_JSON

ModelType = TypeVar("ModelType", bound=BaseModel)
ModelClass = TypeVar("ModelClass", bound=BaseModel)
ModelOrListType = TypeVar("ModelOrListType", bound=Union[BaseModel, list])

UnionOfModelTypes: TypeAlias = Union[type[ModelClass], type[ModelClass]]

@contextmanager
def handle_validation_as_http_error(
Expand Down Expand Up @@ -102,11 +102,11 @@ def handle_validation_as_http_error(


def parse_request_path_parameters_as(
parameters_schema: type[ModelType],
parameters_schema_cls: type[ModelClass],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelType:
) -> ModelClass:
"""Parses path parameters from 'request' and validates against 'parameters_schema'


Expand All @@ -126,15 +126,15 @@ def parse_request_path_parameters_as(
use_error_v1=use_enveloped_error_v1,
):
data = dict(request.match_info)
return parameters_schema.parse_obj(data)
return parameters_schema_cls.parse_obj(data)


def parse_request_query_parameters_as(
parameters_schema: type[ModelType],
parameters_schema_cls: type[ModelClass] | UnionOfModelTypes,
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelType:
) -> ModelClass:
"""Parses query parameters from 'request' and validates against 'parameters_schema'


Expand All @@ -154,11 +154,13 @@ def parse_request_query_parameters_as(
use_error_v1=use_enveloped_error_v1,
):
data = dict(request.query)
return parameters_schema.parse_obj(data)
if hasattr(parameters_schema_cls, "parse_obj"):
return parameters_schema_cls.parse_obj(data)
return parse_obj_as(parameters_schema_cls, data)
GitHK marked this conversation as resolved.
Show resolved Hide resolved


async def parse_request_body_as(
model_schema: type[ModelOrListType],
model_schema_cls: type[ModelOrListType],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
Expand Down Expand Up @@ -189,11 +191,11 @@ async def parse_request_body_as(
except json.decoder.JSONDecodeError as err:
raise web.HTTPBadRequest(reason=f"Invalid json in body: {err}")

if hasattr(model_schema, "parse_obj"):
if hasattr(model_schema_cls, "parse_obj"):
# NOTE: model_schema can be 'list[T]' or 'dict[T]' which raise TypeError
# with issubclass(model_schema, BaseModel)
assert issubclass(model_schema, BaseModel) # nosec
return model_schema.parse_obj(body)
assert issubclass(model_schema_cls, BaseModel) # nosec
return model_schema_cls.parse_obj(body)

# used for model_schema like 'list[T]' or 'dict[T]'
return parse_obj_as(model_schema, body)
return parse_obj_as(model_schema_cls, body)
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 7 additions & 3 deletions services/web/server/src/simcore_service_webserver/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

multiprocessing.set_start_method("spawn", True)

log = logging.getLogger(__name__)
_logger = logging.getLogger(__name__)


def _setup_app_from_settings(
Expand Down Expand Up @@ -64,7 +64,9 @@ async def app_factory() -> web.Application:
app_settings = ApplicationSettings.create_from_envs()
assert app_settings.SC_BUILD_TARGET # nosec

log.info("Application settings: %s", app_settings.json(indent=2, sort_keys=True))
_logger.info(
"Application settings: %s", app_settings.json(indent=2, sort_keys=True)
)

app, _ = _setup_app_from_settings(app_settings)

Expand All @@ -75,7 +77,9 @@ async def app_factory() -> web.Application:

main = typer.Typer(name="simcore-service-webserver")

main.command()(create_settings_command(settings_cls=ApplicationSettings, logger=log))
main.command()(
create_settings_command(settings_cls=ApplicationSettings, logger=_logger)
)

main.command()(login_cli.invitations)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
"""
This app module dispatches pre-configure or on-the-fly studies to user from a link request
that is redirected to the front-end which customizes its view

- Table services_consume_filetypes defines a map between a service (key,version,input_port) and a filetype
- Exposes viewers resource in API (handlers_rest.py)
- Entrypoint that dispatches a study to download & view a file (handlers_redirects.py)
- finds default viewer
- get or create user
- get or create project with file-picker(download-link)+viewer
- redirect to main page (passing study information to the front-end in the fragment)

This app module dispatches pre-configured or on-the-fly studies to a user from a permalink
that is redirected to the front-end
"""
# TODO: move here all studies_access.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from contextlib import suppress
from dataclasses import dataclass
from typing import AsyncIterator
Expand All @@ -8,6 +9,7 @@
from aiopg.sa.engine import Engine
from models_library.services import ServiceKey, ServiceVersion
from pydantic import HttpUrl, PositiveInt, ValidationError, parse_obj_as
from servicelib.logging_utils import log_decorator
from simcore_postgres_database.models.services import (
services_access_rights,
services_meta_data,
Expand All @@ -18,12 +20,14 @@
from simcore_postgres_database.utils_services import create_select_latest_services_query

from ..db import get_database_engine
from ._exceptions import StudyDispatcherError
from ._errors import ServiceNotFound
from .settings import StudiesDispatcherSettings, get_plugin_settings

_EVERYONE_GROUP_ID = 1
LARGEST_PAGE_SIZE = 1000

_logger = logging.getLogger(__name__)


@dataclass
class ServiceMetaData:
Expand Down Expand Up @@ -116,20 +120,21 @@ async def iter_latest_product_services(


@dataclass
class ServiceValidated:
class ValidService:
key: str
version: str
title: str
is_public: bool
thumbnail: HttpUrl | None # nullable


@log_decorator(_logger, level=logging.DEBUG)
async def validate_requested_service(
app: web.Application,
*,
service_key: ServiceKey,
service_version: ServiceVersion,
) -> ServiceValidated:
) -> ValidService:
engine: Engine = get_database_engine(app)

async with engine.acquire() as conn:
Expand All @@ -148,8 +153,8 @@ async def validate_requested_service(
row = await result.fetchone()

if row is None:
raise StudyDispatcherError(
f"Service {service_key}:{service_version} not found"
raise ServiceNotFound(
service_key=service_key, service_version=service_version
)

assert row.key == service_key # nosec
Expand All @@ -170,7 +175,7 @@ async def validate_requested_service(
with suppress(ValidationError):
thumbnail_or_none = parse_obj_as(HttpUrl, row.thumbnail)

return ServiceValidated(
return ValidService(
key=service_key,
version=service_version,
is_public=bool(is_guest_allowed),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
from typing import Final

#
# NOTE: MSG_$(ERROR_CODE_NAME) strings MUST be human readable messages
# Please keep alphabetical order
#


MSG_PROJECT_NOT_FOUND: Final[str] = "Cannot find any study with ID '{project_id}'"


# This error happens when the linked study ID does not exists OR is not shared with everyone
MSG_PROJECT_NOT_PUBLISHED: Final[str] = "Cannot find any study with ID '{project_id}'"

# This error happens when the linked study ID does not exists OR is not shared with everyone OR is NOT public
MSG_PUBLIC_PROJECT_NOT_PUBLISHED: Final[str] = (
"You need to be logged in to access study with ID '{project_id}'\n"
"Please login and try again\n"
"If you don't have an account, write to the Support email to request one\n"
"You need to be logged in to access study with ID '{project_id}'.\n"
"Please login and try again.\n"
"If you don't have an account, please email to support and request one\n"
)

MSG_GUESTS_NOT_ALLOWED: Final[str] = (
"Access restricted to registered users.\n"
"If you don't have an account, please email to support and request one\n"
)

MSG_UNEXPECTED_ERROR: Final[
str
Expand Down
Loading