Skip to content

Commit

Permalink
Update hexkit and use manual DI
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Oct 27, 2023
1 parent a17e0c1 commit 214b92a
Show file tree
Hide file tree
Showing 26 changed files with 661 additions and 414 deletions.
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "wps"
version = "0.1.5"
version = "1.0.0"
description = "Work Package Service"
readme = "README.md"
authors = [
Expand All @@ -23,10 +23,9 @@ classifiers = [
"Intended Audience :: Developers",
]
dependencies = [
"ghga-event-schemas==0.13.5",
"ghga-service-commons[api,auth,crypt]>=0.5.0",
"hexkit[akafka,mongodb]>=0.10.2",
"httpx>=0.23.3",
"ghga-event-schemas~=1.0.0",
"ghga-service-commons[api,auth,crypt]>=1.0.1",
"hexkit[akafka,mongodb]>=0.11.0",
"typer>=0.7.0",
]

Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
-r requirements-dev-common.in

# additional requirements can be listed here
testcontainers[kafka,mongo]>=3.4.1
pytest_httpx>=0.21.3
testcontainers[kafka,mongo]>=3.4.1
223 changes: 161 additions & 62 deletions requirements-dev.txt

Large diffs are not rendered by default.

206 changes: 151 additions & 55 deletions requirements.txt

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions scripts/script_utils/fastapi_app_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@
from typing import Any

from fastapi import FastAPI
from ghga_service_commons.api import ApiConfigBase

from wps.adapters.inbound.fastapi_.openapi import get_openapi_schema
from wps.adapters.inbound.fastapi_.configure import get_openapi_schema
from wps.adapters.inbound.fastapi_.routes import router

app = FastAPI()
app.include_router(router)

CONFIG = ApiConfigBase() # pyright: ignore


def custom_openapi() -> dict[str, Any]:
"""Get custom OpenAPI schema."""
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi_schema(app)
openapi_schema = get_openapi_schema(app, config=CONFIG)
app.openapi_schema = openapi_schema
return app.openapi_schema

Expand Down
2 changes: 1 addition & 1 deletion scripts/update_config_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def get_example() -> str:
normalized_config_dict = json.loads(
config.json() # change eventually to .model_dump_json()
)
return yaml.dump(normalized_config_dict) # pyright: ignore
return yaml.dump(normalized_config_dict)


def update_docs():
Expand Down
6 changes: 3 additions & 3 deletions scripts/update_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ def get_package_details() -> PackageDetails:
description = read_package_description()
config_description = generate_config_docs()
return PackageDetails(
**header.dict(),
**name.dict(),
**header.model_dump(),
**name.model_dump(),
description=description,
config_description=config_description,
design_description=read_design_description(),
Expand All @@ -195,7 +195,7 @@ def generate_single_readme(*, details: PackageDetails) -> str:

template_content = README_TEMPLATE_PATH.read_text()
template = Template(template_content)
return template.substitute(details.dict())
return template.substitute(details.model_dump())


def main(check: bool = False) -> None:
Expand Down
9 changes: 5 additions & 4 deletions src/wps/adapters/inbound/event_sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
from ghga_event_schemas.validation import get_validated_payload
from hexkit.custom_types import Ascii, JsonObject
from hexkit.protocols.eventsub import EventSubscriberProtocol
from pydantic import BaseSettings, Field
from pydantic import Field
from pydantic_settings import BaseSettings

from wps.core.models import Dataset, DatasetFile, WorkType
from wps.ports.inbound.repository import WorkPackageRepositoryPort
Expand All @@ -36,17 +37,17 @@ class EventSubTranslatorConfig(BaseSettings):
dataset_change_event_topic: str = Field(
...,
description="Name of the topic for events that inform about datasets.",
example="metadata_datasets",
examples=["metadata_datasets"],
)
dataset_upsertion_event_type: str = Field(
...,
description="The type of events that inform about new and changed datasets.",
example="dataset_created",
examples=["dataset_created"],
)
dataset_deletion_event_type: str = Field(
...,
description="The type of events that inform about deleted datasets.",
example="dataset_deleted",
examples=["dataset_deleted"],
)


Expand Down
24 changes: 13 additions & 11 deletions src/wps/adapters/inbound/fastapi_/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@

"""Helper dependencies for requiring authentication and authorization."""

from dependency_injector.wiring import Provide, inject
from typing import Annotated

from fastapi import Depends, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from ghga_service_commons.auth.context import AuthContextProtocol
from ghga_service_commons.auth.ghga import AuthContext, is_active
from ghga_service_commons.auth.policies import require_auth_context_using_credentials

from wps.container import Container
from wps.adapters.inbound.fastapi_.dummies import auth_provider

__all__ = ["requires_auth_context", "requires_work_package_access_token"]
__all__ = ["RequiresAuthContext", "RequiresWorkPackageAccessToken"]


@inject
async def require_active_context(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=True)),
auth_provider: AuthContextProtocol[AuthContext] = Depends(
Provide[Container.auth_provider]
),
credentials: Annotated[
HTTPAuthorizationCredentials, Depends(HTTPBearer(auto_error=True))
],
auth_provider: Annotated[AuthContextProtocol[AuthContext], Depends(auth_provider)],
) -> AuthContext:
"""Require an active GHGA auth context using FastAPI."""
return await require_auth_context_using_credentials(
Expand All @@ -42,14 +42,16 @@ async def require_active_context(


async def require_access_token(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=True)),
credentials: Annotated[
HTTPAuthorizationCredentials, Depends(HTTPBearer(auto_error=True))
],
) -> str:
"""Require an access token using FastAPI."""
return credentials.credentials


