climate-ref-core 0.6.5__tar.gz → 0.6.6__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.
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/PKG-INFO +2 -2
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/pyproject.toml +2 -2
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/constraints.py +4 -2
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/datasets.py +17 -2
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/diagnostics.py +25 -8
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/executor.py +4 -1
- climate_ref_core-0.6.6/src/climate_ref_core/metric_values/typing.py +139 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/providers.py +25 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/controlled_vocabulary.py +33 -15
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/metric_values/test_typing.py +32 -0
- climate_ref_core-0.6.6/tests/unit/test_datasets/dataset_collection_hash.yml +2 -0
- climate_ref_core-0.6.6/tests/unit/test_datasets/dataset_collection_obs4mips_hash.yml +2 -0
- climate_ref_core-0.6.6/tests/unit/test_datasets/metric_dataset_hash.yml +2 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_datasets.py +12 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_providers.py +7 -0
- climate_ref_core-0.6.5/src/climate_ref_core/metric_values/typing.py +0 -74
- climate_ref_core-0.6.5/tests/unit/test_datasets/dataset_collection_hash.yml +0 -2
- climate_ref_core-0.6.5/tests/unit/test_datasets/dataset_collection_obs4mips_hash.yml +0 -2
- climate_ref_core-0.6.5/tests/unit/test_datasets/metric_dataset_hash.yml +0 -2
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/.gitignore +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/LICENCE +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/NOTICE +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/README.md +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/__init__.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/dataset_registry.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/env.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/exceptions.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/logging.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/metric_values/__init__.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/py.typed +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/README.md +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/__init__.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/cv_cmip7_aft.yaml +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/metric.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/output.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/cmec_metric_sample.json +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/cmec_output_sample.json +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/cv_sample.yaml +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/test_metric_json_schema.yml +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/test_output_json_schema.yml +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/conftest.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/test_cmec_metric.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/test_cmec_output.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/test_controlled_vocabulary.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_constraints.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_dataset_registry/test_dataset_registry.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_exceptions.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_executor.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_logging.py +0 -0
- {climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/test_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: climate-ref-core
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: Core library for the CMIP Rapid Evaluation Framework
|
|
5
5
|
Author-email: Jared Lewis <jared.lewis@climate-resource.com>, Mika Pflueger <mika.pflueger@climate-resource.com>, Bouwe Andela <b.andela@esciencecenter.nl>, Jiwoo Lee <lee1043@llnl.gov>, Min Xu <xum1@ornl.gov>, Nathan Collier <collierno@ornl.gov>, Dora Hegedus <dora.hegedus@stfc.ac.uk>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,7 @@ Requires-Dist: pydantic>=2.10.6
|
|
|
29
29
|
Requires-Dist: pyyaml>=6.0.2
|
|
30
30
|
Requires-Dist: requests
|
|
31
31
|
Requires-Dist: rich
|
|
32
|
-
Requires-Dist: setuptools
|
|
32
|
+
Requires-Dist: setuptools<81
|
|
33
33
|
Requires-Dist: typing-extensions
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "climate-ref-core"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.6"
|
|
4
4
|
description = "Core library for the CMIP Rapid Evaluation Framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -39,7 +39,7 @@ dependencies = [
|
|
|
39
39
|
"environs>=11",
|
|
40
40
|
"pyyaml>=6.0.2",
|
|
41
41
|
# Not used directly, but required to support some installations
|
|
42
|
-
"setuptools
|
|
42
|
+
"setuptools<81",
|
|
43
43
|
|
|
44
44
|
# SPEC 0000 constraints
|
|
45
45
|
# We follow [SPEC-0000](https://scientific-python.org/specs/spec-0000/)
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import warnings
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from collections.abc import Mapping
|
|
9
|
-
from typing import Protocol, runtime_checkable
|
|
9
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
10
10
|
|
|
11
11
|
if sys.version_info < (3, 11):
|
|
12
12
|
from typing_extensions import Self
|
|
@@ -148,6 +148,7 @@ class RequireFacets:
|
|
|
148
148
|
|
|
149
149
|
dimension: str
|
|
150
150
|
required_facets: tuple[str, ...]
|
|
151
|
+
operator: Literal["all", "any"] = "all"
|
|
151
152
|
|
|
152
153
|
def validate(self, group: pd.DataFrame) -> bool:
|
|
153
154
|
"""
|
|
@@ -156,7 +157,8 @@ class RequireFacets:
|
|
|
156
157
|
if self.dimension not in group:
|
|
157
158
|
logger.warning(f"Dimension {self.dimension} not present in group {group}")
|
|
158
159
|
return False
|
|
159
|
-
|
|
160
|
+
op = all if self.operator == "all" else any
|
|
161
|
+
return op(value in group[self.dimension].values for value in self.required_facets)
|
|
160
162
|
|
|
161
163
|
|
|
162
164
|
@frozen
|
|
@@ -5,7 +5,7 @@ Dataset management and filtering
|
|
|
5
5
|
import enum
|
|
6
6
|
import functools
|
|
7
7
|
import hashlib
|
|
8
|
-
from collections.abc import Collection, Iterable
|
|
8
|
+
from collections.abc import Collection, Iterable, Iterator
|
|
9
9
|
from typing import Any, Self
|
|
10
10
|
|
|
11
11
|
import pandas as pd
|
|
@@ -172,9 +172,24 @@ class ExecutionDatasetCollection:
|
|
|
172
172
|
def __hash__(self) -> int:
|
|
173
173
|
return hash(self.hash)
|
|
174
174
|
|
|
175
|
+
def __iter__(self) -> Iterator[SourceDatasetType]:
|
|
176
|
+
return iter(self._collection)
|
|
177
|
+
|
|
178
|
+
def keys(self) -> Iterable[SourceDatasetType]:
|
|
179
|
+
"""
|
|
180
|
+
Iterate over the source types in the collection.
|
|
181
|
+
"""
|
|
182
|
+
return self._collection.keys()
|
|
183
|
+
|
|
184
|
+
def values(self) -> Iterable[DatasetCollection]:
|
|
185
|
+
"""
|
|
186
|
+
Iterate over the datasets in the collection.
|
|
187
|
+
"""
|
|
188
|
+
return self._collection.values()
|
|
189
|
+
|
|
175
190
|
def items(self) -> Iterable[tuple[SourceDatasetType, DatasetCollection]]:
|
|
176
191
|
"""
|
|
177
|
-
Iterate over the
|
|
192
|
+
Iterate over the items in the collection.
|
|
178
193
|
"""
|
|
179
194
|
return self._collection.items()
|
|
180
195
|
|
|
@@ -14,6 +14,7 @@ from attrs import field, frozen
|
|
|
14
14
|
from climate_ref_core.constraints import GroupConstraint
|
|
15
15
|
from climate_ref_core.datasets import ExecutionDatasetCollection, FacetFilter, SourceDatasetType
|
|
16
16
|
from climate_ref_core.metric_values import SeriesMetricValue
|
|
17
|
+
from climate_ref_core.metric_values.typing import SeriesDefinition
|
|
17
18
|
from climate_ref_core.pycmec.metric import CMECMetric
|
|
18
19
|
from climate_ref_core.pycmec.output import CMECOutput
|
|
19
20
|
|
|
@@ -182,9 +183,11 @@ class ExecutionResult:
|
|
|
182
183
|
Whether the diagnostic execution ran successfully.
|
|
183
184
|
"""
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
series_filename: pathlib.Path | None = None
|
|
186
187
|
"""
|
|
187
188
|
A collection of series metric values that were extracted from the execution.
|
|
189
|
+
|
|
190
|
+
These are written to a CSV file in the output directory.
|
|
188
191
|
"""
|
|
189
192
|
|
|
190
193
|
@staticmethod
|
|
@@ -193,6 +196,7 @@ class ExecutionResult:
|
|
|
193
196
|
*,
|
|
194
197
|
cmec_output_bundle: CMECOutput | dict[str, Any],
|
|
195
198
|
cmec_metric_bundle: CMECMetric | dict[str, Any],
|
|
199
|
+
series: Sequence[SeriesMetricValue] = tuple(),
|
|
196
200
|
) -> ExecutionResult:
|
|
197
201
|
"""
|
|
198
202
|
Build a ExecutionResult from a CMEC output bundle.
|
|
@@ -205,6 +209,8 @@ class ExecutionResult:
|
|
|
205
209
|
An output bundle in the CMEC format.
|
|
206
210
|
cmec_metric_bundle
|
|
207
211
|
An diagnostic bundle in the CMEC format.
|
|
212
|
+
series
|
|
213
|
+
Series metric values extracted from the execution.
|
|
208
214
|
|
|
209
215
|
Returns
|
|
210
216
|
-------
|
|
@@ -223,17 +229,21 @@ class ExecutionResult:
|
|
|
223
229
|
cmec_metric = cmec_metric_bundle
|
|
224
230
|
|
|
225
231
|
definition.to_output_path(filename=None).mkdir(parents=True, exist_ok=True)
|
|
226
|
-
bundle_path = definition.to_output_path("output.json")
|
|
227
|
-
cmec_output.dump_to_json(bundle_path)
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
output_filename = "output.json"
|
|
234
|
+
metric_filename = "diagnostic.json"
|
|
235
|
+
series_filename = "series.json"
|
|
232
236
|
|
|
237
|
+
cmec_output.dump_to_json(definition.to_output_path(output_filename))
|
|
238
|
+
cmec_metric.dump_to_json(definition.to_output_path(metric_filename))
|
|
239
|
+
SeriesMetricValue.dump_to_json(definition.to_output_path(series_filename), series)
|
|
240
|
+
|
|
241
|
+
# We are using relative paths for the output files for portability of the results
|
|
233
242
|
return ExecutionResult(
|
|
234
243
|
definition=definition,
|
|
235
|
-
output_bundle_filename=pathlib.Path(
|
|
236
|
-
metric_bundle_filename=pathlib.Path(
|
|
244
|
+
output_bundle_filename=pathlib.Path(output_filename),
|
|
245
|
+
metric_bundle_filename=pathlib.Path(metric_filename),
|
|
246
|
+
series_filename=pathlib.Path(series_filename),
|
|
237
247
|
successful=True,
|
|
238
248
|
)
|
|
239
249
|
|
|
@@ -432,6 +442,11 @@ class AbstractDiagnostic(Protocol):
|
|
|
432
442
|
is raised.
|
|
433
443
|
"""
|
|
434
444
|
|
|
445
|
+
series: Sequence[SeriesDefinition]
|
|
446
|
+
"""
|
|
447
|
+
Definition of the series that are produced by the diagnostic.
|
|
448
|
+
"""
|
|
449
|
+
|
|
435
450
|
provider: DiagnosticProvider
|
|
436
451
|
"""
|
|
437
452
|
The provider that provides the diagnostic.
|
|
@@ -493,6 +508,8 @@ class Diagnostic(AbstractDiagnostic):
|
|
|
493
508
|
See (climate_ref_example.example.ExampleDiagnostic)[] for an example implementation.
|
|
494
509
|
"""
|
|
495
510
|
|
|
511
|
+
series: Sequence[SeriesDefinition] = tuple()
|
|
512
|
+
|
|
496
513
|
def __init__(self) -> None:
|
|
497
514
|
super().__init__()
|
|
498
515
|
self._provider: DiagnosticProvider | None = None
|
|
@@ -160,12 +160,15 @@ def import_executor_cls(fqn: str) -> type[Executor]:
|
|
|
160
160
|
imp = importlib.import_module(module)
|
|
161
161
|
executor: type[Executor] = getattr(imp, attribute_name)
|
|
162
162
|
|
|
163
|
+
if isinstance(executor, Exception):
|
|
164
|
+
raise executor
|
|
165
|
+
|
|
163
166
|
# We can't really check if the executor is a subclass of Executor here
|
|
164
167
|
# Protocols can't be used with issubclass if they have non-method members
|
|
165
168
|
# We have to check this at class instantiation time
|
|
166
169
|
|
|
167
170
|
return executor
|
|
168
|
-
except ModuleNotFoundError:
|
|
171
|
+
except (ModuleNotFoundError, ImportError):
|
|
169
172
|
logger.error(f"Package '{fqn}' not found")
|
|
170
173
|
raise InvalidExecutorException(fqn, f"Module '{module}' not found")
|
|
171
174
|
except AttributeError:
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, model_validator
|
|
7
|
+
|
|
8
|
+
Value = float | int
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SeriesDefinition(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
A definition of a 1-d array with an associated index and additional dimensions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
file_pattern: str
|
|
17
|
+
"""A glob pattern to match files that contain the series values."""
|
|
18
|
+
|
|
19
|
+
sel: dict[str, Any] | None = None
|
|
20
|
+
"""A dictionary of selection criteria to apply with :meth:`xarray.Dataset.sel` after loading the file."""
|
|
21
|
+
|
|
22
|
+
dimensions: dict[str, str]
|
|
23
|
+
"""Key, value pairs that identify the dimensions of the metric."""
|
|
24
|
+
|
|
25
|
+
values_name: str
|
|
26
|
+
"""The name of the variable in the file that contains the values of the series."""
|
|
27
|
+
|
|
28
|
+
index_name: str
|
|
29
|
+
"""The name of the variable in the file that contains the index of the series."""
|
|
30
|
+
|
|
31
|
+
attributes: Sequence[str]
|
|
32
|
+
"""A list of attributes that should be extracted from the file and included in the series metadata."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SeriesMetricValue(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
A 1-d array with an associated index and additional dimensions
|
|
38
|
+
|
|
39
|
+
These values are typically sourced from the CMEC metrics bundle
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
dimensions: dict[str, str]
|
|
43
|
+
"""
|
|
44
|
+
Key, value pairs that identify the dimensions of the metric
|
|
45
|
+
|
|
46
|
+
These values are used for a faceted search of the metric values.
|
|
47
|
+
"""
|
|
48
|
+
values: Sequence[Value]
|
|
49
|
+
"""
|
|
50
|
+
A 1-d array of values
|
|
51
|
+
"""
|
|
52
|
+
index: Sequence[str | Value]
|
|
53
|
+
"""
|
|
54
|
+
A 1-d array of index values
|
|
55
|
+
|
|
56
|
+
Values must be strings or numbers and have the same length as values.
|
|
57
|
+
Non-unique index values are not allowed.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
index_name: str
|
|
61
|
+
"""
|
|
62
|
+
The name of the index.
|
|
63
|
+
|
|
64
|
+
This is used for presentation purposes and is not used in the controlled vocabulary.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
attributes: dict[str, str | Value] | None = None
|
|
68
|
+
"""
|
|
69
|
+
Additional unstructured attributes associated with the metric value
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@model_validator(mode="after")
|
|
73
|
+
def validate_index_length(self) -> Self:
|
|
74
|
+
"""Validate that index has the same length as values"""
|
|
75
|
+
if len(self.index) != len(self.values):
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Index length ({len(self.index)}) must match values length ({len(self.values)})"
|
|
78
|
+
)
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def dump_to_json(cls, path: Path, series: Sequence["SeriesMetricValue"]) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Dump a sequence of SeriesMetricValue to a JSON file.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
path
|
|
89
|
+
The path to the JSON file.
|
|
90
|
+
|
|
91
|
+
The directory containing this file must already exist.
|
|
92
|
+
This file will be overwritten if it already exists.
|
|
93
|
+
series
|
|
94
|
+
The series values to dump.
|
|
95
|
+
"""
|
|
96
|
+
with open(path, "w") as f:
|
|
97
|
+
json.dump([s.model_dump() for s in series], f, indent=2)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def load_from_json(
|
|
101
|
+
cls,
|
|
102
|
+
path: Path,
|
|
103
|
+
) -> list["SeriesMetricValue"]:
|
|
104
|
+
"""
|
|
105
|
+
Dump a sequence of SeriesMetricValue to a JSON file.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
path
|
|
110
|
+
The path to the JSON file.
|
|
111
|
+
"""
|
|
112
|
+
with open(path) as f:
|
|
113
|
+
data = json.load(f)
|
|
114
|
+
|
|
115
|
+
if not isinstance(data, list):
|
|
116
|
+
raise ValueError(f"Expected a list of series values, got {type(data)}")
|
|
117
|
+
|
|
118
|
+
return [cls.model_validate(s) for s in data]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ScalarMetricValue(BaseModel):
|
|
122
|
+
"""
|
|
123
|
+
A scalar value with an associated dimensions
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
dimensions: dict[str, str]
|
|
127
|
+
"""
|
|
128
|
+
Key, value pairs that identify the dimensions of the metric
|
|
129
|
+
|
|
130
|
+
These values are used for a faceted search of the metric values.
|
|
131
|
+
"""
|
|
132
|
+
value: Value
|
|
133
|
+
"""
|
|
134
|
+
A scalar value
|
|
135
|
+
"""
|
|
136
|
+
attributes: dict[str, str | Value] | None = None
|
|
137
|
+
"""
|
|
138
|
+
Additional unstructured attributes associated with the metric value
|
|
139
|
+
"""
|
|
@@ -232,6 +232,27 @@ def _get_micromamba_url() -> str:
|
|
|
232
232
|
class CondaDiagnosticProvider(CommandLineDiagnosticProvider):
|
|
233
233
|
"""
|
|
234
234
|
A provider for diagnostics that can be run from the command line in a conda environment.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
name
|
|
239
|
+
The name of the provider.
|
|
240
|
+
version
|
|
241
|
+
The version of the provider.
|
|
242
|
+
slug
|
|
243
|
+
A slugified version of the name.
|
|
244
|
+
repo
|
|
245
|
+
URL of the git repository to install a development version of the package from.
|
|
246
|
+
tag_or_commit
|
|
247
|
+
Tag or commit to install from the `repo` repository.
|
|
248
|
+
|
|
249
|
+
Attributes
|
|
250
|
+
----------
|
|
251
|
+
env_vars
|
|
252
|
+
Environment variables to set when running commands in the conda environment.
|
|
253
|
+
url
|
|
254
|
+
URL to install a development version of the package from.
|
|
255
|
+
|
|
235
256
|
"""
|
|
236
257
|
|
|
237
258
|
def __init__(
|
|
@@ -246,6 +267,7 @@ class CondaDiagnosticProvider(CommandLineDiagnosticProvider):
|
|
|
246
267
|
self._conda_exe: Path | None = None
|
|
247
268
|
self._prefix: Path | None = None
|
|
248
269
|
self.url = f"git+{repo}@{tag_or_commit}" if repo and tag_or_commit else None
|
|
270
|
+
self.env_vars: dict[str, str] = {}
|
|
249
271
|
|
|
250
272
|
@property
|
|
251
273
|
def prefix(self) -> Path:
|
|
@@ -404,6 +426,8 @@ class CondaDiagnosticProvider(CommandLineDiagnosticProvider):
|
|
|
404
426
|
*cmd,
|
|
405
427
|
]
|
|
406
428
|
logger.info(f"Running '{' '.join(cmd)}'")
|
|
429
|
+
env_vars = os.environ.copy()
|
|
430
|
+
env_vars.update(self.env_vars)
|
|
407
431
|
try:
|
|
408
432
|
# This captures the log output until the execution is complete
|
|
409
433
|
# We could poll using `subprocess.Popen` if we want something more responsive
|
|
@@ -413,6 +437,7 @@ class CondaDiagnosticProvider(CommandLineDiagnosticProvider):
|
|
|
413
437
|
stdout=subprocess.PIPE,
|
|
414
438
|
stderr=subprocess.STDOUT,
|
|
415
439
|
text=True,
|
|
440
|
+
env=env_vars,
|
|
416
441
|
)
|
|
417
442
|
logger.info("Command output: \n" + res.stdout)
|
|
418
443
|
logger.info("Command execution successful")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pathlib
|
|
2
|
+
from collections.abc import Iterable, Sequence
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
from attrs import field, frozen, validators
|
|
@@ -7,6 +8,7 @@ from loguru import logger
|
|
|
7
8
|
from yaml import safe_load
|
|
8
9
|
|
|
9
10
|
from climate_ref_core.exceptions import ResultValidationError
|
|
11
|
+
from climate_ref_core.metric_values import ScalarMetricValue, SeriesMetricValue
|
|
10
12
|
from climate_ref_core.pycmec.metric import CMECMetric
|
|
11
13
|
|
|
12
14
|
RESERVED_DIMENSION_NAMES = {"attributes", "json_structure", "created_at", "updated_at", "value", "id"}
|
|
@@ -122,33 +124,49 @@ class CV:
|
|
|
122
124
|
return dim
|
|
123
125
|
raise KeyError(f"Dimension {name} not found")
|
|
124
126
|
|
|
125
|
-
def
|
|
127
|
+
def _validate_value(self, metric_value: ScalarMetricValue | SeriesMetricValue) -> None:
|
|
126
128
|
"""
|
|
127
|
-
Validate a
|
|
129
|
+
Validate a single metric value against the CV
|
|
130
|
+
"""
|
|
131
|
+
for k, v in metric_value.dimensions.items():
|
|
132
|
+
try:
|
|
133
|
+
dimension = self.get_dimension_by_name(k)
|
|
134
|
+
except KeyError:
|
|
135
|
+
raise ResultValidationError(f"Unknown dimension: {k!r}")
|
|
136
|
+
if not dimension.allow_extra_values:
|
|
137
|
+
if v not in [dv.name for dv in dimension.values]:
|
|
138
|
+
raise ResultValidationError(f"Unknown value {v!r} for dimension {k!r}")
|
|
139
|
+
|
|
140
|
+
if hasattr(metric_value, "value") and not isinstance(metric_value.value, float): # pragma: no cover
|
|
141
|
+
# This may not be possible with the current CMECMetric implementation
|
|
142
|
+
raise ResultValidationError(f"Unexpected value: {metric_value.value!r}")
|
|
143
|
+
|
|
144
|
+
def validate_metrics(self, metric_value_collection: CMECMetric | Sequence[SeriesMetricValue]) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Validate a set of metric values (either scalar or series) against a CV
|
|
128
147
|
|
|
129
148
|
The CV describes the accepted dimensions and values within a bundle
|
|
130
149
|
|
|
131
150
|
Parameters
|
|
132
151
|
----------
|
|
133
|
-
|
|
152
|
+
metric_value_collection
|
|
153
|
+
A collection of metric values to validate.
|
|
154
|
+
|
|
155
|
+
This can be a CMECMetric instance or a sequence of SeriesMetricValue instances.
|
|
134
156
|
|
|
135
157
|
Raises
|
|
136
158
|
------
|
|
137
159
|
ResultValidationError
|
|
138
160
|
If the validation of the dimensions or values fails
|
|
139
161
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
raise ResultValidationError(f"Unknown value {v!r} for dimension {k!r}")
|
|
149
|
-
if not isinstance(result.value, float): # pragma: no cover
|
|
150
|
-
# This may not be possible with the current CMECMetric implementation
|
|
151
|
-
raise ResultValidationError(f"Unexpected value: {result.value!r}")
|
|
162
|
+
generator: Iterable[SeriesMetricValue | ScalarMetricValue]
|
|
163
|
+
if isinstance(metric_value_collection, CMECMetric):
|
|
164
|
+
generator = metric_value_collection.iter_results()
|
|
165
|
+
else:
|
|
166
|
+
generator = iter(metric_value_collection)
|
|
167
|
+
|
|
168
|
+
for result in generator:
|
|
169
|
+
self._validate_value(result)
|
|
152
170
|
|
|
153
171
|
@staticmethod
|
|
154
172
|
def load_from_file(filename: pathlib.Path | str) -> "CV":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
@@ -65,3 +66,34 @@ class TestSeriesMetricValue:
|
|
|
65
66
|
index=[1.0],
|
|
66
67
|
index_name="time",
|
|
67
68
|
)
|
|
69
|
+
|
|
70
|
+
def test_dump_and_load_json(self, tmp_path: Path):
|
|
71
|
+
series = [
|
|
72
|
+
SeriesMetricValue(
|
|
73
|
+
dimensions={"model": "test1"},
|
|
74
|
+
values=[1.0, 2.0, 3.0],
|
|
75
|
+
index=[0, 1, 2],
|
|
76
|
+
index_name="time",
|
|
77
|
+
attributes={"attr": "value1"},
|
|
78
|
+
),
|
|
79
|
+
SeriesMetricValue(
|
|
80
|
+
dimensions={"model": "test2"},
|
|
81
|
+
values=[4.0, 5.0],
|
|
82
|
+
index=["a", "b"],
|
|
83
|
+
index_name="other",
|
|
84
|
+
attributes=None,
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
path = tmp_path / "test.json"
|
|
88
|
+
|
|
89
|
+
SeriesMetricValue.dump_to_json(path, series)
|
|
90
|
+
loaded_series = SeriesMetricValue.load_from_json(path)
|
|
91
|
+
|
|
92
|
+
assert loaded_series == series
|
|
93
|
+
|
|
94
|
+
def test_load_from_json_not_a_list(self, tmp_path: Path):
|
|
95
|
+
path = tmp_path / "test.json"
|
|
96
|
+
path.write_text('{"not": "a list"}')
|
|
97
|
+
|
|
98
|
+
with pytest.raises(ValueError, match="Expected a list of series values, got <class 'dict'>"):
|
|
99
|
+
SeriesMetricValue.load_from_json(path)
|
|
@@ -35,6 +35,18 @@ class TestMetricDataset:
|
|
|
35
35
|
with pytest.raises(KeyError):
|
|
36
36
|
metric_dataset["cmip7"]
|
|
37
37
|
|
|
38
|
+
def test_iter(self, metric_dataset):
|
|
39
|
+
assert tuple(iter(metric_dataset)) == tuple(iter(metric_dataset._collection))
|
|
40
|
+
|
|
41
|
+
def test_keys(self, metric_dataset):
|
|
42
|
+
assert metric_dataset.keys() == metric_dataset._collection.keys()
|
|
43
|
+
|
|
44
|
+
def test_values(self, metric_dataset):
|
|
45
|
+
assert tuple(metric_dataset.values()) == tuple(metric_dataset._collection.values())
|
|
46
|
+
|
|
47
|
+
def test_items(self, metric_dataset):
|
|
48
|
+
assert metric_dataset.items() == metric_dataset._collection.items()
|
|
49
|
+
|
|
38
50
|
def test_python_hash(self, metric_dataset, cmip6_data_catalog, data_regression):
|
|
39
51
|
dataset_hash = hash(metric_dataset)
|
|
40
52
|
|
|
@@ -294,6 +294,12 @@ class TestCondaMetricsProvider:
|
|
|
294
294
|
):
|
|
295
295
|
provider.run(["mock-command"])
|
|
296
296
|
else:
|
|
297
|
+
mocker.patch.object(
|
|
298
|
+
climate_ref_core.providers.os.environ,
|
|
299
|
+
"copy",
|
|
300
|
+
return_value={"existing_var": "existing_value"},
|
|
301
|
+
)
|
|
302
|
+
provider.env_vars = {"test_var": "test_value"}
|
|
297
303
|
provider.run(["mock-command"])
|
|
298
304
|
|
|
299
305
|
run.assert_called_with(
|
|
@@ -308,4 +314,5 @@ class TestCondaMetricsProvider:
|
|
|
308
314
|
stdout=subprocess.PIPE,
|
|
309
315
|
stderr=subprocess.STDOUT,
|
|
310
316
|
text=True,
|
|
317
|
+
env={"existing_var": "existing_value", "test_var": "test_value"},
|
|
311
318
|
)
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from typing import Self
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, model_validator
|
|
5
|
-
|
|
6
|
-
Value = float | int
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SeriesMetricValue(BaseModel):
|
|
10
|
-
"""
|
|
11
|
-
A 1-d array with an associated index and additional dimensions
|
|
12
|
-
|
|
13
|
-
These values are typically sourced from the CMEC metrics bundle
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
dimensions: dict[str, str]
|
|
17
|
-
"""
|
|
18
|
-
Key, value pairs that identify the dimensions of the metric
|
|
19
|
-
|
|
20
|
-
These values are used for a faceted search of the metric values.
|
|
21
|
-
"""
|
|
22
|
-
values: Sequence[Value]
|
|
23
|
-
"""
|
|
24
|
-
A 1-d array of values
|
|
25
|
-
"""
|
|
26
|
-
index: Sequence[str | Value]
|
|
27
|
-
"""
|
|
28
|
-
A 1-d array of index values
|
|
29
|
-
|
|
30
|
-
Values must be strings or numbers and have the same length as values.
|
|
31
|
-
Non-unique index values are not allowed.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
index_name: str
|
|
35
|
-
"""
|
|
36
|
-
The name of the index.
|
|
37
|
-
|
|
38
|
-
This is used for presentation purposes and is not used in the controlled vocabulary.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
attributes: dict[str, str | Value] | None = None
|
|
42
|
-
"""
|
|
43
|
-
Additional unstructured attributes associated with the metric value
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
@model_validator(mode="after")
|
|
47
|
-
def validate_index_length(self) -> Self:
|
|
48
|
-
"""Validate that index has the same length as values"""
|
|
49
|
-
if len(self.index) != len(self.values):
|
|
50
|
-
raise ValueError(
|
|
51
|
-
f"Index length ({len(self.index)}) must match values length ({len(self.values)})"
|
|
52
|
-
)
|
|
53
|
-
return self
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class ScalarMetricValue(BaseModel):
|
|
57
|
-
"""
|
|
58
|
-
A scalar value with an associated dimensions
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
dimensions: dict[str, str]
|
|
62
|
-
"""
|
|
63
|
-
Key, value pairs that identify the dimensions of the metric
|
|
64
|
-
|
|
65
|
-
These values are used for a faceted search of the metric values.
|
|
66
|
-
"""
|
|
67
|
-
value: Value
|
|
68
|
-
"""
|
|
69
|
-
A scalar value
|
|
70
|
-
"""
|
|
71
|
-
attributes: dict[str, str | Value] | None = None
|
|
72
|
-
"""
|
|
73
|
-
Additional unstructured attributes associated with the metric value
|
|
74
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/metric_values/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/src/climate_ref_core/pycmec/cv_cmip7_aft.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/cmec_testdata/cv_sample.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.6.5 → climate_ref_core-0.6.6}/tests/unit/pycmec/test_controlled_vocabulary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|