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

♻️ Is3626/api-server test tools and coverage #4149

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
11bfbb5
app_environment fixture
pcrespov Apr 24, 2023
eacfa5a
fixes respx test fixtures
pcrespov Apr 24, 2023
f715e48
fixes response
pcrespov Apr 24, 2023
00f1aee
fixe responses
pcrespov Apr 24, 2023
e038142
create-job test mocked
pcrespov Apr 24, 2023
cfa9b4c
all test passes
pcrespov Apr 24, 2023
87fd046
cleanup
pcrespov Apr 24, 2023
7bd2fd1
fixes fixture app_environment
pcrespov Apr 24, 2023
154ade8
cleanup dependencies
pcrespov Apr 24, 2023
a28bb58
msg
pcrespov Apr 24, 2023
d6ade52
cleanup _meta
pcrespov Apr 24, 2023
faa1f88
fixes pylint
pcrespov Apr 25, 2023
aa31376
adds test_cli
pcrespov Apr 25, 2023
d19bd7d
tooling to capture
pcrespov Apr 25, 2023
3a3a808
utils
pcrespov Apr 26, 2023
e8f5b2d
adds captures in catalog module
pcrespov Apr 26, 2023
d225eb1
capture
pcrespov Apr 26, 2023
96afb82
minor rename
pcrespov Apr 26, 2023
ace1c62
captures directorv2
pcrespov Apr 26, 2023
49544af
adds captions storage
pcrespov Apr 26, 2023
86b75c0
renames modules -> plugin
pcrespov Apr 26, 2023
ebf6ab8
fixes logs and changes path
pcrespov Apr 26, 2023
e09a0ca
types
pcrespov Apr 26, 2023
f107b89
fix mypy
pcrespov Apr 26, 2023
fe31431
cleanup settings
pcrespov Apr 26, 2023
1e67e53
fixes mypy
pcrespov Apr 26, 2023
c8ef9db
tests fixes
pcrespov Apr 26, 2023
1ea2370
fixes hotspot
pcrespov Apr 26, 2023
2fdfb29
fix
pcrespov Apr 26, 2023
5808790
minor
pcrespov Apr 26, 2023
56e313f
fix logs
pcrespov Apr 26, 2023
cfa7f4d
pylint fix
pcrespov Apr 27, 2023
d2d3028
code smells
pcrespov Apr 27, 2023
aa5b532
code smells
pcrespov Apr 27, 2023
c879e8e
pylint
pcrespov Apr 27, 2023
1c1d351
@GitHK review: renaming and doc
pcrespov Apr 27, 2023
7aa068e
rename
pcrespov Apr 27, 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
10 changes: 8 additions & 2 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@ WEBSERVER_PROMETHEUS_PORT=9090
WEBSERVER_RESOURCES_DELETION_TIMEOUT_SECONDS=900
WEBSERVER_SESSION_SECRET_KEY='REPLACE_ME_with_result__Fernet_generate_key='
WEBSERVER_STUDIES_ACCESS_ENABLED=0
# for debugging


# For development ONLY ---------------
#
# AIODEBUG_SLOW_DURATION_SECS=0.25
# API_SERVER_DEV_HTTP_CALLS_LOGS_PATH=captures.ignore.keep.log
# PYTHONTRACEMALLOC=1
# PYTHONASYNCIODEBUG=1
# AIODEBUG_SLOW_DURATION_SECS=0.25
#
# ------------------------------------
5 changes: 2 additions & 3 deletions packages/settings-library/src/settings_library/postgres.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import urllib.parse
from functools import cached_property
from typing import Optional

from pydantic import Field, PostgresDsn, SecretStr, conint, validator

Expand Down Expand Up @@ -30,7 +29,7 @@ class PostgresSettings(BaseCustomSettings):
50, description="Maximum number of connections in the pool"
)

