Skip to content

Commit

Permalink
🎨 web-api services api response includes manifest info (part 5) (#6061
Browse files Browse the repository at this point in the history
)
  • Loading branch information
pcrespov authored Jul 16, 2024
1 parent 9e7cf24 commit 0c23386
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 166 deletions.
28 changes: 12 additions & 16 deletions packages/pytest-simcore/src/pytest_simcore/docker_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,13 @@ def simcore_docker_compose(
docker_compose_path.exists() for docker_compose_path in docker_compose_paths
)

compose_specs = run_docker_compose_config(
return run_docker_compose_config(
project_dir=osparc_simcore_root_dir / "services",
scripts_dir=osparc_simcore_scripts_dir,
docker_compose_paths=docker_compose_paths,
env_file_path=env_file_for_testing,
destination_path=temp_folder / "simcore_docker_compose.yml",
)
# NOTE: do not add indent. Copy&Paste log into editor instead
print(
HEADER_STR.format("simcore docker-compose"),
json.dumps(compose_specs),
HEADER_STR.format("-"),
)
return compose_specs


@pytest.fixture(scope="module")
Expand All @@ -203,20 +196,13 @@ def ops_docker_compose(
)
assert docker_compose_path.exists()

compose_specs = run_docker_compose_config(
return run_docker_compose_config(
project_dir=osparc_simcore_root_dir / "services",
scripts_dir=osparc_simcore_scripts_dir,
docker_compose_paths=docker_compose_path,
env_file_path=env_file_for_testing,
destination_path=temp_folder / "ops_docker_compose.yml",
)
# NOTE: do not add indent. Copy&Paste log into editor instead
print(
HEADER_STR.format("ops docker-compose"),
json.dumps(compose_specs),
HEADER_STR.format("-"),
)
return compose_specs


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -245,6 +231,11 @@ def core_docker_compose_file(
core_services_selection, simcore_docker_compose, docker_compose_path
)

print(
HEADER_STR.format(f"{docker_compose_path}"),
json.dumps(docker_compose_path.read_text()),
HEADER_STR.format("-"),
)
return docker_compose_path


Expand Down Expand Up @@ -281,6 +272,11 @@ def ops_docker_compose_file(
ops_services_selection, ops_docker_compose, docker_compose_path
)

print(
HEADER_STR.format(f"{docker_compose_path}"),
json.dumps(docker_compose_path.read_text()),
HEADER_STR.format("-"),
)
return docker_compose_path


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@


# string templates
HEADER_STR: str = "{:-^50}\n"
HEADER_STR: str = "{:-^100}\n"
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ async def get_service_from_manifest(
"""
try:
return await manifest.get_service(
service_key=service_key,
service_version=service_version,
key=service_key,
version=service_version,
director_client=director_client,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
)

from ...db.repositories.services import ServicesRepository
from ...services import catalog
from ...services import services_api
from ..dependencies.director import get_director_api

_logger = logging.getLogger(__name__)

Expand All @@ -38,8 +39,9 @@ async def list_services_paginated(
) -> PageRpcServicesGetV2:
assert app.state.engine # nosec

total_count, items = await catalog.list_services_paginated(
total_count, items = await services_api.list_services_paginated(
repo=ServicesRepository(app.state.engine),
director_api=get_director_api(app),
product_name=product_name,
user_id=user_id,
limit=limit,
Expand Down Expand Up @@ -69,8 +71,9 @@ async def get_service(
) -> ServiceGetV2:
assert app.state.engine # nosec

service = await catalog.get_service(
service = await services_api.get_service(
repo=ServicesRepository(app.state.engine),
director_api=get_director_api(app),
product_name=product_name,
user_id=user_id,
service_key=service_key,
Expand Down Expand Up @@ -98,8 +101,9 @@ async def update_service(

assert app.state.engine # nosec

service = await catalog.update_service(
service = await services_api.update_service(
repo=ServicesRepository(app.state.engine),
director_api=get_director_api(app),
product_name=product_name,
user_id=user_id,
service_key=service_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,9 @@ async def list_services_access_rights(
)
async with self.db_engine.connect() as conn:
async for row in await conn.stream(query):
service_to_access_rights[
(
row[services_access_rights.c.key],
row[services_access_rights.c.version],
)
].append(ServiceAccessRightsAtDB.from_orm(row))
service_to_access_rights[(row.key, row.version)].append(
ServiceAccessRightsAtDB.from_orm(row)
)
return service_to_access_rights

async def upsert_service_access_rights(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from models_library.services_metadata_published import ServiceMetaDataPublished
from models_library.services_types import ServiceKey, ServiceVersion
from models_library.utils.json_serialization import json_dumps
from pydantic import parse_obj_as
from servicelib.logging_utils import log_context
from starlette import status
from tenacity._asyncio import AsyncRetrying
Expand Down Expand Up @@ -140,12 +139,6 @@ async def is_responsive(self) -> bool:
except (httpx.HTTPStatusError, httpx.RequestError, httpx.TimeoutException):
return False

async def list_all_services(self) -> list[ServiceMetaDataPublished]:
# WARNING: this function probably raise ValidationError since director does NOT offer guarantees.
# SEE list_registered_services
data = await self.get("/services")
return parse_obj_as(list[ServiceMetaDataPublished], data)

async def get_service(
self, service_key: ServiceKey, service_version: ServiceVersion
) -> ServiceMetaDataPublished:
Expand Down
38 changes: 33 additions & 5 deletions services/catalog/src/simcore_service_catalog/services/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
import logging
from typing import Any, TypeAlias, cast

from aiocache import cached
from models_library.function_services_catalog.api import iter_service_docker_data
from models_library.services_metadata_published import ServiceMetaDataPublished
from models_library.services_types import ServiceKey, ServiceVersion
from pydantic import ValidationError
from servicelib.utils import limited_gather

from .._constants import DIRECTOR_CACHING_TTL
from .director import DirectorApi
from .function_services import get_function_service, is_function_service

Expand Down Expand Up @@ -80,18 +83,43 @@ async def get_services_map(
return services


@cached(
ttl=DIRECTOR_CACHING_TTL,
namespace=__name__,
key_builder=lambda f, *ag, **kw: f"{f.__name__}/{kw['key']}/{kw['version']}",
)
async def get_service(
service_key: ServiceKey,
service_version: ServiceVersion,
director_client: DirectorApi,
*,
key: ServiceKey,
version: ServiceVersion,
) -> ServiceMetaDataPublished:
"""
Retrieves service metadata from the docker registry via the director and accounting
raises if does not exist or if validation fails
"""
if is_function_service(service_key):
service = get_function_service(key=service_key, version=service_version)
if is_function_service(key):
service = get_function_service(key=key, version=version)
else:
service = await director_client.get_service(
service_key=service_key, service_version=service_version
service_key=key, service_version=version
)
return service


async def get_batch_services(
selection: list[tuple[ServiceKey, ServiceVersion]],
director_client: DirectorApi,
) -> list[ServiceMetaDataPublished | BaseException]:

batch: list[ServiceMetaDataPublished | BaseException] = await limited_gather(
*(
get_service(key=k, version=v, director_client=director_client)
for k, v in selection
),
reraise=False,
log=_logger,
tasks_group_prefix="manifest.get_batch_services",
)
return batch
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
)
from models_library.products import ProductName
from models_library.rest_pagination import PageLimitInt
from models_library.services_authoring import Author
from models_library.services_enums import ServiceType
from models_library.services_metadata_published import ServiceMetaDataPublished
from models_library.services_types import ServiceKey, ServiceVersion
from models_library.users import UserID
from pydantic import NonNegativeInt
Expand All @@ -21,6 +21,8 @@
ServiceMetaDataAtDB,
ServiceWithHistoryFromDB,
)
from simcore_service_catalog.services import manifest
from simcore_service_catalog.services.director import DirectorApi

from ..db.repositories.services import ServicesRepository
from .function_services import is_function_service
Expand All @@ -39,29 +41,33 @@ def _deduce_service_type_from(key: str) -> ServiceType:
def _db_to_api_model(
service_db: ServiceWithHistoryFromDB,
access_rights_db: list[ServiceAccessRightsAtDB],
service_manifest: ServiceMetaDataPublished,
) -> ServiceGetV2:
assert (
_deduce_service_type_from(service_db.key) == service_manifest.service_type
) # nosec
return ServiceGetV2(
key=service_db.key,
version=service_db.version,
name=service_db.name,
thumbnail=service_db.thumbnail or None,
description=service_db.description,
version_display=f"V{service_db.version}", # rg.version_display,
type=_deduce_service_type_from(service_db.key), # rg.service_type,
contact=Author.Config.schema_extra["examples"][0]["email"], # rg.contact,
authors=Author.Config.schema_extra["examples"],
version_display=service_manifest.version_display,
type=service_manifest.service_type,
contact=service_manifest.contact,
authors=service_manifest.authors,
owner=service_db.owner_email or None,
inputs={}, # rg.inputs,
outputs={}, # rg.outputs,
boot_options=None, # rg.boot_options,
min_visible_inputs=None, # rg.min_visible_inputs,
inputs=service_manifest.inputs or {},
outputs=service_manifest.outputs or {},
boot_options=service_manifest.boot_options,
min_visible_inputs=service_manifest.min_visible_inputs,
access_rights={
a.gid: ServiceGroupAccessRightsV2.construct(
execute=a.execute_access,
write=a.write_access,
)
for a in access_rights_db
}, # db.access_rights,
},
classifiers=service_db.classifiers,
quality=service_db.quality,
history=[h.to_api_model() for h in service_db.history],
Expand All @@ -70,6 +76,7 @@ def _db_to_api_model(

async def list_services_paginated(
repo: ServicesRepository,
director_api: DirectorApi,
product_name: ProductName,
user_id: UserID,
limit: PageLimitInt | None,
Expand All @@ -94,19 +101,31 @@ async def list_services_paginated(
product_name=product_name,
)

# get manifest of those with access rights
got = await manifest.get_batch_services(
[(s.key, s.version) for s in services if access_rights.get((s.key, s.version))],
director_api,
)
service_manifest = {
(s.key, s.version): s for s in got if isinstance(s, ServiceMetaDataPublished)
}

# NOTE: aggregates published (i.e. not editable) is still missing in this version
items = [
_db_to_api_model(s, ar)
_db_to_api_model(s, ar, sm)
for s in services
if (ar := access_rights.get((s.key, s.version)))
if (
(ar := access_rights.get((s.key, s.version)))
and (sm := service_manifest.get((s.key, s.version)))
)
]

return total_count, items


async def get_service(
repo: ServicesRepository,
# image_registry,
director_api: DirectorApi,
product_name: ProductName,
user_id: UserID,
service_key: ServiceKey,
Expand Down Expand Up @@ -142,11 +161,18 @@ async def get_service(
product_name=product_name,
)

return _db_to_api_model(service, access_rights)
service_manifest = await manifest.get_service(
key=service_key,
version=service_version,
director_client=director_api,
)

return _db_to_api_model(service, access_rights, service_manifest)


async def update_service(
repo: ServicesRepository,
director_api: DirectorApi,
*,
product_name: ProductName,
user_id: UserID,
Expand Down Expand Up @@ -229,6 +255,7 @@ async def update_service(

return await get_service(
repo=repo,
director_api=director_api,
product_name=product_name,
user_id=user_id,
service_key=service_key,
Expand Down
Loading

0 comments on commit 0c23386

Please sign in to comment.