rs-dpr-service 0.2__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.2/PKG-INFO +52 -0
- rs_dpr_service-0.2/README.md +2 -0
- rs_dpr_service-0.2/pyproject.toml +138 -0
- rs_dpr_service-0.2/rs_dpr_service/__init__.py +13 -0
- rs_dpr_service-0.2/rs_dpr_service/call_dask.py +358 -0
- rs_dpr_service-0.2/rs_dpr_service/conversion_processor.py +147 -0
- rs_dpr_service-0.2/rs_dpr_service/jobs_table.py +83 -0
- rs_dpr_service-0.2/rs_dpr_service/main.py +329 -0
- rs_dpr_service-0.2/rs_dpr_service/openapi_validation.py +79 -0
- rs_dpr_service-0.2/rs_dpr_service/processors.py +598 -0
- rs_dpr_service-0.2/rs_dpr_service/safe_to_zarr.py +80 -0
- rs_dpr_service-0.2/rs_dpr_service/utils/__init__.py +15 -0
- rs_dpr_service-0.2/rs_dpr_service/utils/init_opentelemetry.py +254 -0
- rs_dpr_service-0.2/rs_dpr_service/utils/logging.py +116 -0
- rs_dpr_service-0.2/rs_dpr_service/utils/utils.py +31 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: rs-dpr-service
|
|
3
|
+
Version: 0.2
|
|
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.116.1,<0.118.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.57b0,<0.58)
|
|
35
|
+
Requires-Dist: opentelemetry-exporter-otlp (>=1.36.0,<2.0.0)
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-aws-lambda (>=0.57b0,<0.58)
|
|
37
|
+
Requires-Dist: opentelemetry-test-utils (>=0.57b0,<0.58)
|
|
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: python-logging-loki (>=0.3.1,<0.4.0)
|
|
42
|
+
Requires-Dist: s3fs (>=2025.7.0,<2026.0.0)
|
|
43
|
+
Requires-Dist: tornado (>=6.5,<7.0)
|
|
44
|
+
Requires-Dist: uvicorn (>=0.34.1,<0.35.0)
|
|
45
|
+
Project-URL: Documentation, https://home.rs-python.eu/rs-documentation/rs-server/docs/doc/
|
|
46
|
+
Project-URL: Homepage, https://home.rs-python.eu/
|
|
47
|
+
Project-URL: Repository, https://github.com/RS-PYTHON/rs-dpr-service
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# rs-dpr-service
|
|
51
|
+
A service responsible for triggering the DPR processor execution within the Dask cluster.
|
|
52
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
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.2"
|
|
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.116.1,<0.118.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.57b0" # then run 'poetry run opentelemetry-bootstrap -a install'
|
|
57
|
+
opentelemetry-instrumentation-aws-lambda = "^0.57b0"
|
|
58
|
+
opentelemetry-test-utils = "^0.57b0"
|
|
59
|
+
opentelemetry-exporter-otlp = "^1.36.0"
|
|
60
|
+
tornado = "^6.5" # fix trivy vulnerability
|
|
61
|
+
python-logging-loki = "^0.3.1" # to avoid otl error: No module named 'logging_loki'
|
|
62
|
+
s3fs = "^2025.7.0"
|
|
63
|
+
|
|
64
|
+
[tool.poetry.group.dev]
|
|
65
|
+
optional = true
|
|
66
|
+
|
|
67
|
+
[tool.poetry.group.dev.dependencies]
|
|
68
|
+
python = "^3.11"
|
|
69
|
+
pygeoapi = { git = "https://github.com/geopython/pygeoapi.git", branch = "master" }
|
|
70
|
+
pre-commit = ">=3.7.1,<5.0.0"
|
|
71
|
+
pylint = "^3.3.2"
|
|
72
|
+
pytest = "^8.3.3"
|
|
73
|
+
pytest-asyncio = "^1.1.0"
|
|
74
|
+
pytest-cov = ">=5,<7"
|
|
75
|
+
pytest-docker = "^3.1.0"
|
|
76
|
+
pytest-error-for-skips = "^2.0.2"
|
|
77
|
+
pytest-mock = "^3.14.0"
|
|
78
|
+
responses = "^0.25.3"
|
|
79
|
+
safety = "^3.5.1"
|
|
80
|
+
|
|
81
|
+
[tool.black]
|
|
82
|
+
line-length = 120
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
markers = [
|
|
86
|
+
"unit", # mark a test as unittest
|
|
87
|
+
"integration", # mark a test as integration test
|
|
88
|
+
]
|
|
89
|
+
asyncio_mode = "auto"
|
|
90
|
+
|
|
91
|
+
[tool.mypy]
|
|
92
|
+
warn_redundant_casts = true
|
|
93
|
+
# Force analyze of partially annotate function signatures
|
|
94
|
+
check_untyped_defs = true
|
|
95
|
+
ignore_missing_imports = true
|
|
96
|
+
|
|
97
|
+
[tool.flake8]
|
|
98
|
+
max-line-length = 120
|
|
99
|
+
# per-file-ignores = [
|
|
100
|
+
# '__init__.py:F401',
|
|
101
|
+
# ]
|
|
102
|
+
|
|
103
|
+
[tool.pylint]
|
|
104
|
+
max-line-length = 120
|
|
105
|
+
disable = [
|
|
106
|
+
"logging-fstring-interpolation", # Logging f-string interpolation
|
|
107
|
+
"R0917", # Too many positional arguments
|
|
108
|
+
"E0401", # import-error
|
|
109
|
+
"R0902", # too-many-instance-attributes
|
|
110
|
+
"R0913", # too-many-arguments
|
|
111
|
+
"too-many-statements",
|
|
112
|
+
"not-context-manager",
|
|
113
|
+
"too-many-branches",
|
|
114
|
+
]
|
|
115
|
+
ignore-paths = '^docs/.*$'
|
|
116
|
+
|
|
117
|
+
# Only add notes as comments that are not checked for errors.
|
|
118
|
+
notes = ["XXX"] # No errors on TODO and FIXME
|
|
119
|
+
|
|
120
|
+
min-similarity-lines = 10 # Minimum lines number of a similarity
|
|
121
|
+
|
|
122
|
+
[tool.bandit.assert_used]
|
|
123
|
+
skips = ['*/*.py']
|
|
124
|
+
|
|
125
|
+
[tool.bandit]
|
|
126
|
+
skips = ['B603', 'B607', 'B404']
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
[tool.poetry.requires-plugins]
|
|
130
|
+
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
|
131
|
+
|
|
132
|
+
[tool.poetry-dynamic-versioning]
|
|
133
|
+
enable = false
|
|
134
|
+
metadata = false
|
|
135
|
+
|
|
136
|
+
[build-system]
|
|
137
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
138
|
+
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,358 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
"""
|
|
16
|
+
This module contains the code that is related to dask and/or sent to the dask workers.
|
|
17
|
+
Avoid import unnecessary dependencies here.
|
|
18
|
+
"""
|
|
19
|
+
import ast
|
|
20
|
+
import importlib
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import os.path as osp
|
|
25
|
+
import re
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
import time
|
|
31
|
+
import zipfile
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
import yaml
|
|
35
|
+
from distributed.client import Client as DaskClient
|
|
36
|
+
from opentelemetry.trace.span import SpanContext
|
|
37
|
+
|
|
38
|
+
from rs_dpr_service.utils import init_opentelemetry
|
|
39
|
+
|
|
40
|
+
SERVICE_NAME = "rs.dpr.dask"
|
|
41
|
+
|
|
42
|
+
# Don't use rs_dpr_service.utils.logging, it's not forwarded to the client
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
logger.setLevel(logging.DEBUG)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def upload_this_module(dask_client: DaskClient):
|
|
48
|
+
"""
|
|
49
|
+
Upload this current module from the caller environment to the dask client.
|
|
50
|
+
|
|
51
|
+
WARNING: These modules should not import other modules that are not installed in the dask
|
|
52
|
+
environment or you'll have import errors.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
clients: list of dask clients to which upload the modules.
|
|
56
|
+
"""
|
|
57
|
+
# Root of the current project
|
|
58
|
+
root = Path(__file__).parent
|
|
59
|
+
|
|
60
|
+
# Files and dirs to upload and associated name in the zip archive
|
|
61
|
+
files = {
|
|
62
|
+
root / "__init__.py": "rs_dpr_service/__init__.py",
|
|
63
|
+
root / "call_dask.py": "rs_dpr_service/call_dask.py",
|
|
64
|
+
root / "safe_to_zarr.py": "rs_dpr_service/safe_to_zarr.py",
|
|
65
|
+
root / "utils/__init__.py": "rs_dpr_service/utils/__init__.py",
|
|
66
|
+
root / "utils/init_opentelemetry.py": "rs_dpr_service/utils/init_opentelemetry.py",
|
|
67
|
+
root / "utils/logging.py": "rs_dpr_service/utils/logging.py",
|
|
68
|
+
root / "utils/utils.py": "rs_dpr_service/utils/utils.py",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# From a temp dir
|
|
72
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
73
|
+
|
|
74
|
+
# Create a zip with our files
|
|
75
|
+
zip_path = f"{tmpdir}/{root.name}.zip"
|
|
76
|
+
with zipfile.ZipFile(zip_path, "w") as zipped:
|
|
77
|
+
|
|
78
|
+
# Zip all files
|
|
79
|
+
for key, value in files.items():
|
|
80
|
+
zipped.write(str(key), str(value))
|
|
81
|
+
|
|
82
|
+
# Upload zip file to dask clients.
|
|
83
|
+
# This also installs the zipped modules inside the dask python interpreter.
|
|
84
|
+
try:
|
|
85
|
+
dask_client.upload_file(zip_path)
|
|
86
|
+
|
|
87
|
+
# We have this error if we scale up the number of workers.
|
|
88
|
+
# But it's OK, the zip file is automatically uploaded to them anyway.
|
|
89
|
+
except KeyError as e:
|
|
90
|
+
logger.debug(f"Ignoring error {e}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def copy_caller_env(caller_env: dict[str, str]):
|
|
94
|
+
"""
|
|
95
|
+
Copy environment variables from the calling service environment to the dask client.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
caller_env: os.environ coming from caller
|
|
99
|
+
"""
|
|
100
|
+
local_mode = caller_env.get("RSPY_LOCAL_MODE") == "1"
|
|
101
|
+
|
|
102
|
+
# Copy env vars from the caller
|
|
103
|
+
keys = [
|
|
104
|
+
"RSPY_LOCAL_MODE",
|
|
105
|
+
"S3_ACCESSKEY",
|
|
106
|
+
"S3_SECRETKEY",
|
|
107
|
+
"S3_ENDPOINT",
|
|
108
|
+
"S3_REGION",
|
|
109
|
+
"PREFECT_BUCKET_NAME",
|
|
110
|
+
"PREFECT_BUCKET_FOLDER",
|
|
111
|
+
"DASK_GATEWAY_EOPF_ADDRESS",
|
|
112
|
+
"DASK_CLUSTER_EOPF_NAME",
|
|
113
|
+
"AWS_REQUEST_CHECKSUM_CALCULATION",
|
|
114
|
+
"AWS_RESPONSE_CHECKSUM_VALIDATION",
|
|
115
|
+
"TEMPO_ENDPOINT",
|
|
116
|
+
"OTEL_PYTHON_REQUESTS_TRACE_HEADERS",
|
|
117
|
+
"OTEL_PYTHON_REQUESTS_TRACE_BODY",
|
|
118
|
+
]
|
|
119
|
+
if local_mode:
|
|
120
|
+
keys.extend(
|
|
121
|
+
[
|
|
122
|
+
"LOCAL_DASK_USERNAME",
|
|
123
|
+
"LOCAL_DASK_PASSWORD",
|
|
124
|
+
"access_key",
|
|
125
|
+
"bucket_location",
|
|
126
|
+
"host_base",
|
|
127
|
+
"host_bucket",
|
|
128
|
+
"secret_key",
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
keys.extend(["JUPYTERHUB_API_TOKEN"])
|
|
133
|
+
for key in keys:
|
|
134
|
+
if value := caller_env.get(key):
|
|
135
|
+
os.environ[key] = value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def dpr_tasktable_task(
|
|
139
|
+
caller_env: dict[str, str],
|
|
140
|
+
flow_span_context: SpanContext,
|
|
141
|
+
use_mockup: bool,
|
|
142
|
+
module_name: str,
|
|
143
|
+
class_name: str,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Return the DPR tasktable. This function is run from inside the dask pod.
|
|
147
|
+
"""
|
|
148
|
+
# Copy env vars from the caller
|
|
149
|
+
copy_caller_env(caller_env)
|
|
150
|
+
|
|
151
|
+
# Init opentelemetry and record all task in an Opentelemetry span
|
|
152
|
+
init_opentelemetry.init_traces(None, SERVICE_NAME, logger)
|
|
153
|
+
with init_opentelemetry.start_span(__name__, "main_dask_flow", flow_span_context):
|
|
154
|
+
|
|
155
|
+
if use_mockup:
|
|
156
|
+
time.sleep(1)
|
|
157
|
+
return {}
|
|
158
|
+
|
|
159
|
+
# Load the python class
|
|
160
|
+
class_ = getattr(importlib.import_module(module_name), class_name)
|
|
161
|
+
|
|
162
|
+
# Get the tasktable for default mode. See:
|
|
163
|
+
# https://cpm.pages.eopf.copernicus.eu/eopf-cpm/main/processor-orchestration-guide/tasktables.html#tasktables
|
|
164
|
+
logger.debug(f"Available modes for {class_}: {class_.get_available_modes()}")
|
|
165
|
+
default_mode = class_.get_default_mode()
|
|
166
|
+
tasktable = class_.get_tasktable_description(default_mode)
|
|
167
|
+
return tasktable
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def dpr_processor_task( # pylint: disable=R0914, R0917
|
|
171
|
+
caller_env: dict[str, str],
|
|
172
|
+
data: dict,
|
|
173
|
+
use_mockup: bool,
|
|
174
|
+
):
|
|
175
|
+
"""
|
|
176
|
+
Run the DPR processor. This function is run from inside the dask pod.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
caller_env: env variables coming from the caller
|
|
180
|
+
data: data to send to the processor
|
|
181
|
+
use_mockup: use the mockup or real processor
|
|
182
|
+
"""
|
|
183
|
+
# Copy env vars from the caller
|
|
184
|
+
copy_caller_env(caller_env)
|
|
185
|
+
|
|
186
|
+
# Mockup processor
|
|
187
|
+
if use_mockup:
|
|
188
|
+
try:
|
|
189
|
+
payload_abs_path = osp.join("/", os.getcwd(), "payload.cfg")
|
|
190
|
+
with open(payload_abs_path, "w+", encoding="utf-8") as payload:
|
|
191
|
+
payload.write(yaml.safe_dump(data))
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.exception("Exception during payload file creation: %s", e)
|
|
194
|
+
raise
|
|
195
|
+
command = ["python3.11", "DPR_processor_mock.py", "-p", payload_abs_path]
|
|
196
|
+
working_dir = "/src/DPR"
|
|
197
|
+
log_path = "./mockup.log" # not used
|
|
198
|
+
logger.debug(f"Working directory for subprocess: {working_dir} (type: {type(working_dir)})")
|
|
199
|
+
|
|
200
|
+
# Real processor
|
|
201
|
+
else:
|
|
202
|
+
# Read arguments
|
|
203
|
+
s3_config_dir = data["s3_config_dir"]
|
|
204
|
+
payload_subpath = data["payload_subpath"]
|
|
205
|
+
s3_report_dir = data["s3_report_dir"]
|
|
206
|
+
|
|
207
|
+
# Get S3 file handler.
|
|
208
|
+
# NOTE: eopf exists in the dask worker environment, not in the rs-dpr-service env,
|
|
209
|
+
# so we cannot import it from the top of this module.
|
|
210
|
+
from eopf.common.file_utils import ( # pylint: disable=import-outside-toplevel
|
|
211
|
+
AnyPath,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
s3 = AnyPath(
|
|
215
|
+
s3_config_dir,
|
|
216
|
+
key=os.environ["S3_ACCESSKEY"],
|
|
217
|
+
secret=os.environ["S3_SECRETKEY"],
|
|
218
|
+
client_kwargs={
|
|
219
|
+
"endpoint_url": os.environ["S3_ENDPOINT"],
|
|
220
|
+
"region_name": os.environ["S3_REGION"],
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
logger.info("The dpr processing task started")
|
|
225
|
+
|
|
226
|
+
# Download the configuration folder from the S3 bucket into a local temp folder
|
|
227
|
+
local_config_dir = s3.get(recursive=True)
|
|
228
|
+
|
|
229
|
+
# Payload path and parent dir
|
|
230
|
+
payload_file = osp.realpath(osp.join(local_config_dir, payload_subpath))
|
|
231
|
+
payload_dir = osp.dirname(payload_file)
|
|
232
|
+
|
|
233
|
+
with open(payload_file, encoding="utf-8") as opened:
|
|
234
|
+
payload_contents = yaml.safe_load(opened)
|
|
235
|
+
logger.debug(f"Payload file contents: {payload_file!r}\n{json.dumps(payload_contents, indent=2)}")
|
|
236
|
+
|
|
237
|
+
command = ["eopf", "trigger", "local", payload_file]
|
|
238
|
+
|
|
239
|
+
# Change working directory
|
|
240
|
+
working_dir = osp.join(local_config_dir, payload_dir)
|
|
241
|
+
os.chdir(working_dir)
|
|
242
|
+
|
|
243
|
+
# Create the reports dir
|
|
244
|
+
# WARNING: fields from the payload file: dask__export_graphs, performance_report_file, ... should
|
|
245
|
+
# also use this directory: ./reports
|
|
246
|
+
local_report_dir = osp.realpath("./reports")
|
|
247
|
+
log_path = osp.join(local_report_dir, Path(payload_file).with_suffix(".processor.log").name)
|
|
248
|
+
shutil.rmtree(local_report_dir, ignore_errors=True)
|
|
249
|
+
os.makedirs(local_report_dir, exist_ok=True)
|
|
250
|
+
|
|
251
|
+
# Trigger EOPF processing, catch output
|
|
252
|
+
with subprocess.Popen(
|
|
253
|
+
command,
|
|
254
|
+
stdout=subprocess.PIPE,
|
|
255
|
+
stderr=subprocess.STDOUT,
|
|
256
|
+
text=True,
|
|
257
|
+
cwd=working_dir,
|
|
258
|
+
) as proc:
|
|
259
|
+
|
|
260
|
+
# Log contents
|
|
261
|
+
log_str = ""
|
|
262
|
+
|
|
263
|
+
# Write output to a log file and string + redirect to the prefect logger
|
|
264
|
+
with open(log_path, "w+", encoding="utf-8") as log_file:
|
|
265
|
+
while proc.stdout and (line := proc.stdout.readline()) != "":
|
|
266
|
+
|
|
267
|
+
# The log prints password in clear e.g 'key': '<my-secret>'... hide them with a regex
|
|
268
|
+
for key in (
|
|
269
|
+
"key",
|
|
270
|
+
"secret",
|
|
271
|
+
"endpoint_url",
|
|
272
|
+
"region_name",
|
|
273
|
+
"api_token",
|
|
274
|
+
"password",
|
|
275
|
+
):
|
|
276
|
+
line = re.sub(rf"(\W{key}\W)[^,}}]*", r"\1: ***", line)
|
|
277
|
+
|
|
278
|
+
# Write to log file and string
|
|
279
|
+
log_file.write(line)
|
|
280
|
+
log_str += line
|
|
281
|
+
|
|
282
|
+
# Write to logger if not empty
|
|
283
|
+
line = line.rstrip()
|
|
284
|
+
if line:
|
|
285
|
+
logger.info(line)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
# Wait for the execution to finish
|
|
289
|
+
status_code = proc.wait()
|
|
290
|
+
|
|
291
|
+
# Raise exception if the status code is != 0
|
|
292
|
+
if status_code:
|
|
293
|
+
raise RuntimeError(f"EOPF error, status code {status_code!r}, please see the log.")
|
|
294
|
+
logger.info(f"EOPF finished successfully with status code {status_code!r}")
|
|
295
|
+
|
|
296
|
+
# search for the JSON-like part, parse it, and ignore the rest.
|
|
297
|
+
if use_mockup:
|
|
298
|
+
match = re.search(r"(\[\s*\{.*\}\s*\])", log_str, re.DOTALL)
|
|
299
|
+
if not match:
|
|
300
|
+
raise ValueError(f"No valid dpr_payload structure found in the output:\n{log_str}")
|
|
301
|
+
|
|
302
|
+
payload_str = match.group(1)
|
|
303
|
+
|
|
304
|
+
# Use `ast.literal_eval` to safely evaluate the structure
|
|
305
|
+
try:
|
|
306
|
+
# payload_str is a string that looks like a JSON, extracted from the dpr mockup's raw output.
|
|
307
|
+
# ast.literal_eval() parses that string and returns the actual Python object (not just the string).
|
|
308
|
+
return ast.literal_eval(payload_str)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise ValueError(f"Failed to parse dpr_payload structure: {e}") from e
|
|
311
|
+
|
|
312
|
+
# NOTE: with the real processor, what should we return ?
|
|
313
|
+
return {}
|
|
314
|
+
|
|
315
|
+
# In all cases, upload the reports dir to the s3 bucket.
|
|
316
|
+
finally:
|
|
317
|
+
try:
|
|
318
|
+
if not use_mockup:
|
|
319
|
+
logger.info(f"Upload reports {local_report_dir!r} to {s3_report_dir!r}")
|
|
320
|
+
s3._fs.put(local_report_dir, s3_report_dir, recursive=True) # pylint: disable=protected-access
|
|
321
|
+
except Exception as exception: # pylint: disable=broad-exception-caught
|
|
322
|
+
logger.error(exception)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def convert_safe_to_zarr(cfg):
|
|
326
|
+
"""
|
|
327
|
+
Convert from legacy product (safe format) into Zarr format using EOPF in a subprocess.
|
|
328
|
+
|
|
329
|
+
This runs the rs_dpr_service.safe_to_zarr module as a subprocess, passing config as JSON string.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
# Serialize the config
|
|
333
|
+
cfg_str = json.dumps(cfg)
|
|
334
|
+
|
|
335
|
+
# Find the ZIP that this code lives in
|
|
336
|
+
module_path = Path(__file__).resolve()
|
|
337
|
+
zip_path = Path(str(module_path).split(".zip", maxsplit=1)[0] + ".zip")
|
|
338
|
+
if not zip_path.is_file():
|
|
339
|
+
raise RuntimeError(f"Cannot locate rs_dpr_service.zip at {zip_path}")
|
|
340
|
+
|
|
341
|
+
# Prepare an env that lets Python import from inside the ZIP
|
|
342
|
+
env = os.environ.copy()
|
|
343
|
+
# prepend the zip onto PYTHONPATH (so zipimport will kick in)
|
|
344
|
+
env["PYTHONPATH"] = str(zip_path) + os.pathsep + env.get("PYTHONPATH", "")
|
|
345
|
+
|
|
346
|
+
# Run the converter as a module
|
|
347
|
+
cmd = [sys.executable, "-m", "rs_dpr_service.safe_to_zarr", cfg_str]
|
|
348
|
+
result = subprocess.run(
|
|
349
|
+
cmd,
|
|
350
|
+
env=env,
|
|
351
|
+
capture_output=True,
|
|
352
|
+
text=True,
|
|
353
|
+
check=False,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if result.returncode != 0:
|
|
357
|
+
raise RuntimeError(f"Conversion failed: {result.stderr.strip()}")
|
|
358
|
+
return result.stdout.strip()
|