POSTGRES_CLIENT_NAME: Optional[str] = Field(
POSTGRES_CLIENT_NAME: str | None = Field(
None,
description="Name of the application connecting the postgres database, will default to use the host hostname (hostname on linux)",
env=[
Expand Down Expand Up @@ -87,7 +86,7 @@ def dsn_with_query(self) -> str:
class Config(BaseCustomSettings.Config):
schema_extra = {
"examples": [
# minimal
# minimal required
{
"POSTGRES_HOST": "localhost",
"POSTGRES_PORT": "5432",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Final

MSG_BACKEND_SERVICE_UNAVAILABLE: Final[
str
] = "backend service is disabled or unreachable"
38 changes: 21 additions & 17 deletions services/api-server/src/simcore_service_api_server/_meta.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
""" Application's metadata

"""
from contextlib import suppress

import pkg_resources

_current_distribution = pkg_resources.get_distribution("simcore_service_api_server")
from typing import Final

PROJECT_NAME: str = _current_distribution.project_name
from packaging.version import Version
from servicelib.utils_meta import PackageInfo

API_VERSION: str = _current_distribution.version
MAJOR, MINOR, PATCH = _current_distribution.version.split(".")
API_VTAG: str = f"v{MAJOR}"
info: Final = PackageInfo(package_name="simcore-service-api-server")
__version__: Final[str] = info.__version__

__version__ = _current_distribution.version

PROJECT_NAME: Final[str] = info.project_name
VERSION: Final[Version] = info.version
API_VERSION: Final[str] = info.__version__
API_VTAG: Final[str] = info.api_prefix_path_tag
SUMMARY: Final[str] = info.get_summary()

def get_summary() -> str:
with suppress(Exception):
try:
metadata = _current_distribution.get_metadata_lines("METADATA")
except FileNotFoundError:
metadata = _current_distribution.get_metadata_lines("PKG-INFO")

return next(x.split(":") for x in metadata if x.startswith("Summary:"))[-1]
return ""
#
# https://patorjk.com/software/taag/#p=display&f=JS%20Stick%20Letters&t=API-server%0A
#
APP_STARTED_BANNER_MSG = r"""
__ __ ___ __ ___ __
/\ |__) | __ /__` |__ |__) \ / |__ |__)
/~~\ | | .__/ |___ | \ \/ |___ | \ {}

""".format(
f"v{__version__}"
)

SUMMARY: str = get_summary()
APP_FINISHED_BANNER_MSG = info.get_finished_banner()
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,23 @@
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.requests import Request

from ...core.settings import WebServerSettings
from ...modules.webserver import AuthSession
from ..._constants import MSG_BACKEND_SERVICE_UNAVAILABLE
from ...core.settings import ApplicationSettings, WebServerSettings
from ...plugins.webserver import AuthSession
from .application import get_app, get_settings
from .authentication import get_active_user_email

UNAVAILBLE_MSG = "backend service is disabled or unreachable"


def _get_app(request: Request) -> FastAPI:
return request.app


def _get_settings(request: Request) -> WebServerSettings:
s = request.app.state.settings.API_SERVER_WEBSERVER
if not s:
def _get_settings(
app_settings: ApplicationSettings = Depends(get_settings),
) -> WebServerSettings:
settings = app_settings.API_SERVER_WEBSERVER
if not settings:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE, detail="webserver disabled"
status.HTTP_503_SERVICE_UNAVAILABLE, detail=MSG_BACKEND_SERVICE_UNAVAILABLE
)

assert isinstance(s, WebServerSettings) # nosec
return s
assert isinstance(settings, WebServerSettings) # nosec
return settings


def _get_encrypt(request: Request) -> Fernet | None:
Expand All @@ -41,7 +38,9 @@ def get_session_cookie(
# SEE services/web/server/tests/unit/with_dbs/test_login.py

if fernet is None:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=UNAVAILBLE_MSG)
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE, detail=MSG_BACKEND_SERVICE_UNAVAILABLE
)

# builds session cookie
cookie_name = settings.WEBSERVER_SESSION_NAME
Expand All @@ -59,7 +58,7 @@ def get_session_cookie(


def get_webserver_session(
app: FastAPI = Depends(_get_app),
app: FastAPI = Depends(get_app),
session_cookies: dict = Depends(get_session_cookie),
) -> AuthSession:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from ..._meta import API_VTAG
from ...models.schemas.files import File
from ...modules.storage import StorageApi, StorageFileMetaData, to_file_api_model
from ...plugins.storage import StorageApi, StorageFileMetaData, to_file_api_model
from ..dependencies.authentication import get_current_user_id
from ..dependencies.services import get_api_client

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from models_library.app_diagnostics import AppStatusCheck

from ..._meta import API_VERSION, PROJECT_NAME
from ...modules.catalog import CatalogApi
from ...modules.director_v2 import DirectorV2Api
from ...modules.storage import StorageApi
from ...modules.webserver import WebserverApi
from ...plugins.catalog import CatalogApi
from ...plugins.director_v2 import DirectorV2Api
from ...plugins.storage import StorageApi
from ...plugins.webserver import WebserverApi
from ..dependencies.application import get_reverse_url_mapper
from ..dependencies.services import get_api_client

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from ...core.settings import BasicSettings
from ...models.basic_types import VersionStr
from ...models.schemas.solvers import Solver, SolverKeyId, SolverPort
from ...modules.catalog import CatalogApi
from ...plugins.catalog import CatalogApi
from ..dependencies.application import get_product_name, get_reverse_url_mapper
from ..dependencies.authentication import get_current_user_id
from ..dependencies.services import get_api_client

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

router = APIRouter()
settings = BasicSettings.create_from_envs()
Expand Down Expand Up @@ -193,7 +193,7 @@ async def list_solver_ports(

except ValidationError as err:
error_code = create_error_code(err)
logger.exception(
_logger.exception(
"Corrupted port data for service %s [%s]",
f"{solver_key}:{version}",
f"{error_code}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from ...models.schemas.files import File
from ...models.schemas.jobs import ArgumentType, Job, JobInputs, JobOutputs, JobStatus
from ...models.schemas.solvers import Solver, SolverKeyId
from ...modules.catalog import CatalogApi
from ...modules.director_v2 import DirectorV2Api, DownloadLink, NodeName
from ...modules.storage import StorageApi, to_file_api_model
from ...plugins.catalog import CatalogApi
from ...plugins.director_v2 import DirectorV2Api, DownloadLink, NodeName
from ...plugins.storage import StorageApi, to_file_api_model
from ...utils.solver_job_models_converters import (
create_job_from_project,
create_jobstatus_from_task,
Expand All @@ -33,7 +33,7 @@
from ..dependencies.services import get_api_client
from ..dependencies.webserver import AuthSession, get_webserver_session

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

router = APIRouter()

Expand Down Expand Up @@ -73,7 +73,7 @@ async def list_jobs(
version=version,
product_name=product_name,
)
logger.debug("Listing Jobs in Solver '%s'", solver.name)
_logger.debug("Listing Jobs in Solver '%s'", solver.name)

projects: list[Project] = await webserver_api.list_projects(solver.name)
jobs: deque[Job] = deque()
Expand Down Expand Up @@ -116,7 +116,7 @@ async def create_job(

# creates NEW job as prototype
pre_job = Job.create_solver_job(solver=solver, inputs=inputs)
logger.debug("Creating Job '%s'", pre_job.name)
_logger.debug("Creating Job '%s'", pre_job.name)

# -> catalog
# TODO: validate inputs against solver input schema
Expand Down Expand Up @@ -154,7 +154,7 @@ async def get_job(
"""Gets job of a given solver"""

job_name = _compose_job_resource_name(solver_key, version, job_id)
logger.debug("Getting Job '%s'", job_name)
_logger.debug("Getting Job '%s'", job_name)

project: Project = await webserver_api.get_project(project_id=job_id)

Expand Down Expand Up @@ -182,7 +182,7 @@ async def start_job(
"""

job_name = _compose_job_resource_name(solver_key, version, job_id)
logger.debug("Start Job '%s'", job_name)
_logger.debug("Start Job '%s'", job_name)

task = await director2_api.start_computation(
project_id=job_id,
Expand All @@ -205,7 +205,7 @@ async def stop_job(
director2_api: DirectorV2Api = Depends(get_api_client(DirectorV2Api)),
):
job_name = _compose_job_resource_name(solver_key, version, job_id)
logger.debug("Stopping Job '%s'", job_name)
_logger.debug("Stopping Job '%s'", job_name)

await director2_api.stop_computation(job_id, user_id)

Expand All @@ -226,7 +226,7 @@ async def inspect_job(
director2_api: DirectorV2Api = Depends(get_api_client(DirectorV2Api)),
):
job_name = _compose_job_resource_name(solver_key, version, job_id)
logger.debug("Inspecting Job '%s'", job_name)
_logger.debug("Inspecting Job '%s'", job_name)

task = await director2_api.get_computation(job_id, user_id)
job_status: JobStatus = create_jobstatus_from_task(task)
Expand All @@ -247,7 +247,7 @@ async def get_job_outputs(
storage_client: StorageApi = Depends(get_api_client(StorageApi)),
):
job_name = _compose_job_resource_name(solver_key, version, job_id)
logger.debug("Get Job '%s' outputs", job_name)
_logger.debug("Get Job '%s' outputs", job_name)

project: Project = await webserver_api.get_project(project_id=job_id)
node_ids = list(project.workbench.keys())
Expand Down Expand Up @@ -328,7 +328,7 @@ async def get_job_output_logfile(
), "Current version only supports one node per solver"

for presigned_download_link in logs_urls.values():
logger.info(
_logger.info(
"Redirecting '%s' to %s ...",
f"{solver_key}/releases/{version}/jobs/{job_id}/outputs/logfile",
presigned_download_link,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

from ...core.settings import BasicSettings
from ...models.schemas.studies import StudyID, StudyPort
from ...modules.webserver import AuthSession
from ...plugins.webserver import AuthSession
from ..dependencies.webserver import get_webserver_session

logger = logging.getLogger(__name__)
_logger = logging.getLogger(__name__)
router = APIRouter()
settings = BasicSettings.create_from_envs()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from fastapi import APIRouter, Depends, Security

from ...models.schemas.profiles import Profile, ProfileUpdate
from ...modules.webserver import AuthSession
from ...plugins.webserver import AuthSession
from ..dependencies.webserver import get_webserver_session

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


router = APIRouter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@
from ..api.errors.validation_error import http422_error_handler
from ..api.root import create_router
from ..api.routes.health import router as health_router
from ..modules import catalog, director_v2, remote_debug, storage, webserver
from ..plugins import catalog, director_v2, remote_debug, storage, webserver
from .events import create_start_app_handler, create_stop_app_handler
from .openapi import override_openapi_method, use_route_names_as_operation_ids
from .redoc import create_redoc_handler
from .settings import ApplicationSettings

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


def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
if settings is None:
settings = ApplicationSettings.create_from_envs()
assert settings # nosec

logging.basicConfig(level=settings.LOG_LEVEL.value)
logging.root.setLevel(settings.LOG_LEVEL.value)
logging.basicConfig(level=settings.log_level)
logging.root.setLevel(settings.log_level)
config_all_loggers(settings.API_SERVER_LOG_FORMAT_LOCAL_DEV_ENABLED)
logger.debug("App settings:\n%s", settings.json(indent=2))
_logger.debug("App settings:\n%s", settings.json(indent=2))

# creates app instance
app = FastAPI(
Expand Down
Loading