oceanarray 0.1.0__py3-none-any.whl
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.
- oceanarray/__init__.py +20 -0
- oceanarray/_version.py +1 -0
- oceanarray/cli.py +1343 -0
- oceanarray/clock_offset.py +675 -0
- oceanarray/config/OS1_sensor_attrs.yaml +49 -0
- oceanarray/config/OS1_var_names.yaml +5 -0
- oceanarray/config/OS1_vocab_attrs.yaml +84 -0
- oceanarray/config/legacy/project_RAPID.yaml +69 -0
- oceanarray/config/legacy/rodb_keys.txt +206 -0
- oceanarray/config/legacy/rodb_keys.yaml +1012 -0
- oceanarray/config/logging.yaml +28 -0
- oceanarray/legacy/__init__.py +47 -0
- oceanarray/legacy/caldip.py +753 -0
- oceanarray/legacy/convertOS.py +527 -0
- oceanarray/legacy/find_deployment.py +489 -0
- oceanarray/legacy/mooring_rodb.py +429 -0
- oceanarray/legacy/process_rodb.py +369 -0
- oceanarray/legacy/rodb.py +330 -0
- oceanarray/logger.py +192 -0
- oceanarray/logsheet/__init__.py +60 -0
- oceanarray/logsheet/_columns.py +97 -0
- oceanarray/logsheet/_config.py +245 -0
- oceanarray/logsheet/_firmware.py +43 -0
- oceanarray/logsheet/_latex.py +128 -0
- oceanarray/logsheet/_render.py +126 -0
- oceanarray/logsheet/builders/__init__.py +13 -0
- oceanarray/logsheet/builders/caldip_download.py +195 -0
- oceanarray/logsheet/builders/caldip_setup.py +188 -0
- oceanarray/logsheet/builders/deployment_setup.py +218 -0
- oceanarray/logsheet/builders/mooring_download.py +243 -0
- oceanarray/logsheet/builders/recovery.py +157 -0
- oceanarray/logsheet/data/column_defs.yaml +686 -0
- oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +42 -0
- oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +47 -0
- oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +70 -0
- oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +97 -0
- oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +54 -0
- oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +103 -0
- oceanarray/mooring_level.py +1368 -0
- oceanarray/oceanarray.mplstyle +18 -0
- oceanarray/parameters.py +270 -0
- oceanarray/plotter.py +942 -0
- oceanarray/plotters/__init__.py +97 -0
- oceanarray/plotters/_animation.py +261 -0
- oceanarray/plotters/_current.py +705 -0
- oceanarray/plotters/_diagnostic.py +667 -0
- oceanarray/plotters/_helpers.py +57 -0
- oceanarray/plotters/_primitives.py +101 -0
- oceanarray/plotters/_section.py +16 -0
- oceanarray/plotters/_timeseries.py +11 -0
- oceanarray/rapid_interp.py +739 -0
- oceanarray/read_rbr_hex.py +541 -0
- oceanarray/readers.py +303 -0
- oceanarray/report/__init__.py +5 -0
- oceanarray/report/_array.py +386 -0
- oceanarray/report/_grid.py +539 -0
- oceanarray/report/_html_helpers.py +1004 -0
- oceanarray/report/_instrument.py +853 -0
- oceanarray/report/_mooring.py +1385 -0
- oceanarray/report/_plots.py +4284 -0
- oceanarray/report/_stack.py +941 -0
- oceanarray/stage1.py +1959 -0
- oceanarray/stage2.py +1160 -0
- oceanarray/stage3.py +2494 -0
- oceanarray/time_gridding.py +958 -0
- oceanarray/tools.py +400 -0
- oceanarray/transports.py +1 -0
- oceanarray/utilities.py +538 -0
- oceanarray/validation.py +389 -0
- oceanarray/writers.py +99 -0
- oceanarray-0.1.0.dist-info/METADATA +503 -0
- oceanarray-0.1.0.dist-info/RECORD +76 -0
- oceanarray-0.1.0.dist-info/WHEEL +5 -0
- oceanarray-0.1.0.dist-info/entry_points.txt +2 -0
- oceanarray-0.1.0.dist-info/licenses/LICENSE +21 -0
- oceanarray-0.1.0.dist-info/top_level.txt +1 -0
oceanarray/logger.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Logging configuration and helpers for the oceanarray processing pipeline."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Global logger instance (will be configured by setup_logger)
|
|
9
|
+
log = logging.getLogger("oceanarray")
|
|
10
|
+
log.setLevel(logging.DEBUG) # capture everything; handlers filter later
|
|
11
|
+
|
|
12
|
+
# Global logging flag
|
|
13
|
+
# Set to True to enable logging, False to disable
|
|
14
|
+
LOGGING_ENABLED = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def enable_logging() -> None:
|
|
18
|
+
"""Enable logging globally."""
|
|
19
|
+
global LOGGING_ENABLED
|
|
20
|
+
LOGGING_ENABLED = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def disable_logging() -> None:
|
|
24
|
+
"""Disable logging globally."""
|
|
25
|
+
global LOGGING_ENABLED
|
|
26
|
+
LOGGING_ENABLED = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def log_info(message: str, *args: Any) -> None:
|
|
30
|
+
"""Log an info message, if logging is enabled."""
|
|
31
|
+
if LOGGING_ENABLED:
|
|
32
|
+
log.info(message, *args, stacklevel=2)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def log_warning(message: str, *args: Any) -> None:
|
|
36
|
+
"""Log a warning message, if logging is enabled."""
|
|
37
|
+
if LOGGING_ENABLED:
|
|
38
|
+
log.warning(message, *args, stacklevel=2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def log_error(message: str, *args: Any) -> None:
|
|
42
|
+
"""Log an error message, if logging is enabled."""
|
|
43
|
+
if LOGGING_ENABLED:
|
|
44
|
+
log.error(message, *args, stacklevel=2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def log_debug(message: str, *args: Any) -> None:
|
|
48
|
+
"""Log a debug message, if logging is enabled."""
|
|
49
|
+
if LOGGING_ENABLED:
|
|
50
|
+
log.debug(message, *args, stacklevel=2)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def setup_logger(array_name: str, output_dir: str = "logs") -> None:
|
|
54
|
+
"""Configure the global logger to output to a file for the given array.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
array_name : str
|
|
59
|
+
Name of the observing array (e.g., 'move', 'rapid', etc.).
|
|
60
|
+
output_dir : str
|
|
61
|
+
Directory to save log files.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
if not LOGGING_ENABLED:
|
|
65
|
+
return
|
|
66
|
+
# Resolve output directory to project root
|
|
67
|
+
project_root = Path(__file__).resolve().parent.parent
|
|
68
|
+
output_path = project_root / output_dir
|
|
69
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%dT%H")
|
|
72
|
+
log_filename = f"{array_name.upper()}_{timestamp}_read.log"
|
|
73
|
+
log_path = output_path / log_filename
|
|
74
|
+
|
|
75
|
+
# Prevent duplicate handlers in case of multiple calls
|
|
76
|
+
if not any(
|
|
77
|
+
isinstance(h, logging.FileHandler) and h.baseFilename == log_path
|
|
78
|
+
for h in log.handlers
|
|
79
|
+
):
|
|
80
|
+
file_handler = logging.FileHandler(log_path, encoding="utf-8", mode="w")
|
|
81
|
+
formatter = logging.Formatter(
|
|
82
|
+
fmt="%(asctime)s %(levelname)-8s %(funcName)s %(message)s",
|
|
83
|
+
datefmt="%Y%m%dT%H%M%S",
|
|
84
|
+
)
|
|
85
|
+
file_handler.setFormatter(formatter)
|
|
86
|
+
|
|
87
|
+
# Optional: console handler
|
|
88
|
+
console_handler = logging.StreamHandler()
|
|
89
|
+
console_handler.setFormatter(formatter)
|
|
90
|
+
|
|
91
|
+
# Clear existing handlers first
|
|
92
|
+
log.handlers.clear()
|
|
93
|
+
|
|
94
|
+
log.addHandler(file_handler)
|
|
95
|
+
log.addHandler(console_handler)
|
|
96
|
+
|
|
97
|
+
log.info(f"Logger initialized for array: {array_name}, writing to {log_path}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_logging_config() -> dict:
|
|
101
|
+
"""Load the global logging configuration from config/logging.yaml.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
dict
|
|
106
|
+
Logging configuration dictionary
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
FileNotFoundError
|
|
111
|
+
If logging.yaml is not found
|
|
112
|
+
yaml.YAMLError
|
|
113
|
+
If logging.yaml cannot be parsed
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
import yaml
|
|
117
|
+
|
|
118
|
+
config_path = Path(__file__).parent / "config" / "logging.yaml"
|
|
119
|
+
|
|
120
|
+
if not config_path.exists():
|
|
121
|
+
raise FileNotFoundError(f"Logging config not found: {config_path}") # noqa: TRY003
|
|
122
|
+
|
|
123
|
+
with open(config_path, "r") as f:
|
|
124
|
+
config = yaml.safe_load(f)
|
|
125
|
+
|
|
126
|
+
return config.get("logging", {})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def setup_stage_logging(mooring_name: str, stage_name: str, proc_dir: Path) -> Path:
|
|
130
|
+
"""Set up logging for a processing stage using global configuration.
|
|
131
|
+
|
|
132
|
+
This creates simple file-based logging for processing stages (stage1, stage2, etc.)
|
|
133
|
+
using the configuration from config/logging.yaml.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
mooring_name : str
|
|
138
|
+
Name of the mooring (e.g., 'dsE_1_2018')
|
|
139
|
+
stage_name : str
|
|
140
|
+
Name of the processing stage (e.g., 'stage1', 'stage2', 'time_gridding')
|
|
141
|
+
proc_dir : Path
|
|
142
|
+
Processing directory for the mooring
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
Path
|
|
147
|
+
Full path to the log file. When the logging configuration file
|
|
148
|
+
(``config/logging.yaml``) is missing or unparseable, falls back to
|
|
149
|
+
``{proc_dir}/processing_logs/{mooring_name}_{timestamp}_{stage_name}.mooring.log``.
|
|
150
|
+
This function does not raise; config failures are handled silently.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
import yaml
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
config = load_logging_config()
|
|
157
|
+
except (FileNotFoundError, yaml.YAMLError):
|
|
158
|
+
# Fallback: place logs in processing_logs/ subdir
|
|
159
|
+
log_time = datetime.datetime.now().strftime("%Y%m%dT%H")
|
|
160
|
+
log_dir = proc_dir / "processing_logs"
|
|
161
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
return log_dir / f"{mooring_name}_{log_time}_{stage_name}.mooring.log"
|
|
163
|
+
|
|
164
|
+
# Extract configuration values with defaults
|
|
165
|
+
log_directory = config.get("directory", "processing_logs")
|
|
166
|
+
filename_pattern = config.get(
|
|
167
|
+
"filename_pattern", "{mooring_name}_{timestamp}_{stage}.log"
|
|
168
|
+
)
|
|
169
|
+
timestamp_format = config.get("timestamp_format", "%Y%m%dT%H")
|
|
170
|
+
create_directory = config.get("create_directory", True)
|
|
171
|
+
|
|
172
|
+
# Generate timestamp
|
|
173
|
+
timestamp = datetime.datetime.now().strftime(timestamp_format)
|
|
174
|
+
|
|
175
|
+
# Determine log directory path
|
|
176
|
+
if Path(log_directory).is_absolute():
|
|
177
|
+
# Absolute path specified
|
|
178
|
+
log_dir = Path(log_directory)
|
|
179
|
+
else:
|
|
180
|
+
# Relative path - create within mooring proc directory
|
|
181
|
+
log_dir = proc_dir / log_directory
|
|
182
|
+
|
|
183
|
+
# Create directory if requested and it doesn't exist
|
|
184
|
+
if create_directory:
|
|
185
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
# Generate filename using pattern
|
|
188
|
+
filename = filename_pattern.format(
|
|
189
|
+
mooring_name=mooring_name, timestamp=timestamp, stage=stage_name
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return log_dir / filename
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""oceanarray.logsheet
|
|
2
|
+
====================
|
|
3
|
+
Generate fieldwork logsheet PDFs from mooring YAML configuration files.
|
|
4
|
+
|
|
5
|
+
Bundled defaults
|
|
6
|
+
----------------
|
|
7
|
+
``column_defs.yaml`` and LaTeX templates are bundled with the package.
|
|
8
|
+
Users do **not** need to supply these.
|
|
9
|
+
|
|
10
|
+
User-supplied config (``--config-dir``)
|
|
11
|
+
----------------------------------------
|
|
12
|
+
- ``cruise_config.yaml`` — cruise metadata, casts list, moorings to recover/deploy,
|
|
13
|
+
filename conventions, paths.
|
|
14
|
+
- ``instruments.yaml`` — lab instrument registry (serial → model, firmware, owner).
|
|
15
|
+
|
|
16
|
+
Optionally, place a ``column_defs.yaml`` or ``latex_templates/`` directory in
|
|
17
|
+
``--config-dir`` to override the bundled defaults.
|
|
18
|
+
|
|
19
|
+
Mooring YAMLs
|
|
20
|
+
-------------
|
|
21
|
+
Each builder looks up the mooring YAML via:
|
|
22
|
+
|
|
23
|
+
1. ``cruise_config.yaml → mooring_yaml_map`` (explicit path mapping), then
|
|
24
|
+
2. ``{proc_dir}/{mooring}/{mooring}.mooring.yaml`` (oceanarray convention).
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
-------
|
|
28
|
+
::
|
|
29
|
+
|
|
30
|
+
from oceanarray.logsheet import build_recovery
|
|
31
|
+
from oceanarray.logsheet._config import resolve_config
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
cfg = resolve_config(
|
|
35
|
+
config_dir=Path("/data/OdB2026/logsheets_config"),
|
|
36
|
+
output_dir=Path("/data/OdB2026/logsheets_output"),
|
|
37
|
+
proc_dir=Path("/data/OdB2026/proc"),
|
|
38
|
+
)
|
|
39
|
+
build_recovery("dsG3_1_2026", cfg, fmt="tex")
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from ._config import LogsheetsConfig, resolve_config
|
|
44
|
+
from .builders import (
|
|
45
|
+
build_caldip_setup,
|
|
46
|
+
build_caldip_download,
|
|
47
|
+
build_recovery,
|
|
48
|
+
build_mooring_download,
|
|
49
|
+
build_deployment_setup,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"LogsheetsConfig",
|
|
54
|
+
"resolve_config",
|
|
55
|
+
"build_caldip_setup",
|
|
56
|
+
"build_caldip_download",
|
|
57
|
+
"build_recovery",
|
|
58
|
+
"build_mooring_download",
|
|
59
|
+
"build_deployment_setup",
|
|
60
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""oceanarray.logsheet._columns
|
|
2
|
+
================================
|
|
3
|
+
Column resolution from the ``column_library``, download-convention
|
|
4
|
+
substitution, and data-path note formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._latex import tex_safe
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_columns(col_list: list, col_library: dict) -> list[dict]:
|
|
11
|
+
"""Resolve a raw column list from ``column_defs.yaml`` into full column dicts.
|
|
12
|
+
|
|
13
|
+
Each item in *col_list* may be:
|
|
14
|
+
|
|
15
|
+
- **plain string** — a key in *col_library*, used as-is.
|
|
16
|
+
- **{key: {overrides}}** — library key with field overrides.
|
|
17
|
+
- **{header: …, width: …, input: …}** — inline full definition.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
col_list:
|
|
22
|
+
Raw list loaded directly from YAML.
|
|
23
|
+
col_library:
|
|
24
|
+
The ``column_library`` section from ``column_defs.yaml``.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
result = []
|
|
28
|
+
for item in col_list:
|
|
29
|
+
if isinstance(item, str):
|
|
30
|
+
col = col_library.get(item, {}).copy()
|
|
31
|
+
if not col:
|
|
32
|
+
print(f" Warning: column key '{item}' not in column_library")
|
|
33
|
+
result.append(col)
|
|
34
|
+
elif isinstance(item, dict):
|
|
35
|
+
if "header" in item:
|
|
36
|
+
result.append(item.copy())
|
|
37
|
+
else:
|
|
38
|
+
key = next(iter(item))
|
|
39
|
+
overrides = item[key] or {}
|
|
40
|
+
col = col_library.get(key, {}).copy()
|
|
41
|
+
if not col:
|
|
42
|
+
print(f" Warning: column key '{key}' not in column_library")
|
|
43
|
+
if overrides:
|
|
44
|
+
col.update(overrides)
|
|
45
|
+
result.append(col)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def apply_download_convention(cols: list[dict], convention: str) -> list[dict]:
|
|
50
|
+
"""Return a copy of *cols* with ``{download_convention}`` in headers replaced."""
|
|
51
|
+
result = []
|
|
52
|
+
for col in cols:
|
|
53
|
+
col = col.copy()
|
|
54
|
+
if "{download_convention}" in col.get("header", ""):
|
|
55
|
+
col["header"] = col["header"].replace("{download_convention}", convention)
|
|
56
|
+
result.append(col)
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_data_path_note(
|
|
61
|
+
note_raw: str,
|
|
62
|
+
raw_data: str = "",
|
|
63
|
+
mooring: str = "",
|
|
64
|
+
caldip_data: str = "",
|
|
65
|
+
cast_label: str = "",
|
|
66
|
+
instrument: str = "",
|
|
67
|
+
download_convention: str = "",
|
|
68
|
+
) -> list[str]:
|
|
69
|
+
"""Substitute path placeholders in a ``data_path_note`` string.
|
|
70
|
+
|
|
71
|
+
``{instrument}`` and ``{mooring}`` are rendered bold in the PDF.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list[str]
|
|
76
|
+
One LaTeX-escaped string per non-blank line.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
if not note_raw:
|
|
80
|
+
return []
|
|
81
|
+
s = note_raw.strip()
|
|
82
|
+
s = s.replace("{raw_data}", raw_data)
|
|
83
|
+
s = s.replace("{caldip_data}", caldip_data)
|
|
84
|
+
s = s.replace("{cast_label}", cast_label)
|
|
85
|
+
s = s.replace("{download_convention}", download_convention)
|
|
86
|
+
|
|
87
|
+
_B_MOORING = "\x00MOORING\x00"
|
|
88
|
+
_B_INSTRUMENT = "\x00INSTRUMENT\x00"
|
|
89
|
+
s = s.replace("{mooring}", _B_MOORING)
|
|
90
|
+
s = s.replace("{instrument}", _B_INSTRUMENT)
|
|
91
|
+
|
|
92
|
+
s = tex_safe(s)
|
|
93
|
+
|
|
94
|
+
s = s.replace(_B_MOORING, r"\textbf{" + tex_safe(mooring) + r"}")
|
|
95
|
+
s = s.replace(_B_INSTRUMENT, r"\textbf{" + tex_safe(instrument) + r"}")
|
|
96
|
+
|
|
97
|
+
return [ln for ln in s.split("\n") if ln.strip()]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""oceanarray.logsheet._config
|
|
2
|
+
=============================
|
|
3
|
+
Path resolution, YAML loading helpers, and instrument registry access.
|
|
4
|
+
|
|
5
|
+
Two categories of config files:
|
|
6
|
+
|
|
7
|
+
* **Bundled** (installed with the package, in ``data/``):
|
|
8
|
+
``column_defs.yaml`` and ``latex_templates/``.
|
|
9
|
+
|
|
10
|
+
* **User-supplied** (via ``--config-dir`` or ``LOGSHEETS_CONFIG_DIR`` env var):
|
|
11
|
+
``logsheet_config.yaml`` and ``instrument_inventory.csv``.
|
|
12
|
+
|
|
13
|
+
Mooring YAMLs are resolved from the oceanarray proc directory
|
|
14
|
+
(``{proc_dir}/{mooring}/{mooring}.mooring.yaml``), with an optional
|
|
15
|
+
``mooring_yaml_map`` override in ``logsheet_config.yaml``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import csv
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Bundled package data
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_DATA = Path(__file__).parent / "data"
|
|
33
|
+
DEFAULT_COLUMN_DEFS: Path = _DATA / "column_defs.yaml"
|
|
34
|
+
DEFAULT_TEMPLATES_DIR: Path = _DATA / "latex_templates"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Config container
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class LogsheetsConfig:
|
|
44
|
+
"""Resolved paths for a logsheet generation run."""
|
|
45
|
+
|
|
46
|
+
cruise_config_path: Path
|
|
47
|
+
instruments_path: Path
|
|
48
|
+
column_defs_path: Path
|
|
49
|
+
templates_dir: Path
|
|
50
|
+
output_dir: Path
|
|
51
|
+
proc_dir: Optional[Path]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_config(
|
|
55
|
+
config_dir: Optional[Path] = None,
|
|
56
|
+
inventory_path: Optional[Path] = None,
|
|
57
|
+
logsheet_config_path: Optional[Path] = None,
|
|
58
|
+
output_dir: Optional[Path] = None,
|
|
59
|
+
proc_dir: Optional[Path] = None,
|
|
60
|
+
) -> LogsheetsConfig:
|
|
61
|
+
"""Build a :class:`LogsheetsConfig` from user-supplied paths.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
config_dir:
|
|
66
|
+
Shorthand: directory containing ``logsheet_config.yaml`` and
|
|
67
|
+
``instrument_inventory.csv``. Ignored for any path supplied explicitly
|
|
68
|
+
via *inventory_path* or *logsheet_config_path*. Falls back to the
|
|
69
|
+
``LOGSHEETS_CONFIG_DIR`` env var, then the current working directory.
|
|
70
|
+
inventory_path:
|
|
71
|
+
Explicit path to ``instrument_inventory.csv``.
|
|
72
|
+
Takes precedence over ``config_dir / instrument_inventory.csv``.
|
|
73
|
+
logsheet_config_path:
|
|
74
|
+
Explicit path to ``logsheet_config.yaml``. Takes precedence over
|
|
75
|
+
``config_dir / logsheet_config.yaml``. Optional; if the file does not
|
|
76
|
+
exist, path/filename fields are left blank on the sheet.
|
|
77
|
+
output_dir:
|
|
78
|
+
Where PDFs are written. Defaults to ``./logsheets/``.
|
|
79
|
+
proc_dir:
|
|
80
|
+
Oceanarray proc directory (parent of per-mooring subdirs). Used to
|
|
81
|
+
locate mooring YAMLs when not found via ``mooring_yaml_map``.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
if config_dir is None and inventory_path is None and logsheet_config_path is None:
|
|
85
|
+
env = os.environ.get("LOGSHEETS_CONFIG_DIR")
|
|
86
|
+
config_dir = Path(env) if env else Path.cwd()
|
|
87
|
+
|
|
88
|
+
# Resolve cruise_config and instruments from explicit paths or config_dir
|
|
89
|
+
resolved_cruise = logsheet_config_path or (
|
|
90
|
+
(config_dir / "cruise_config.yaml")
|
|
91
|
+
if config_dir
|
|
92
|
+
else Path("cruise_config.yaml")
|
|
93
|
+
)
|
|
94
|
+
resolved_inventory = inventory_path or (
|
|
95
|
+
(config_dir / "instruments.yaml") if config_dir else Path("instruments.yaml")
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Allow user to override bundled column_defs.yaml by placing one in config_dir
|
|
99
|
+
user_col_defs = (config_dir / "column_defs.yaml") if config_dir else None
|
|
100
|
+
col_defs_path = (
|
|
101
|
+
user_col_defs
|
|
102
|
+
if (user_col_defs and user_col_defs.exists())
|
|
103
|
+
else DEFAULT_COLUMN_DEFS
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Similarly allow a custom templates/ dir alongside cruise_config
|
|
107
|
+
user_templates = (config_dir / "latex_templates") if config_dir else None
|
|
108
|
+
templates_dir = (
|
|
109
|
+
user_templates
|
|
110
|
+
if (user_templates and user_templates.is_dir())
|
|
111
|
+
else DEFAULT_TEMPLATES_DIR
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return LogsheetsConfig(
|
|
115
|
+
cruise_config_path=resolved_cruise,
|
|
116
|
+
instruments_path=resolved_inventory,
|
|
117
|
+
column_defs_path=col_defs_path,
|
|
118
|
+
templates_dir=templates_dir,
|
|
119
|
+
output_dir=output_dir or Path("logsheets"),
|
|
120
|
+
proc_dir=proc_dir,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# YAML helpers
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_yaml(path: Path) -> dict:
|
|
130
|
+
"""Load and return a YAML file as a dict."""
|
|
131
|
+
with open(path) as fh:
|
|
132
|
+
return yaml.safe_load(fh)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _parse_csv_value(val: str) -> object:
|
|
136
|
+
"""Convert a raw CSV string to a Python scalar.
|
|
137
|
+
|
|
138
|
+
* ``"true"`` / ``"false"`` (case-insensitive) → ``bool``
|
|
139
|
+
* Empty string → ``None``
|
|
140
|
+
* Anything else → left as ``str``
|
|
141
|
+
|
|
142
|
+
Integers and floats are intentionally left as strings so that firmware
|
|
143
|
+
version strings like ``"3.0h"`` or serial suffixes are never miscast.
|
|
144
|
+
"""
|
|
145
|
+
stripped = val.strip()
|
|
146
|
+
lower = stripped.lower()
|
|
147
|
+
if lower == "true":
|
|
148
|
+
return True
|
|
149
|
+
if lower == "false":
|
|
150
|
+
return False
|
|
151
|
+
if lower == "":
|
|
152
|
+
return None
|
|
153
|
+
return stripped
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_instruments(cfg: LogsheetsConfig) -> dict:
|
|
157
|
+
"""Return the instrument registry keyed by ``(type_str, serial_int)``.
|
|
158
|
+
|
|
159
|
+
Reads ``instrument_inventory.csv`` or legacy ``instruments.yaml``.
|
|
160
|
+
CSV files are detected by a ``.csv`` suffix; everything else is treated as
|
|
161
|
+
YAML. Boolean-string fields (``"true"``/``"false"``) in CSV rows are
|
|
162
|
+
converted to Python ``bool`` so that checks like ``entry.get("odo")``
|
|
163
|
+
behave correctly. Example key: ``("microcat", 7507)``.
|
|
164
|
+
"""
|
|
165
|
+
path = cfg.instruments_path
|
|
166
|
+
lookup = {}
|
|
167
|
+
if path.suffix.lower() == ".csv":
|
|
168
|
+
with open(path, newline="") as fh:
|
|
169
|
+
for row in csv.DictReader(fh):
|
|
170
|
+
itype = row.get("type", "").strip()
|
|
171
|
+
serial_str = row.get("serial", "").strip()
|
|
172
|
+
if not itype or not serial_str:
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
sn = int(serial_str)
|
|
176
|
+
except ValueError:
|
|
177
|
+
continue
|
|
178
|
+
entry = {
|
|
179
|
+
k: _parse_csv_value(v)
|
|
180
|
+
for k, v in row.items()
|
|
181
|
+
if k not in ("type", "serial")
|
|
182
|
+
}
|
|
183
|
+
entry["serial"] = sn
|
|
184
|
+
lookup[(itype, sn)] = entry
|
|
185
|
+
else:
|
|
186
|
+
raw = load_yaml(path)
|
|
187
|
+
for itype, entries in raw.items():
|
|
188
|
+
for entry in entries:
|
|
189
|
+
sn = entry["serial"]
|
|
190
|
+
lookup[(itype, sn)] = entry
|
|
191
|
+
return lookup
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def sn_to_entry(instruments: dict, sn: int) -> tuple[str, dict]:
|
|
195
|
+
"""Look up a serial number across all instrument types.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
instruments:
|
|
200
|
+
Registry returned by :func:`load_instruments`.
|
|
201
|
+
sn:
|
|
202
|
+
Serial number to look up.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
tuple[str, dict]
|
|
207
|
+
``(instrument_type, entry_dict)``.
|
|
208
|
+
|
|
209
|
+
Raises
|
|
210
|
+
------
|
|
211
|
+
KeyError
|
|
212
|
+
If the serial number is not found in any type.
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
for itype in ("microcat", "aquadopp", "tr1050", "rbrsolo", "rbrduet", "adcp"):
|
|
216
|
+
if (itype, sn) in instruments:
|
|
217
|
+
return itype, instruments[(itype, sn)]
|
|
218
|
+
raise KeyError(f"SN {sn} not found in instrument_inventory.csv")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def resolve_mooring_yaml(
|
|
222
|
+
mooring: str, cfg: LogsheetsConfig, mooring_yaml_map: dict
|
|
223
|
+
) -> Optional[Path]:
|
|
224
|
+
"""Find the mooring YAML path, trying two strategies in order.
|
|
225
|
+
|
|
226
|
+
1. ``mooring_yaml_map`` in ``logsheet_config.yaml`` (relative paths are
|
|
227
|
+
resolved relative to ``logsheet_config.yaml``'s parent directory).
|
|
228
|
+
2. Oceanarray convention: ``{proc_dir}/{mooring}/{mooring}.mooring.yaml``.
|
|
229
|
+
|
|
230
|
+
Returns ``None`` if neither strategy finds a readable file.
|
|
231
|
+
"""
|
|
232
|
+
if mooring in mooring_yaml_map:
|
|
233
|
+
rel = mooring_yaml_map[mooring]
|
|
234
|
+
p = Path(rel)
|
|
235
|
+
if not p.is_absolute():
|
|
236
|
+
p = cfg.cruise_config_path.parent / rel
|
|
237
|
+
if p.exists():
|
|
238
|
+
return p
|
|
239
|
+
|
|
240
|
+
if cfg.proc_dir is not None:
|
|
241
|
+
candidate = cfg.proc_dir / mooring / f"{mooring}.mooring.yaml"
|
|
242
|
+
if candidate.exists():
|
|
243
|
+
return candidate
|
|
244
|
+
|
|
245
|
+
return None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""oceanarray.logsheet._firmware
|
|
2
|
+
================================
|
|
3
|
+
MicroCAT firmware group detection and display labels.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from packaging.version import Version
|
|
7
|
+
|
|
8
|
+
#: Section-title strings used in generated logsheets.
|
|
9
|
+
FW_GROUP_LABELS: dict[str, str] = {
|
|
10
|
+
"old_seaterm": r"MicroCAT --- Firmware $<$ 3.0d (Seaterm v1, 9600 baud)",
|
|
11
|
+
"seaterm_v2": r"MicroCAT --- Firmware $\geq$ 3.0d (SeatermV2, 38400 baud)",
|
|
12
|
+
"seaterm_v2_odo": r"MicroCAT --- Firmware $\geq$ 3.0d + ODO (SeatermV2, 38400 baud)",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fw_group(entry: dict, sentinel_str: str = "3.0d") -> str:
|
|
17
|
+
"""Determine the firmware group for a MicroCAT instrument entry.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
entry:
|
|
22
|
+
Instrument entry from ``instruments.yaml``.
|
|
23
|
+
sentinel_str:
|
|
24
|
+
Version string separating old_seaterm from seaterm_v2
|
|
25
|
+
(from ``cruise_config.yaml → microcat_firmware_sentinel``).
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
str
|
|
30
|
+
One of ``"old_seaterm"``, ``"seaterm_v2"``, ``"seaterm_v2_odo"``.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
if entry.get("odo"):
|
|
34
|
+
return "seaterm_v2_odo"
|
|
35
|
+
fw = entry.get("firmware")
|
|
36
|
+
if fw is None:
|
|
37
|
+
return "seaterm_v2"
|
|
38
|
+
try:
|
|
39
|
+
if Version(fw) < Version(sentinel_str):
|
|
40
|
+
return "old_seaterm"
|
|
41
|
+
return "seaterm_v2"
|
|
42
|
+
except Exception: # noqa: BLE001
|
|
43
|
+
return "old_seaterm" if fw < sentinel_str else "seaterm_v2"
|