# policy that requires (and returns) an active auth context
requires_auth_context = Security(require_active_context)
RequiresAuthContext = Annotated[AuthContext, Security(require_active_context)]

# policy that requires (and returns) a work package access token
requires_work_package_access_token = Security(require_access_token)
RequiresWorkPackageAccessToken = Annotated[str, Security(require_access_token)]
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,45 @@
# limitations under the License.
#

"""Utils to customize the OpenAPI script"""
"""Utils to configure the FastAPI app"""

from typing import Any

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from ghga_service_commons.api import ApiConfigBase, configure_app

from wps import __version__
from wps.config import Config
from wps.adapters.inbound.fastapi_.routes import router

__all__ = ["get_openapi_schema"]


def get_openapi_schema(api) -> dict[str, Any]:
def get_openapi_schema(app: FastAPI, config: ApiConfigBase) -> dict[str, Any]:
"""Generate a custom OpenAPI schema for the service."""
config = Config() # type: ignore

return get_openapi(
title="Work Package Service",
version=__version__,
description="A service managing work packages for the GHGA CLI",
servers=[{"url": config.api_root_path}],
tags=[{"name": "WorkPackages"}, {"name": "Datasets"}],
routes=api.routes,
routes=app.routes,
)


def get_configured_app(*, config: ApiConfigBase) -> FastAPI:
"""Create and configure a FastAPI application."""
app = FastAPI()
app.include_router(router)
configure_app(app, config=config)

def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi_schema(app, config=config)
app.openapi_schema = openapi_schema
return app.openapi_schema

app.openapi = custom_openapi # type: ignore[method-assign]

