wraptile 0.2.0.dev1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/.gitignore +6 -2
  2. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/PKG-INFO +3 -3
  3. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/pyproject.toml +3 -3
  4. wraptile-0.3.0/wraptile/ap_response.py +11 -0
  5. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/app.py +11 -1
  6. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/cli.py +2 -2
  7. wraptile-0.3.0/wraptile/dru_routes.py +158 -0
  8. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/airflow/airflow_service.py +7 -1
  9. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/base/service_base.py +11 -0
  10. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/local/local_service.py +5 -0
  11. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/LICENSE +0 -0
  12. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/README.md +0 -0
  13. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/__init__.py +0 -0
  14. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/constants.py +0 -0
  15. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/exceptions.py +0 -0
  16. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/logging.py +0 -0
  17. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/main.py +0 -0
  18. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/provider.py +0 -0
  19. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/py.typed +0 -0
  20. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/routes.py +0 -0
  21. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/__init__.py +0 -0
  22. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/airflow/__init__.py +0 -0
  23. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/airflow/tokens.py +0 -0
  24. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/base/__init__.py +0 -0
  25. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/local/__init__.py +0 -0
  26. {wraptile-0.2.0.dev1 → wraptile-0.3.0}/wraptile/services/local/testing.py +0 -0
@@ -1,9 +1,10 @@
1
1
  # Testing
2
2
  *.zarr/
3
3
  .cov-report
4
+ notebooks/auth-test.ipynb
4
5
 
