rs-dpr-service 0.2a13__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.
- rs_dpr_service-0.2a13/PKG-INFO +49 -0
- rs_dpr_service-0.2a13/README.md +2 -0
- rs_dpr_service-0.2a13/pyproject.toml +132 -0
- rs_dpr_service-0.2a13/rs_dpr_service/__init__.py +13 -0
- rs_dpr_service-0.2a13/rs_dpr_service/jobs_table.py +83 -0
- rs_dpr_service-0.2a13/rs_dpr_service/main.py +284 -0
- rs_dpr_service-0.2a13/rs_dpr_service/opentelemetry.py +141 -0
- rs_dpr_service-0.2a13/rs_dpr_service/processors.py +737 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: rs-dpr-service
|
|
3
|
+
Version: 0.2a13
|
|
4
|
+
Summary: Copernicus Sentinel rs-server, DPR as a service.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: stac,copernicus,sentinel,processing
|
|
7
|
+
Author: ESA under contract with Airbus and CS
|
|
8
|
+
Author-email: ops_coprs@airbus.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Framework :: OpenTelemetry
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
|
+
Requires-Dist: SQLAlchemy (>=2.0.37,<3.0.0)
|
|
23
|
+
Requires-Dist: bandit (>=1.8.3,<2.0.0)
|
|
24
|
+
Requires-Dist: dask-gateway (==2024.1.0)
|
|
25
|
+
Requires-Dist: dask[distributed] (==2024.5.2)
|
|
26
|
+
Requires-Dist: fastapi (>=0.115.12,<0.116.0)
|
|
27
|
+
Requires-Dist: flake8-pyproject (>=1.2.3,<2.0.0)
|
|
28
|
+
Requires-Dist: flask (>=3.1.1,<4.0.0)
|
|
29
|
+
Requires-Dist: geoalchemy2 (>=0.17.1,<0.18.0)
|
|
30
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
31
|
+
Requires-Dist: jsonpatch (>=1.33,<2.0)
|
|
32
|
+
Requires-Dist: mypy (>=1.15.0,<2.0.0)
|
|
33
|
+
Requires-Dist: openapi-core (>=0.19.5,<0.20.0)
|
|
34
|
+
Requires-Dist: opentelemetry-distro (>=0.53b1,<0.54)
|
|
35
|
+
Requires-Dist: opentelemetry-exporter-otlp (>=1.32.1,<2.0.0)
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-aws-lambda (>=0.53b1,<0.54)
|
|
37
|
+
Requires-Dist: opentelemetry-test-utils (>=0.53b1,<0.54)
|
|
38
|
+
Requires-Dist: psycopg2 (>=2.9.10,<3.0.0)
|
|
39
|
+
Requires-Dist: pygeoapi (>=0.21.dev0)
|
|
40
|
+
Requires-Dist: pylint (>=3.3.6,<4.0.0)
|
|
41
|
+
Requires-Dist: uvicorn (>=0.34.1,<0.35.0)
|
|
42
|
+
Project-URL: Documentation, https://home.rs-python.eu/rs-documentation/rs-server/docs/doc/
|
|
43
|
+
Project-URL: Homepage, https://home.rs-python.eu/
|
|
44
|
+
Project-URL: Repository, https://github.com/RS-PYTHON/rs-dpr-service
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# rs-dpr-service
|
|
48
|
+
A service responsible for triggering the DPR processor execution within the Dask cluster.
|
|
49
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rs-dpr-service"
|
|
3
|
+
dynamic = ["version"] # see: https://github.com/mtkennerly/poetry-dynamic-versioning
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "rs-dpr-service"
|
|
7
|
+
version = "0.2a13"
|
|
8
|
+
description = "Copernicus Sentinel rs-server, DPR as a service."
|
|
9
|
+
license = "Apache-2.0"
|
|
10
|
+
authors = ["ESA under contract with Airbus and CS <ops_coprs@airbus.com>"]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
homepage = "https://home.rs-python.eu/"
|
|
13
|
+
repository = "https://github.com/RS-PYTHON/rs-dpr-service"
|
|
14
|
+
documentation = "https://home.rs-python.eu/rs-documentation/rs-server/docs/doc/"
|
|
15
|
+
keywords = ["stac", "copernicus", "sentinel", "processing"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Framework :: OpenTelemetry",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: Apache Software License",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Image Processing"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
packages = [
|
|
29
|
+
{ include = "rs_dpr_service", from = "."},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Add configuration files to the .whl package
|
|
33
|
+
include = [
|
|
34
|
+
{ path = "config", format = ["wheel"] }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.poetry.dependencies]
|
|
38
|
+
python = "^3.11"
|
|
39
|
+
pygeoapi = ">=0.21.dev0"
|
|
40
|
+
flask = "^3.1.1" # for pygeoapi
|
|
41
|
+
uvicorn = ">=0.34.1,<0.35.0"
|
|
42
|
+
fastapi = ">=0.115.12,<0.116.0"
|
|
43
|
+
geoalchemy2 = ">=0.17.1,<0.18.0"
|
|
44
|
+
SQLAlchemy = "^2.0.37"
|
|
45
|
+
jsonpatch = "^1.33"
|
|
46
|
+
dask-gateway = "2024.1.0"
|
|
47
|
+
# same dask version as in the cluster i.e. same as eopf
|
|
48
|
+
dask = {extras = ["distributed"], version = "2024.5.2"}
|
|
49
|
+
openapi-core = "^0.19.5"
|
|
50
|
+
bandit = "^1.8.3"
|
|
51
|
+
mypy = "^1.15.0"
|
|
52
|
+
pylint = "^3.3.6"
|
|
53
|
+
flake8-pyproject = "^1.2.3"
|
|
54
|
+
httpx = "^0.28.1"
|
|
55
|
+
psycopg2 = "^2.9.10"
|
|
56
|
+
opentelemetry-distro = "^0.53b1" # then run 'poetry run opentelemetry-bootstrap -a install'
|
|
57
|
+
opentelemetry-instrumentation-aws-lambda = "^0.53b1"
|
|
58
|
+
opentelemetry-test-utils = "^0.53b1"
|
|
59
|
+
opentelemetry-exporter-otlp = "^1.32.1"
|
|
60
|
+
|
|
61
|
+
[tool.poetry.group.dev]
|
|
62
|
+
optional = true
|
|
63
|
+
|
|
64
|
+
[tool.poetry.group.dev.dependencies]
|
|
65
|
+
python = "^3.11"
|
|
66
|
+
pygeoapi = { git = "https://github.com/geopython/pygeoapi.git", branch = "master" }
|
|
67
|
+
pre-commit = ">=3.7.1,<5.0.0"
|
|
68
|
+
pylint = "^3.3.2"
|
|
69
|
+
pytest = "^8.3.3"
|
|
70
|
+
pytest-asyncio = ">=0.24,<0.25"
|
|
71
|
+
pytest-cov = ">=5,<7"
|
|
72
|
+
pytest-docker = "^3.1.0"
|
|
73
|
+
pytest-error-for-skips = "^2.0.2"
|
|
74
|
+
pytest-mock = "^3.14.0"
|
|
75
|
+
responses = "^0.25.3"
|
|
76
|
+
safety = "^3.5.1"
|
|
77
|
+
|
|
78
|
+
[tool.black]
|
|
79
|
+
line-length = 120
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
markers = [
|
|
83
|
+
"unit", # mark a test as unittest
|
|
84
|
+
"integration", # mark a test as integration test
|
|
85
|
+
]
|
|
86
|
+
asyncio_mode = "auto"
|
|
87
|
+
|
|
88
|
+
[tool.mypy]
|
|
89
|
+
warn_redundant_casts = true
|
|
90
|
+
# Force analyze of partially annotate function signatures
|
|
91
|
+
check_untyped_defs = true
|
|
92
|
+
ignore_missing_imports = true
|
|
93
|
+
|
|
94
|
+
[tool.flake8]
|
|
95
|
+
max-line-length = 120
|
|
96
|
+
# per-file-ignores = [
|
|
97
|
+
# '__init__.py:F401',
|
|
98
|
+
# ]
|
|
99
|
+
|
|
100
|
+
[tool.pylint]
|
|
101
|
+
max-line-length = 120
|
|
102
|
+
disable = [
|
|
103
|
+
"logging-fstring-interpolation", # Logging f-string interpolation
|
|
104
|
+
"R0917", # Too many positional arguments
|
|
105
|
+
"E0401", # import-error
|
|
106
|
+
"R0902", # too-many-instance-attributes
|
|
107
|
+
"R0913" # too-many-arguments
|
|
108
|
+
]
|
|
109
|
+
ignore-paths = '^docs/.*$'
|
|
110
|
+
|
|
111
|
+
# Only add notes as comments that are not checked for errors.
|
|
112
|
+
notes = ["XXX"] # No errors on TODO and FIXME
|
|
113
|
+
|
|
114
|
+
min-similarity-lines = 10 # Minimum lines number of a similarity
|
|
115
|
+
|
|
116
|
+
[tool.bandit.assert_used]
|
|
117
|
+
skips = ['*/*.py']
|
|
118
|
+
|
|
119
|
+
[tool.bandit]
|
|
120
|
+
skips = ['B603', 'B607', 'B404']
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
[tool.poetry.requires-plugins]
|
|
124
|
+
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
|
125
|
+
|
|
126
|
+
[tool.poetry-dynamic-versioning]
|
|
127
|
+
enable = false
|
|
128
|
+
metadata = false
|
|
129
|
+
|
|
130
|
+
[build-system]
|
|
131
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
132
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2024 CS Group
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright 2024 CS Group
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Module used to implement abstract model of an SQLAlchemy table."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import enum
|
|
20
|
+
from threading import Lock
|
|
21
|
+
|
|
22
|
+
from pygeoapi.util import JobStatus
|
|
23
|
+
from sqlalchemy import Column, DateTime, Enum, Integer, String, func, orm
|
|
24
|
+
from sqlalchemy.orm import declarative_base
|
|
25
|
+
|
|
26
|
+
# Construct a sqlalchemy base class for declarative class definitions.
|
|
27
|
+
Base = declarative_base()
|
|
28
|
+
|
|
29
|
+
# pylint: disable=attribute-defined-outside-init
|
|
30
|
+
# mypy: ignore-errors
|
|
31
|
+
# Ignore pylint and mypy false positive errors on sqlalchemy
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JobType(enum.Enum):
|
|
35
|
+
"""
|
|
36
|
+
Enum for the job type specified in OGC API Processes specification
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# From the specification
|
|
40
|
+
process = "process" # pylint: disable=invalid-name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JobsTable(Base): # pylint: disable=too-few-public-methods
|
|
44
|
+
"""
|
|
45
|
+
Abstract implementation of SQLAlchemy Base
|
|
46
|
+
|
|
47
|
+
Must be kept in line with
|
|
48
|
+
https://github.com/geopython/pygeoapi/blob/master/tests/data/postgres_manager_full_structure.backup.sql
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
__tablename__ = "jobs"
|
|
52
|
+
|
|
53
|
+
type = Column(Enum(JobType), nullable=False, server_default=JobType.process.value)
|
|
54
|
+
identifier = Column(String, primary_key=True, unique=True, index=True)
|
|
55
|
+
processID = Column(String, nullable=False)
|
|
56
|
+
status = Column(Enum(JobStatus), nullable=False)
|
|
57
|
+
progress = Column(Integer, server_default="0", nullable=False)
|
|
58
|
+
# Pylint issue with func.now, check this: https://github.com/sqlalchemy/sqlalchemy/issues/9189
|
|
59
|
+
created = Column(DateTime, server_default=func.now()) # pylint: disable=not-callable
|
|
60
|
+
started = Column(DateTime, server_default=func.now()) # pylint: disable=not-callable
|
|
61
|
+
finished = Column(DateTime) # pylint: disable=not-callable
|
|
62
|
+
# onupdate=func.now(), server_onupdate=func.now() is not working, did not figure why
|
|
63
|
+
# instead, force the PostgreSQLManager from pygeoapi to update the updated column specifically with
|
|
64
|
+
# update_job function (check processors.py log_job_execution function)
|
|
65
|
+
updated = Column(
|
|
66
|
+
DateTime,
|
|
67
|
+
server_default=func.now(), # pylint: disable=not-callable
|
|
68
|
+
onupdate=func.now(), # pylint: disable=not-callable
|
|
69
|
+
server_onupdate=func.now(), # pylint: disable=not-callable
|
|
70
|
+
)
|
|
71
|
+
location = Column(String)
|
|
72
|
+
mimetype = Column(String)
|
|
73
|
+
message = Column(String)
|
|
74
|
+
|
|
75
|
+
def __init__(self, *args, **kwargs):
|
|
76
|
+
"""Invoked when creating a new record in the database table."""
|
|
77
|
+
super().__init__(*args, **kwargs)
|
|
78
|
+
self.lock = Lock()
|
|
79
|
+
|
|
80
|
+
@orm.reconstructor
|
|
81
|
+
def init_on_load(self):
|
|
82
|
+
"""Invoked when retrieving an existing record from the database table."""
|
|
83
|
+
self.lock = Lock()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Copyright 2024 CS Group
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""rs dpr service main module."""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import pathlib
|
|
20
|
+
from contextlib import asynccontextmanager
|
|
21
|
+
from string import Template
|
|
22
|
+
from time import sleep
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
# from dask.distributed import LocalCluster
|
|
27
|
+
from fastapi import APIRouter, FastAPI, HTTPException
|
|
28
|
+
from pygeoapi.api import API
|
|
29
|
+
from pygeoapi.process.base import JobNotFoundError
|
|
30
|
+
from pygeoapi.process.manager.postgresql import PostgreSQLManager
|
|
31
|
+
from pygeoapi.provider.postgresql import get_engine
|
|
32
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
33
|
+
from starlette.requests import Request
|
|
34
|
+
from starlette.responses import JSONResponse
|
|
35
|
+
from starlette.status import ( # pylint: disable=C0411
|
|
36
|
+
HTTP_200_OK,
|
|
37
|
+
HTTP_201_CREATED,
|
|
38
|
+
HTTP_404_NOT_FOUND,
|
|
39
|
+
HTTP_500_INTERNAL_SERVER_ERROR,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from rs_dpr_service import opentelemetry
|
|
43
|
+
from rs_dpr_service.jobs_table import Base
|
|
44
|
+
from rs_dpr_service.processors import processors
|
|
45
|
+
|
|
46
|
+
# DON'T REMOVE (needed for SQLAlchemy)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Initialize a FastAPI application
|
|
50
|
+
app = FastAPI(title="rs-dpr-service", root_path="", debug=True)
|
|
51
|
+
router = APIRouter(tags=["DPR service"])
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger("my_logger")
|
|
54
|
+
logger.setLevel(logging.DEBUG)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def env_bool(var: str, default: bool) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Return True if an environemnt variable is set to 1, true or yes (case insensitive).
|
|
60
|
+
Return False if set to 0, false or no (case insensitive).
|
|
61
|
+
Return the default value if not set or set to a different value.
|
|
62
|
+
"""
|
|
63
|
+
val = os.getenv(var, str(default)).lower()
|
|
64
|
+
if val in ("y", "yes", "t", "true", "on", "1"):
|
|
65
|
+
return True
|
|
66
|
+
if val in ("n", "no", "f", "false", "off", "0"):
|
|
67
|
+
return False
|
|
68
|
+
return default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_config_path() -> pathlib.Path:
|
|
72
|
+
"""Return the pygeoapi configuration path and set the PYGEOAPI_CONFIG env var accordingly."""
|
|
73
|
+
path = pathlib.Path(__file__).parent.parent / "config" / "geoapi.yaml"
|
|
74
|
+
os.environ["PYGEOAPI_CONFIG"] = str(path)
|
|
75
|
+
return path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_config_contents() -> dict:
|
|
79
|
+
"""Return the pygeoapi configuration yaml file contents."""
|
|
80
|
+
# Open the configuration file
|
|
81
|
+
with open(get_config_path(), encoding="utf8") as opened:
|
|
82
|
+
contents = opened.read()
|
|
83
|
+
|
|
84
|
+
# Replace env vars by their value
|
|
85
|
+
contents = Template(contents).substitute(os.environ)
|
|
86
|
+
|
|
87
|
+
# Parse contents as yaml
|
|
88
|
+
return yaml.safe_load(contents)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def init_pygeoapi() -> API:
|
|
92
|
+
"""Init pygeoapi"""
|
|
93
|
+
return API(get_config_contents(), "")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
api = init_pygeoapi()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Filelock to be added ?
|
|
100
|
+
def init_db(pause: int = 3, timeout: int | None = None) -> PostgreSQLManager:
|
|
101
|
+
"""Initialize the PostgreSQL database connection and sets up required table and ENUM type.
|
|
102
|
+
|
|
103
|
+
This function constructs the database URL using environment variables for PostgreSQL
|
|
104
|
+
credentials, host, port, and database name. It then creates an SQLAlchemy engine and
|
|
105
|
+
registers the ENUM type JobStatus and the 'job' tables if they don't already exist.
|
|
106
|
+
|
|
107
|
+
Environment Variables:
|
|
108
|
+
- POSTGRES_USER: Username for database authentication.
|
|
109
|
+
- POSTGRES_PASSWORD: Password for the database.
|
|
110
|
+
- POSTGRES_HOST: Hostname of the PostgreSQL server.
|
|
111
|
+
- POSTGRES_PORT: Port number of the PostgreSQL server.
|
|
112
|
+
- POSTGRES_DB: Database name.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
pause: pause in seconds to wait for the database connection.
|
|
116
|
+
timeout: timeout in seconds to wait for the database connection.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
PostgreSQLManager instance
|
|
120
|
+
"""
|
|
121
|
+
manager_def = api.config["manager"]
|
|
122
|
+
if not manager_def or not isinstance(manager_def, dict) or not isinstance(manager_def["connection"], dict):
|
|
123
|
+
message = "Error reading the manager definition for pygeoapi PostgreSQL Manager"
|
|
124
|
+
# logger.error(message)
|
|
125
|
+
raise RuntimeError(message)
|
|
126
|
+
connection = manager_def["connection"]
|
|
127
|
+
|
|
128
|
+
# Create SQL Alchemy engine
|
|
129
|
+
engine = get_engine(**connection)
|
|
130
|
+
|
|
131
|
+
while True:
|
|
132
|
+
try:
|
|
133
|
+
# This registers the ENUM type and creates the jobs table if they do not exist
|
|
134
|
+
Base.metadata.create_all(bind=engine)
|
|
135
|
+
logger.info(f"Reached {engine.url!r}")
|
|
136
|
+
logger.info("Database table and ENUM type created successfully.")
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
# It fails if the database is unreachable. Wait a few seconds and try again.
|
|
140
|
+
except SQLAlchemyError:
|
|
141
|
+
logger.warning(f"Trying to reach {engine.url!r}")
|
|
142
|
+
|
|
143
|
+
# Sleep for n seconds and raise exception if timeout is reached.
|
|
144
|
+
if timeout is not None:
|
|
145
|
+
timeout -= pause
|
|
146
|
+
if timeout < 0:
|
|
147
|
+
raise
|
|
148
|
+
sleep(pause)
|
|
149
|
+
|
|
150
|
+
# Initialize PostgreSQLManager with the manager configuration
|
|
151
|
+
return PostgreSQLManager(manager_def)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@asynccontextmanager
|
|
155
|
+
async def app_lifespan(fastapi_app: FastAPI):
|
|
156
|
+
"""Lifespann app to be implemented with start up / stop logic"""
|
|
157
|
+
logger.info("Starting up the application...")
|
|
158
|
+
# Create jobs table
|
|
159
|
+
process_manager = init_db()
|
|
160
|
+
fastapi_app.extra["local_mode"] = env_bool("RSPY_LOCAL_MODE", default=False)
|
|
161
|
+
# There are 2 containers / pods that may be used:
|
|
162
|
+
# - one with the image that has the real eopf processor
|
|
163
|
+
# - one with the image that has the mockup eopf processor
|
|
164
|
+
# Set by default the env variables for the dask cluster name that will select one of
|
|
165
|
+
# these 2 containers / pods to the one with the real processor
|
|
166
|
+
# Later on, the user that requests one of the endpoints
|
|
167
|
+
# - /dpr/processes/{resource}/execution
|
|
168
|
+
# - /dpr/processes/{resource}
|
|
169
|
+
# may add in the content the following param:
|
|
170
|
+
# "use_mockup": True
|
|
171
|
+
# and the env variables will be changed
|
|
172
|
+
os.environ["DASK_CLUSTER_EOPF_NAME"] = os.environ["RSPY_DASK_DPR_SERVICE_CLUSTER_NAME"]
|
|
173
|
+
os.environ["DASK_GATEWAY_EOPF_ADDRESS"] = os.environ["DASK_GATEWAY__ADDRESS"]
|
|
174
|
+
|
|
175
|
+
fastapi_app.extra["process_manager"] = process_manager
|
|
176
|
+
# fastapi_app.extra["db_table"] = db.table("jobs")
|
|
177
|
+
# fastapi_app.extra["dask_cluster"] = cluster
|
|
178
|
+
# token refereshment logic
|
|
179
|
+
|
|
180
|
+
# Yield control back to the application (this is where the app will run)
|
|
181
|
+
yield
|
|
182
|
+
|
|
183
|
+
# Shutdown logic (cleanup)
|
|
184
|
+
logger.info("Shutting down the application...")
|
|
185
|
+
logger.info("Application gracefully stopped...")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Health check route
|
|
189
|
+
@router.get("/_mgmt/ping", include_in_schema=False)
|
|
190
|
+
async def ping():
|
|
191
|
+
"""Liveliness probe."""
|
|
192
|
+
return JSONResponse(status_code=HTTP_200_OK, content="Healthy")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Endpoint to get the status of a job by job_id
|
|
196
|
+
@router.get("/dpr/jobs/{job_id}")
|
|
197
|
+
async def get_job_status_endpoint(request: Request, job_id: str): # pylint: disable=W0613
|
|
198
|
+
"""Used to get status of processing job."""
|
|
199
|
+
try:
|
|
200
|
+
job = app.extra["process_manager"].get_job(job_id)
|
|
201
|
+
pretty_job = {"message": job["message"], "status": job["status"]}
|
|
202
|
+
return JSONResponse(status_code=HTTP_200_OK, content=pretty_job)
|
|
203
|
+
except JobNotFoundError: # pylint: disable=W0718
|
|
204
|
+
# Handle case when job_id is not found
|
|
205
|
+
return HTTPException(HTTP_404_NOT_FOUND, f"Job with ID {job_id} not found")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Endpoint to execute the rs-dpr-service process and generate a job ID
|
|
209
|
+
@router.post("/dpr/processes/{resource}/execution")
|
|
210
|
+
async def execute_process(request: Request, resource: str): # pylint: disable=unused-argument
|
|
211
|
+
"""Used to execute processing jobs."""
|
|
212
|
+
data = await request.json()
|
|
213
|
+
# check if the input resource exists
|
|
214
|
+
if resource not in api.config["resources"]:
|
|
215
|
+
return HTTPException(HTTP_404_NOT_FOUND, f"Process resource '{resource}' not found")
|
|
216
|
+
|
|
217
|
+
processor_name = api.config["resources"][resource]["processor"]["name"]
|
|
218
|
+
if processor_name in processors:
|
|
219
|
+
processor = processors[processor_name]
|
|
220
|
+
_, dpr_status = await processor( # type: ignore
|
|
221
|
+
request,
|
|
222
|
+
app.extra["process_manager"],
|
|
223
|
+
# app.extra["dask_cluster"],
|
|
224
|
+
).execute(data)
|
|
225
|
+
|
|
226
|
+
# Get identifier of the current job
|
|
227
|
+
status_dict = {
|
|
228
|
+
"accepted": HTTP_201_CREATED,
|
|
229
|
+
"running": HTTP_201_CREATED,
|
|
230
|
+
"successful": HTTP_201_CREATED,
|
|
231
|
+
"failed": HTTP_500_INTERNAL_SERVER_ERROR,
|
|
232
|
+
"dismissed": HTTP_500_INTERNAL_SERVER_ERROR,
|
|
233
|
+
}
|
|
234
|
+
id_key = [status for status in status_dict if status in dpr_status][0]
|
|
235
|
+
job = app.extra["process_manager"].get_job(dpr_status[id_key])
|
|
236
|
+
return JSONResponse(status_code=HTTP_201_CREATED, content=str(job))
|
|
237
|
+
return HTTPException(HTTP_404_NOT_FOUND, f"Processor '{processor_name}' not found")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@router.get("/dpr/processes/{resource}")
|
|
241
|
+
async def get_resource(request: Request, resource: str):
|
|
242
|
+
"""Should return info about a specific resource."""
|
|
243
|
+
if resource_info := next( # pylint: disable=W0612 # noqa: F841
|
|
244
|
+
(
|
|
245
|
+
api.config["resources"][defined_resource]
|
|
246
|
+
for defined_resource in api.config["resources"]
|
|
247
|
+
if defined_resource == resource
|
|
248
|
+
),
|
|
249
|
+
None,
|
|
250
|
+
):
|
|
251
|
+
try:
|
|
252
|
+
data = await request.json()
|
|
253
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
254
|
+
data = None
|
|
255
|
+
processor_name = api.config["resources"][resource]["processor"]["name"]
|
|
256
|
+
if processor_name in processors:
|
|
257
|
+
processor = processors[processor_name]
|
|
258
|
+
task_table = await processor( # type: ignore
|
|
259
|
+
request,
|
|
260
|
+
app.extra["process_manager"],
|
|
261
|
+
# app.extra["dask_cluster"],
|
|
262
|
+
).get_tasktable(data)
|
|
263
|
+
|
|
264
|
+
return JSONResponse(status_code=HTTP_200_OK, content=task_table)
|
|
265
|
+
return HTTPException(HTTP_404_NOT_FOUND, f"Resource {resource} not found")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if env_bool("RSPY_LOCAL_MODE", default=False):
|
|
269
|
+
|
|
270
|
+
@router.post("/dpr_service/dask/auth", include_in_schema=False)
|
|
271
|
+
async def dask_auth(local_dask_username: str, local_dask_password: str):
|
|
272
|
+
"""Set dask cluster authentication, only in local mode."""
|
|
273
|
+
os.environ["LOCAL_DASK_USERNAME"] = local_dask_username
|
|
274
|
+
os.environ["LOCAL_DASK_PASSWORD"] = local_dask_password
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# DPR_SERVICE FRONT LOGIC HERE
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
app.include_router(router)
|
|
281
|
+
app.router.lifespan_context = app_lifespan # type: ignore
|
|
282
|
+
opentelemetry.init_traces(app, "rs.dpr.service")
|
|
283
|
+
# Mount pygeoapi endpoints
|
|
284
|
+
app.mount(path="/oapi", app=api)
|