Skip to content

Commit 0f59fb5

Browse files
committed
New: kaleidoscope-resolve utility
1 parent 75800ce commit 0f59fb5

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed

kaleidoscope/main/resolve.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) Brockmann Consult GmbH, 2025
3+
# License: MIT
4+
5+
"""
6+
This module provides the Kaleidoscope resolve main function.
7+
"""
8+
9+
import json
10+
import signal
11+
import sys
12+
import warnings
13+
from argparse import ArgumentDefaultsHelpFormatter
14+
from argparse import ArgumentError
15+
from argparse import ArgumentParser
16+
from argparse import Namespace
17+
from importlib import resources
18+
from pathlib import Path
19+
from typing import Any
20+
from typing import TextIO
21+
22+
import yaml
23+
from xarray import Dataset
24+
25+
from kaleidoscope import __name__
26+
from kaleidoscope import __version__
27+
from kaleidoscope.interface.constants import VID_TIM
28+
from kaleidoscope.interface.processing import Processing
29+
from kaleidoscope.interface.reading import Reading
30+
from kaleidoscope.interface.writing import Writing
31+
from kaleidoscope.logger import get_logger
32+
from kaleidoscope.readerfactory import ReaderFactory
33+
from kaleidoscope.runner import Runner
34+
from kaleidoscope.signalhandler import AbortHandler
35+
from kaleidoscope.signalhandler import KeyboardInterruptHandler
36+
from kaleidoscope.signalhandler import TerminationRequestHandler
37+
from kaleidoscope.writerfactory import WriterFactory
38+
39+
warnings.filterwarnings("ignore")
40+
41+
42+
class _ArgumentParser(ArgumentParser):
43+
"""A command line parser overriding the default error handling."""
44+
45+
def error(self, message):
46+
"""Handles an error by raising an argument error."""
47+
raise ArgumentError(argument=None, message=message)
48+
49+
50+
class Parser:
51+
"""A factory to create the command line parser."""
52+
53+
@staticmethod
54+
def create() -> ArgumentParser:
55+
"""
56+
Creates a new command line parser.
57+
58+
:return: The parser.
59+
"""
60+
parser = _ArgumentParser(
61+
prog=f"{__name__}-resolve",
62+
description="This scientific processor transforms a time series "
63+
"dataset into separate datasets, each of which corresponds to a "
64+
"different time step.",
65+
epilog="Copyright (c) Brockmann Consult GmbH, 2025. "
66+
"License: MIT",
67+
exit_on_error=False,
68+
formatter_class=ArgumentDefaultsHelpFormatter,
69+
)
70+
Parser._add_arguments(parser)
71+
Parser._add_options(parser)
72+
Parser._add_version(parser)
73+
return parser
74+
75+
@staticmethod
76+
def _add_arguments(parser):
77+
"""This method does not belong to public API."""
78+
parser.add_argument(
79+
"source_file",
80+
help="the file path of the source dataset.",
81+
type=Path,
82+
)
83+
parser.add_argument(
84+
"target_file",
85+
help="the file path of the target datasets. The pattern TTT is "
86+
"replaced with the index value of the time slice extracted.",
87+
type=Path,
88+
)
89+
90+
@staticmethod
91+
def _add_options(parser):
92+
"""This method does not belong to public API."""
93+
parser.add_argument(
94+
"--engine-reader",
95+
help="specify the engine used to read the source product file.",
96+
choices=["h5netcdf", "netcdf4", "zarr"],
97+
required=False,
98+
dest="engine_reader",
99+
)
100+
parser.add_argument(
101+
"--engine-writer",
102+
help="specify the engine used to write the target product file.",
103+
choices=["h5netcdf", "netcdf4", "zarr"],
104+
required=False,
105+
dest="engine_writer",
106+
)
107+
parser.add_argument(
108+
"--log-level",
109+
help="specify the log level.",
110+
choices=["debug", "info", "warning", "error", "off"],
111+
required=False,
112+
dest="log_level",
113+
)
114+
parser.add_argument(
115+
"--stack-traces",
116+
help="enable Python stack traces.",
117+
action="store_true",
118+
required=False,
119+
dest="stack_traces",
120+
)
121+
122+
@staticmethod
123+
def _add_version(parser):
124+
"""This method does not belong to public API."""
125+
parser.add_argument(
126+
"-v",
127+
"--version",
128+
action="version",
129+
version=f"%(prog)s {__version__}",
130+
)
131+
132+
133+
def index(i: int, w: int = 3) -> str:
134+
"""
135+
Converts an index number into an index string.
136+
137+
:param i: The index number.
138+
:param w: The width of the index string.
139+
:return: The index string
140+
"""
141+
return str(i).rjust(w, "0")
142+
143+
144+
class Processor(Processing):
145+
"""! The Kaleidoscope resolve processor."""
146+
147+
def __init__(self, config_package: str = "kaleidoscope.config"):
148+
"""
149+
Creates a new processor instance.
150+
151+
:param config_package: The name of the processor
152+
configuration package.
153+
"""
154+
self._config_package = config_package
155+
156+
def get_config_package(self): # noqa: D102
157+
return self._config_package
158+
159+
def get_default_config(self) -> dict[str, Any]: # noqa: D102
160+
package = self.get_config_package()
161+
name = "config.yml"
162+
with resources.path(package, name) as resource:
163+
with open(resource) as r:
164+
config = yaml.safe_load(r)
165+
config["processor_name"] = self.get_name()
166+
config["processor_version"] = self.get_version()
167+
return config
168+
169+
def get_name(self): # noqa: D102
170+
return f"{__name__}-resolve"
171+
172+
def get_version(self): # noqa: D102
173+
return __version__
174+
175+
def run(self, args: Namespace): # noqa: D102
176+
config = sorted(vars(args).items(), key=lambda item: item[0])
177+
for name, value in config:
178+
get_logger().info(f"config: {name} = {value}")
179+
180+
source: Dataset | None = None
181+
try:
182+
reader: Reading = self._create_reader(args)
183+
get_logger().debug(f"opening source dataset: {args.source_file}")
184+
source = reader.read(args.source_file)
185+
n = source[VID_TIM].size
186+
for i in range(n):
187+
writer: Writing = self._create_writer(args)
188+
target: Dataset = source.isel(time=slice(i, i + 1))
189+
get_logger().info(
190+
f"writing time step: {index(i)} ({index(n - 1)})"
191+
)
192+
try:
193+
writer.write(
194+
target,
195+
f"{args.target_file}".replace("TTT", f"{index(i)}"),
196+
)
197+
finally:
198+
if target is not None:
199+
target.close()
200+
finally:
201+
if source is not None:
202+
source.close()
203+
204+
def get_result( # noqa: D102
205+
self, args: Namespace, *inputs: Dataset
206+
) -> Dataset:
207+
"""Not used."""
208+
pass
209+
210+
def _create_reader(self, args) -> Reading:
211+
"""This method does not belong to public API."""
212+
package = self.get_config_package()
213+
name = "config.reader.json"
214+
with resources.path(package, name) as resource:
215+
get_logger().debug(f"reading resource: {resource}")
216+
with open(resource) as r:
217+
config = json.load(r)
218+
chunks = config["config.reader.chunks"]
219+
for k, v in chunks["_"].items():
220+
chunks[k] = v
221+
if args.engine_reader:
222+
config["config.reader.engine"] = args.engine_reader
223+
return ReaderFactory.create_reader(config=config)
224+
225+
def _create_writer(self, args) -> Writing:
226+
"""This method does not belong to public API."""
227+
package = self.get_config_package()
228+
name = "config.writer.json"
229+
with resources.path(package, name) as resource:
230+
get_logger().debug(f"reading resource: {resource}")
231+
with open(resource) as r:
232+
config = json.load(r)
233+
chunks = config["config.writer.chunks"]
234+
for k, v in chunks["_"].items():
235+
chunks[k] = v
236+
if args.engine_writer:
237+
config["config.writer.engine"] = args.engine_writer
238+
return WriterFactory.create_writer(config=config)
239+
240+
241+
def main() -> int:
242+
"""
243+
The main function.
244+
245+
Initializes signal handlers, runs the processor and returns
246+
an exit code.
247+
248+
:return: The exit code.
249+
"""
250+
signal.signal(signal.SIGABRT, AbortHandler())
251+
signal.signal(signal.SIGINT, KeyboardInterruptHandler())
252+
signal.signal(signal.SIGTERM, TerminationRequestHandler())
253+
return run()
254+
255+
256+
def run(
257+
args: list[str] | None = None,
258+
out: TextIO = sys.stdout,
259+
err: TextIO = sys.stderr,
260+
):
261+
"""
262+
Runs the processor and returns an exit code.
263+
264+
:param args: An optional list of arguments.
265+
:param out: The stream to use for usual log messages.
266+
:param err: The stream to use for error log messages.
267+
:return: The exit code.
268+
"""
269+
return Runner(Processor("kaleidoscope.config"), Parser.create()).run(
270+
args, out, err
271+
)
272+
273+
274+
if __name__ == "__main__":
275+
sys.exit(main())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ lint = [
6363
[project.scripts]
6464
kaleidoscope-scatter = "kaleidoscope.main.scatter:main"
6565
kaleidoscope-collect = "kaleidoscope.main.collect:main"
66+
kaleidoscope-resolve = "kaleidoscope.main.resolve:main"
6667

6768
[project.urls]
6869
Homepage = "https://bcdev.github.io/kaleidoscope/"

0 commit comments

Comments
 (0)