return app
37 changes: 37 additions & 0 deletions src/wps/adapters/inbound/fastapi_/dummies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""A collection of dependency dummies.
These dummies are used in path operation definitions, but at runtime they need
to be replaced with actual dependencies.
"""

from typing import Annotated

from fastapi import Depends
from ghga_service_commons.api.di import DependencyDummy

from wps.ports.inbound.repository import WorkPackageRepositoryPort

__all__ = ["auth_provider", "work_package_repo_port", "WorkPackageRepositoryDummy"]

auth_provider = DependencyDummy("auth_provider")

work_package_repo_port = DependencyDummy("work_package_repo_port")
WorkPackageRepositoryDummy = Annotated[
WorkPackageRepositoryPort, Depends(work_package_repo_port)
]
59 changes: 16 additions & 43 deletions src/wps/adapters/inbound/fastapi_/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,19 @@

import logging

from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, HTTPException, Path, status
from fastapi import APIRouter, HTTPException, status

from wps.adapters.inbound.fastapi_.auth import (
AuthContext,
requires_auth_context,
requires_work_package_access_token,
RequiresAuthContext,
RequiresWorkPackageAccessToken,
)
from wps.container import Container
from wps.adapters.inbound.fastapi_.dummies import WorkPackageRepositoryDummy
from wps.core.models import (
Dataset,
WorkPackageCreationData,
WorkPackageCreationResponse,
WorkPackageDetails,
)
from wps.ports.inbound.repository import WorkPackageRepositoryPort

__all__ = ["router"]

Expand Down Expand Up @@ -69,13 +66,10 @@ async def health():
},
status_code=201,
)
@inject
async def create_work_package(
creation_data: WorkPackageCreationData,
repository: WorkPackageRepositoryPort = Depends(
Provide[Container.work_package_repository]
),
auth_context: AuthContext = requires_auth_context,
repository: WorkPackageRepositoryDummy,
auth_context: RequiresAuthContext,
) -> WorkPackageCreationResponse:
"""Create a work package using an internal auth token with a user context."""
try:
Expand All @@ -102,16 +96,10 @@ async def create_work_package(
},
status_code=200,
)
@inject
async def get_work_package(
work_package_id: str = Path(
...,
alias="work_package_id",
),
repository: WorkPackageRepositoryPort = Depends(
Provide[Container.work_package_repository]
),
work_package_access_token: str = requires_work_package_access_token,
work_package_id: str,
repository: WorkPackageRepositoryDummy,
work_package_access_token: RequiresWorkPackageAccessToken,
) -> WorkPackageDetails:
"""Get work package details using a work package access token."""
try:
Expand Down Expand Up @@ -147,20 +135,11 @@ async def get_work_package(
},
status_code=201,
)
@inject
async def create_work_order_token(
work_package_id: str = Path(
...,
alias="work_package_id",
),
file_id: str = Path(
...,
alias="file_id",
),
repository: WorkPackageRepositoryPort = Depends(
Provide[Container.work_package_repository]
),
work_package_access_token: str = requires_work_package_access_token,
work_package_id: str,
file_id: str,
repository: WorkPackageRepositoryDummy,
work_package_access_token: RequiresWorkPackageAccessToken,
) -> str:
"""Get an encrypted work order token using a work package access token."""
try:
Expand Down Expand Up @@ -193,16 +172,10 @@ async def create_work_order_token(
},
status_code=200,
)
@inject
async def get_datasets(
user_id: str = Path(
...,
alias="user_id",
),
repository: WorkPackageRepositoryPort = Depends(
Provide[Container.work_package_repository]
),
auth_context: AuthContext = requires_auth_context,
user_id: str,
repository: WorkPackageRepositoryDummy,
auth_context: RequiresAuthContext,
) -> list[Dataset]:
"""Get datasets using an internal auth token with a user context."""
try:
Expand Down
5 changes: 3 additions & 2 deletions src/wps/adapters/outbound/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from contextlib import asynccontextmanager

import httpx
from pydantic import BaseSettings, Field
from pydantic import Field
from pydantic_settings import BaseSettings

from wps.ports.outbound.access import AccessCheckPort

Expand All @@ -34,7 +35,7 @@ class AccessCheckConfig(BaseSettings):

download_access_url: str = Field(
...,
example="http://127.0.0.1/download-access",
examples=["http://127.0.0.1/download-access"],
description="URL pointing to the internal download access API.",
)

Expand Down
Loading

0 comments on commit 214b92a

Please sign in to comment.