5
- # Documentation generation
6
- docs/notebooks/*.ipynb
6
+ # Legacy generated documentation (excluded from the site)
7
+ docs/notebooks/
7
8
 
8
9
  # Byte-compiled / optimized / DLL files
9
10
  __pycache__/
@@ -202,3 +203,6 @@ cython_debug/
202
203
  # refer to https://docs.cursor.com/context/ignore-files
203
204
  .cursorignore
204
205
  .cursorindexingignore
206
+
207
+ # Downloaded font assets
208
+ docs/assets/fonts/*.ttf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wraptile
3
- Version: 0.2.0.dev1
3
+ Version: 0.3.0
4
4
  Summary: FastAPI server that implements the OGC API - Processes
5
5
  Project-URL: Documentation, https://eo-tools.github.io/eozilla
6
6
  Project-URL: Repository, https://github.com/eo-tools/eozilla
@@ -28,8 +28,8 @@ Classifier: Topic :: Software Development
28
28
  Classifier: Typing :: Typed
29
29
  Requires-Python: >=3.11
30
30
  Requires-Dist: fastapi
31
- Requires-Dist: gavicore>=0.2.0.dev1
32
- Requires-Dist: procodile>=0.2.0.dev1
31
+ Requires-Dist: gavicore>=0.3.0
32
+ Requires-Dist: procodile>=0.3.0
33
33
  Requires-Dist: pydantic
34
34
  Requires-Dist: pyyaml
35
35
  Requires-Dist: typer<1,>=0.26.0
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "wraptile"
7
- version = "0.2.0.dev1"
7
+ version = "0.3.0"
8
8
  description = "FastAPI server that implements the OGC API - Processes"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -46,8 +46,8 @@ dependencies = [
46
46
  "typer >=0.26.0,<1",
47
47
  "uvicorn",
48
48
  # local dependency
49
- "gavicore >=0.2.0.dev1",
50
- "procodile >=0.2.0.dev1",
49
+ "gavicore >=0.3.0",
50
+ "procodile >=0.3.0",
51
51
  ]
52
52
 
53
53
  [project.scripts]
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2026- by the Eozilla team and contributors
2
+ # Permissions are hereby granted under the terms of the Apache 2.0 License:
3
+ # https://opensource.org/license/apache-2-0.
4
+
5
+ from fastapi.responses import JSONResponse
6
+
7
+
8
+ class OgcApplicationPackageResponse(JSONResponse):
9
+ """Custom response class to correctly incorporate content type in response."""
10
+
11
+ media_type = "application/ogcapppkg+json"
@@ -4,6 +4,7 @@
4
4
 
5
5
  import logging
6
6
  import time
7
+ from contextlib import asynccontextmanager
7
8
  from typing import Awaitable, Callable
8
9
 
9
10
  from fastapi import FastAPI, Request, Response
@@ -11,8 +12,17 @@ from fastapi.middleware.cors import CORSMiddleware
11
12
  from fastapi.responses import JSONResponse
12
13
 
13
14
  from .exceptions import ServiceException
15
+ from .provider import get_service
14
16
 
15
- app = FastAPI()
17
+
18
+ @asynccontextmanager
19
+ async def load_app_eagerly(app: FastAPI):
20
+ get_service() # startup ...
21
+ yield # running ...
22
+ # shutdown ...
23
+
24
+
25
+ app = FastAPI(lifespan=load_app_eagerly)
16
26
  app.add_middleware(
17
27
  CORSMiddleware,
18
28
  allow_credentials=False, # we disallow Cookie-Auth (FastAPI default)
@@ -83,7 +83,7 @@ def new_cli(
83
83
  `wraptile` help text will be used
84
84
  summary: A one-sentence human-readable description of the tool that
85
85
  will be used by the default help text. Hence, used only,
86
- if `help`is not provided. Should end with a dot '.'.
86
+ if `help` is not provided. Should end with a dot '.'.
87
87
  version: Optional version string. If not provided, the
88
88
  `wraptile` version will be used.
89
89
  Return:
@@ -114,7 +114,7 @@ def new_cli(
114
114
  from wraptile import __version__ as default_version
115
115
 
116
116
  if version:
117
- typer.echo(f"{version} ({DEFAULT_NAME} {default_version})")
117
+ typer.echo(f"{name} {version} ({DEFAULT_NAME} {default_version})")
118
118
  else:
119
119
  typer.echo(default_version)
120
120
  return
@@ -0,0 +1,158 @@
1
+ # Copyright (c) 2026- by the Eozilla team and contributors
2
+ # Permissions are hereby granted under the terms of the Apache 2.0 License:
3
+ # https://opensource.org/license/apache-2-0.
4
+
5
+ import fastapi
6
+
7
+ from gavicore.dru_models import OgcApplicationPackage
8
+ from gavicore.dru_service import DruService
9
+ from gavicore.models import ApiError, ProcessSummary
10
+
11
+ from .ap_response import OgcApplicationPackageResponse
12
+ from .provider import get_service
13
+
14
+ dru_router = fastapi.APIRouter()
15
+
16
+
17
+ # noinspection PyPep8Naming
18
+ @dru_router.post(
19
+ "/processes",
20
+ response_model=ProcessSummary,
21
+ status_code=201,
22
+ responses={
23
+ "202": {},
24
+ "403": {"model": ApiError},
25
+ "409": {"model": ApiError},
26
+ "415": {"model": ApiError},
27
+ "501": {"model": ApiError},
28
+ },
29
+ response_model_exclude_none=True,
30
+ response_model_exclude_unset=True,
31
+ openapi_extra={
32
+ "requestBody": {
33
+ # NOTE: schemas of the request body below are kept abstract since swagger-ui resolves `$ref`,
34
+ # thus slowing down its web interface considerably.
35
+ # Querying the `openapi.json` directly does not result in slow downs.
36
+ "content": {
37
+ "application/cwl": {
38
+ "schema": {
39
+ "type": "object",
40
+ "additionalProperties": True,
41
+ }
42
+ },
43
+ "application/cwl+json": {
44
+ "schema": {
45
+ "type": "object",
46
+ "additionalProperties": True,
47
+ }
48
+ },
49
+ "application/cwl+yaml": {
50
+ "schema": {
51
+ "type": "object",
52
+ "additionalProperties": True,
53
+ }
54
+ },
55
+ },
56
+ "required": True,
57
+ }
58
+ },
59
+ )
60
+ async def deploy_process(
61
+ request: fastapi.Request,
62
+ response: fastapi.Response,
63
+ w: str | None = None,
64
+ service: DruService = fastapi.Depends(get_service), # noqa B008
65
+ ):
66
+ return await service.deploy_process(
67
+ w=w,
68
+ request=request,
69
+ response=response,
70
+ )
71
+
72
+
73
+ # noinspection PyPep8Naming
74
+ @dru_router.put(
75
+ "/processes/{processId}",
76
+ response_model=ProcessSummary,
77
+ responses={
78
+ "201": {"model": ProcessSummary},
79
+ "202": {"model": ProcessSummary},
80
+ "204": {},
81
+ "403": {"model": ApiError},
82
+ "404": {"model": ApiError},
83
+ "415": {"model": ApiError},
84
+ "501": {"model": ApiError},
85
+ },
86
+ response_model_exclude_none=True,
87
+ response_model_exclude_unset=True,
88
+ openapi_extra={
89
+ "requestBody": {
90
+ "content": {
91
+ "application/cwl": {"schema": ProcessSummary.model_json_schema()},
92
+ "application/cwl+json": {"schema": ProcessSummary.model_json_schema()},
93
+ "application/cwl+yaml": {"schema": ProcessSummary.model_json_schema()},
94
+ },
95
+ "required": True,
96
+ }
97
+ },
98
+ )
99
+ async def replace_process(
100
+ processId: str,
101
+ request: fastapi.Request,
102
+ response: fastapi.Response,
103
+ w: str | None = None,
104
+ service: DruService = fastapi.Depends(get_service), # noqa B008
105
+ ):
106
+ return await service.replace_process(
107
+ process_id=processId,
108
+ w=w,
109
+ request=request,
110
+ response=response,
111
+ )
112
+
113
+
114
+ # noinspection PyPep8Naming
115
+ @dru_router.delete(
116
+ "/processes/{processId}",
117
+ status_code=204,
118
+ responses={
119
+ "403": {"model": ApiError},
120
+ "404": {"model": ApiError},
121
+ "501": {"model": ApiError},
122
+ },
123
+ response_model_exclude_none=True,
124
+ response_model_exclude_unset=True,
125
+ )
126
+ async def undeploy_process(
127
+ processId: str,
128
+ request: fastapi.Request,
129
+ response: fastapi.Response,
130
+ service: DruService = fastapi.Depends(get_service), # noqa B008
131
+ ):
132
+ return await service.undeploy_process(
133
+ process_id=processId, request=request, response=response
134
+ )
135
+
136
+
137
+ # noinspection PyPep8Naming
138
+ @dru_router.get(
139
+ "/processes/{processId}/package",
140
+ response_model=OgcApplicationPackage,
141
+ response_class=OgcApplicationPackageResponse,
142
+ responses={
143
+ "403": {"model": ApiError},
144
+ "404": {"model": ApiError},
145
+ "501": {"model": ApiError},
146
+ },
147
+ response_model_exclude_none=True,
148
+ response_model_exclude_unset=True,
149
+ )
150
+ async def get_formal_description(
151
+ processId: str,
152
+ request: fastapi.Request,
153
+ response: fastapi.Response,
154
+ service: DruService = fastapi.Depends(get_service), # noqa B008
155
+ ):
156
+ return await service.get_formal_description(
157
+ process_id=processId, request=request, response=response
158
+ )
@@ -228,7 +228,13 @@ class AirflowService(ServiceBase):
228
228
  raise ServiceException(
229
229
  e.status, e.reason, exception=e, is_job_problem=True
230
230
  ) from e
231
- return self.dag_run_to_job_info(dag_run)
231
+ job_info: JobInfo = self.dag_run_to_job_info(dag_run)
232
+ # The OGC Api - Processes specifies that the dismiss operation (`DELETE /jobs/{jobId}`)
233
+ # must return a job info with state `"dismissed"`, no matter whether the job was successfully
234
+ # terminated or really already ended (which may take a while). So our interpretation of this
235
+ # status is a weaker "cancellation requested".
236
+ job_info.status = JobStatus.dismissed
237
+ return job_info
232
238
 
233
239
  async def get_job_results(self, job_id: str, *args, **kwargs) -> JobResults:
234
240
  dag_id = self.get_dag_id_from_job_id(job_id)
@@ -12,6 +12,7 @@ import fastapi
12
12
  import yaml
13
13
  from starlette.routing import Route
14
14
 
15
+ from gavicore.dru_service import DruService
15
16
  from gavicore.models import (
16
17
  Capabilities,
17
18
  ConformanceDeclaration,
@@ -92,6 +93,16 @@ class ServiceBase(Service, ABC):
92
93
  name="service",
93
94
  example="path.to.module:service",
94
95
  )
96
+
97
+ if issubclass(service.__class__, DruService):
98
+ from wraptile.app import app as app_module
99
+ from wraptile.dru_routes import dru_router
100
+
101
+ app_module.include_router(dru_router)
102
+
103
+ logging.getLogger("uvicorn").info(
104
+ "Loaded additional routes to support Deploy, Redeploy, Undeploy"
105
+ )
95
106
  except (ValueError, TypeError) as e:
96
107
  raise ServiceConfigException(f"{e}") from e
97
108
  logger = logging.getLogger("uvicorn")
@@ -162,6 +162,11 @@ class LocalService(ServiceBase):
162
162
  del self.jobs[job_id]
163
163
  self.job_results.pop(job_id, None)
164
164
  self.job_uses_processes.pop(job_id, None)
165
+ # The OGC Api - Processes specifies that the dismiss operation (`DELETE /jobs/{jobId}`)
166
+ # must return a job info with state `"dismissed"`, no matter whether the job was successfully
167
+ # terminated or really already ended (which may take a while). So our interpretation of this
168
+ # status is a weaker "cancellation requested".
169
+ job.job_info.status = JobStatus.dismissed
165
170
  return job.job_info
166
171
 
167
172
  async def get_job_results(self, job_id: str, *args, **_kwargs) -> JobResults:
File without changes
File without changes
File without changes