Skip to content
4 changes: 2 additions & 2 deletions docs/gavicore/models/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@

## `gavicore.models` - OGC Application Package and Workflow descriptions

::: gavicore.dru_models.OGCApplicationPackage
::: gavicore.dru_models.OgcApplicationPackage

::: gavicore.dru_models.OGCApplicationPackageProcessDescription
::: gavicore.dru_models.OgcApplicationPackageProcessDescription

::: gavicore.dru_models.CWLDescription

Expand Down
2 changes: 1 addition & 1 deletion docs/gavicore/service/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

::: gavicore.service.Service

::: gavicore.dru_service.DRUService
::: gavicore.dru_service.DruService
14 changes: 7 additions & 7 deletions gavicore/src/gavicore/dru_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# ---------------------------------------------------------------------


class OGCApplicationPackage(BaseModel):
class OgcApplicationPackage(BaseModel):
"""
An OGC Application Package is a document that describes a process in
sufficient detail so that an implementation of this Standard can
Expand All @@ -25,25 +25,25 @@ class OGCApplicationPackage(BaseModel):
For more information, see: /req/ogcapppkg/schema
"""

process_description: OGCApplicationPackageProcessDescription | None = Field(
process_description: OgcApplicationPackageProcessDescription | None = Field(
None, alias="processDescription"
)
"""Process description of a given process."""

execution_uni: ExecutionUnitBase | list[ExecutionUnitBase] = Field(
execution_unit: ExecutionUnitBase | list[ExecutionUnitBase] = Field(
alias="executionUnit"
)
"""The execution unit of process."""


class OGCApplicationPackageProcessDescription(BaseModel):
class OgcApplicationPackageProcessDescription(BaseModel):
"""Wrapper around `ProcessDescription` to insert additional field name."""

process: ProcessDescription | None = None
"""The process description."""


class CWLDescription(BaseModel):
class CwlDescription(BaseModel):
"""
Possible encoding of an execution unit as CWL.
"""
Expand Down Expand Up @@ -202,11 +202,11 @@ class GenericExecutionUnit(BaseModel):


ExecutionUnitBase: TypeAlias = (
Link | CWLDescription | ContainerImage | GenericExecutionUnit
Link | CwlDescription | ContainerImage | GenericExecutionUnit
)
"""Execution unit encoding of a process."""

ContainerBindings.model_rebuild()
ExecutionUnitContainer.model_rebuild()
ContainerImage.model_rebuild()
OGCApplicationPackage.model_rebuild()
OgcApplicationPackage.model_rebuild()
8 changes: 4 additions & 4 deletions gavicore/src/gavicore/dru_service.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from abc import ABC, abstractmethod
from typing import Optional

from .dru_models import OGCApplicationPackage
from .dru_models import OgcApplicationPackage
from .models import ProcessSummary
from .service import Service


class DRUService(Service, ABC):
"""The DRUService interface extends the Service interface by providing
class DruService(Service, ABC):
"""The DruService interface extends the Service interface by providing
four endpoints defined per
[OGC API - Processes — Part 2 (DRU)](https://docs.ogc.org/DRAFTS/20-044.html)."""

Expand Down Expand Up @@ -70,7 +70,7 @@ async def undeploy_process(self, process_id: str, *args, **kwargs) -> None:
@abstractmethod
async def get_formal_description(
self, process_id: str, *args, **kwargs
) -> OGCApplicationPackage:
) -> OgcApplicationPackage:
"""Retrieve a formal description of a previously deployed process
via the deploy operation.
The returned description relates to the most recent deployment.
Expand Down
6 changes: 3 additions & 3 deletions gavicore/tests/test_dru_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import gavicore.dru_models as m

REQUIRED_CLASSES = {
"OGCApplicationPackage",
"OGCApplicationPackageProcessDescription",
"CWLDescription",
"OgcApplicationPackage",
"OgcApplicationPackageProcessDescription",
"CwlDescription",
"ContainerImage",
"ExecutionUnitContainer",
"ContainerConfig",
Expand Down
4 changes: 2 additions & 2 deletions gavicore/tests/test_dru_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import inspect
from unittest import TestCase

from gavicore.dru_service import DRUService
from gavicore.dru_service import DruService

from .test_service import REQUIRED_METHODS as REQUIRED_SERVICE_METHODS

Expand All @@ -22,6 +22,6 @@
class DRUServiceTest(TestCase):
def test_methods(self):
all_method_names = set(
name for name, obj in inspect.getmembers(DRUService, inspect.isfunction)
name for name, obj in inspect.getmembers(DruService, inspect.isfunction)
)
self.assertSetEqual(REQUIRED_DRU_METHODS, set(all_method_names))
7 changes: 7 additions & 0 deletions wraptile/src/wraptile/ap_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi.responses import JSONResponse


class OgcApplicationPackageResponse(JSONResponse):
"""Custom response class to correctly incorporate content type in response."""

media_type = "application/ogcapppkg+json"
12 changes: 11 additions & 1 deletion wraptile/src/wraptile/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@

import logging
import time
from contextlib import asynccontextmanager
from typing import Awaitable, Callable

from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from .exceptions import ServiceException
from .provider import get_service

app = FastAPI()

@asynccontextmanager
async def load_app_eagerly(app: FastAPI):
get_service() # startup ...
yield # running ...
# shutdown ...


app = FastAPI(lifespan=load_app_eagerly)
app.add_middleware(
CORSMiddleware,
allow_credentials=False, # we disallow Cookie-Auth (FastAPI default)
Expand Down
155 changes: 155 additions & 0 deletions wraptile/src/wraptile/dru_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import fastapi

from gavicore.dru_models import OgcApplicationPackage
from gavicore.dru_service import DruService
from gavicore.models import ApiError, ProcessSummary

from .ap_response import OgcApplicationPackageResponse
from .provider import get_service

dru_router = fastapi.APIRouter()


# noinspection PyPep8Naming
@dru_router.post(
"/processes",
response_model=ProcessSummary,
status_code=201,
responses={
"202": {},
"403": {"model": ApiError},
"409": {"model": ApiError},
"415": {"model": ApiError},
"501": {"model": ApiError},
},
response_model_exclude_none=True,
response_model_exclude_unset=True,
openapi_extra={
"requestBody": {
# NOTE: schemas of the request body below are kept abstract since swagger-ui resolves `$ref`,
# thus slowing down its web interface considerably.
# Querying the `openapi.json` directly does not result in slow downs.
"content": {
"application/cwl": {
"schema": {
"type": "object",
"additionalProperties": True,
}
},
"application/cwl+json": {
"schema": {
"type": "object",
"additionalProperties": True,
}
},
"application/cwl+yaml": {
"schema": {
"type": "object",
"additionalProperties": True,
}
},
},
"required": True,
}
},
)
async def deploy_process(
request: fastapi.Request,
response: fastapi.Response,
w: str | None = None,
service: DruService = fastapi.Depends(get_service), # noqa B008
):
return await service.deploy_process(
w=w,
request=request,
response=response,
)


# noinspection PyPep8Naming
@dru_router.put(
"/processes/{processId}",
response_model=ProcessSummary,
responses={
"201": {"model": ProcessSummary},
"202": {"model": ProcessSummary},
"204": {},
"403": {"model": ApiError},
"404": {"model": ApiError},
"415": {"model": ApiError},
# NOTE: use 501 for parts of the standard that are not implemented (yet)
"501": {"model": ApiError},
},
response_model_exclude_none=True,
response_model_exclude_unset=True,
openapi_extra={
"requestBody": {
"content": {
"application/cwl": {"schema": ProcessSummary.model_json_schema()},
"application/cwl+json": {"schema": ProcessSummary.model_json_schema()},
"application/cwl+yaml": {"schema": ProcessSummary.model_json_schema()},
},
"required": True,
}
},
)
async def replace_process(
processId: str,
request: fastapi.Request,
response: fastapi.Response,
w: str | None = None,
service: DruService = fastapi.Depends(get_service), # noqa B008
):
return await service.replace_process(
process_id=processId,
w=w,
request=request,
response=response,
)


# noinspection PyPep8Naming
@dru_router.delete(
"/processes/{processId}",
status_code=204,
responses={
"403": {"model": ApiError},
"404": {"model": ApiError},
"501": {"model": ApiError},
},
response_model_exclude_none=True,
response_model_exclude_unset=True,
)
async def undeploy_process(
processId: str,
request: fastapi.Request,
response: fastapi.Response,
service: DruService = fastapi.Depends(get_service), # noqa B008
):
return await service.undeploy_process(
process_id=processId, request=request, response=response
)


# noinspection PyPep8Naming
@dru_router.get(
"/processes/{processId}/package",
response_model=OgcApplicationPackage,
response_class=OgcApplicationPackageResponse,
responses={
"403": {"model": ApiError},
"404": {"model": ApiError},
"501": {"model": ApiError},
},
response_model_exclude_none=True,
response_model_exclude_unset=True,
)
async def get_formal_description(
processId: str,
request: fastapi.Request,
response: fastapi.Response,
service: DruService = fastapi.Depends(get_service), # noqa B008
):
return await service.get_formal_description(
process_id=processId, request=request, response=response
)
20 changes: 20 additions & 0 deletions wraptile/src/wraptile/services/base/service_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import os
import shlex
from abc import ABC
from importlib import import_module
from typing import Optional

import fastapi
import yaml
from starlette.routing import Route

from gavicore.dru_service import DruService
from gavicore.models import (
Capabilities,
ConformanceDeclaration,
Expand Down Expand Up @@ -92,6 +94,24 @@ def load(cls) -> "ServiceBase":
name="service",
example="path.to.module:service",
)

if issubclass(service.__class__, DruService):
try:
app_module = import_module("wraptile.app")
app: fastapi.FastAPI = getattr(app_module, "app")

route_module = import_module("wraptile.dru_routes")
router: fastapi.APIRouter = getattr(route_module, "dru_router")
except AttributeError:
raise ServiceConfigException(
"Unable to load additional DRU routes"
) from None
else:
app.include_router(router)

logging.getLogger("uvicorn").info(
"Loaded additional routes to support Deploy, Redeploy, Undeploy"
)
except (ValueError, TypeError) as e:
raise ServiceConfigException(f"{e}") from e
logger = logging.getLogger("uvicorn")
Expand Down
15 changes: 15 additions & 0 deletions wraptile/tests/test_ap_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from unittest import TestCase

from fastapi.responses import JSONResponse

from wraptile.ap_response import OgcApplicationPackageResponse


class OgcApplicationPackageResponseTest(TestCase):
def test_subclasses_json_response(self):
self.assertTrue(issubclass(OgcApplicationPackageResponse, JSONResponse))

def test_media_type(self):
self.assertEqual(
OgcApplicationPackageResponse.media_type, "application/ogcapppkg+json"
)