climate-ref-pmp 0.8.0__tar.gz → 0.9.0__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.
Files changed (31) hide show
  1. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/.gitignore +10 -1
  2. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/PKG-INFO +1 -1
  3. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/pyproject.toml +1 -1
  4. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/__init__.py +38 -1
  5. climate_ref_pmp-0.9.0/tests/integration/test_diagnostics.py +137 -0
  6. climate_ref_pmp-0.9.0/tests/unit/test_provider.py +102 -0
  7. climate_ref_pmp-0.8.0/tests/integration/test_diagnostics.py +0 -27
  8. climate_ref_pmp-0.8.0/tests/unit/test_provider.py +0 -9
  9. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/LICENCE +0 -0
  10. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/NOTICE +0 -0
  11. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/README.md +0 -0
  12. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/conftest.py +0 -0
  13. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/dataset_registry/pmp_climatology.txt +0 -0
  14. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/diagnostics/__init__.py +0 -0
  15. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/diagnostics/annual_cycle.py +0 -0
  16. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/diagnostics/enso.py +0 -0
  17. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/diagnostics/variability_modes.py +0 -0
  18. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/drivers/enso_driver.py +0 -0
  19. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/params/pmp_param_MoV-psl.py +0 -0
  20. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/params/pmp_param_MoV-ts.py +0 -0
  21. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/params/pmp_param_annualcycle_1-clims.py +0 -0
  22. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/params/pmp_param_annualcycle_2-metrics.py +0 -0
  23. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/pmp_driver.py +0 -0
  24. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/py.typed +0 -0
  25. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/requirements/conda-lock.yml +0 -0
  26. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/src/climate_ref_pmp/requirements/environment.yml +0 -0
  27. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/tests/unit/conftest.py +0 -0
  28. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/tests/unit/test_annual_cycle.py +0 -0
  29. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/tests/unit/test_enso.py +0 -0
  30. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/tests/unit/test_pmp_driver.py +0 -0
  31. {climate_ref_pmp-0.8.0 → climate_ref_pmp-0.9.0}/tests/unit/test_variability_modes.py +0 -0
@@ -150,7 +150,7 @@ dmypy.json
150
150
 
151
151
  # Generated output
152
152
  out
153
- .ref
153
+ .ref*
154
154
 
155
155
  # Ignore copied LICENCE/NOTICE files
156
156
  packages/*/LICENCE
@@ -158,3 +158,12 @@ packages/*/NOTICE
158
158
 
159
159
  # Local directory for data
160
160
  /data
161
+
162
+ # Generated SDK
163
+ /climate_ref_client
164
+
165
+ # User-specific catalog paths (test data)
166
+ *.paths.yaml
167
+
168
+ # Helm dependencies
169
+ helm/charts/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: climate-ref-pmp
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: PMP diagnostic provider for the Rapid Evaluation Framework
5
5
  Author-email: Jiwoo Lee <jwlee@llnl.gov>, Jared Lewis <jared.lewis@climate-resource.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "climate-ref-pmp"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  description = "PMP diagnostic provider for the Rapid Evaluation Framework"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -6,11 +6,18 @@ from __future__ import annotations
6
6
 
7
7
  import importlib.metadata
8
8
  import os
9
+ from pathlib import Path
9
10
  from typing import TYPE_CHECKING
10
11
 
12
+ import pooch
11
13
  from loguru import logger
12
14
 
13
- from climate_ref_core.dataset_registry import DATASET_URL, dataset_registry_manager
15
+ from climate_ref_core.dataset_registry import (
16
+ DATASET_URL,
17
+ dataset_registry_manager,
18
+ fetch_all_files,
19
+ validate_registry_cache,
20
+ )
14
21
  from climate_ref_core.providers import CondaDiagnosticProvider
15
22
  from climate_ref_pmp.diagnostics import ENSO, AnnualCycle, ExtratropicalModesOfVariability
16
23
 
@@ -19,6 +26,8 @@ if TYPE_CHECKING:
19
26
 
20
27
  __version__ = importlib.metadata.version("climate-ref-pmp")
21
28
 
29
+ _REGISTRY_NAME = "pmp-climatology"
30
+
22
31
 
23
32
  # Create the PMP diagnostics provider
24
33
  # PMP uses a conda environment to run the diagnostics
@@ -38,6 +47,34 @@ class PMPDiagnosticProvider(CondaDiagnosticProvider):
38
47
  logger.debug("Setting env variable 'FI_PROVIDER=tcp'")
39
48
  self.env_vars["FI_PROVIDER"] = "tcp"
40
49
 
50
+ def fetch_data(self, config: Config) -> None:
51
+ """Fetch PMP climatology data."""
52
+ registry = dataset_registry_manager[_REGISTRY_NAME]
53
+ fetch_all_files(registry, _REGISTRY_NAME, output_dir=None)
54
+
55
+ def validate_setup(self, config: Config) -> bool:
56
+ """Validate conda environment and data checksums."""
57
+ # First check conda environment
58
+ if not super().validate_setup(config):
59
+ return False
60
+
61
+ # Then check data checksums
62
+ registry = dataset_registry_manager[_REGISTRY_NAME]
63
+ errors = validate_registry_cache(registry, _REGISTRY_NAME)
64
+ if errors:
65
+ for error in errors:
66
+ logger.error(f"{self.slug} validation failed: {error}")
67
+ logger.error(
68
+ f"Data for {self.slug} is missing or corrupted. "
69
+ f"Please run `ref providers setup --provider {self.slug}` to fetch data."
70
+ )
71
+ return False
72
+ return True
73
+
74
+ def get_data_path(self) -> Path | None:
75
+ """Get the path where PMP data is cached."""
76
+ return Path(pooch.os_cache("climate_ref"))
77
+
41
78
 
42
79
  provider = PMPDiagnosticProvider("PMP", __version__)
43
80
 
@@ -0,0 +1,137 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ from climate_ref_pmp import provider
5
+
6
+ from climate_ref.testing import TestCaseRunner, validate_result
7
+ from climate_ref_core.diagnostics import Diagnostic
8
+ from climate_ref_core.testing import (
9
+ RegressionValidator,
10
+ TestCasePaths,
11
+ collect_test_case_params,
12
+ load_datasets_from_yaml,
13
+ )
14
+
15
+
16
+ @pytest.fixture(scope="session")
17
+ def provider_test_data_dir() -> Path:
18
+ """Path to the package-local test data directory."""
19
+ return Path(__file__).parent.parent / "test-data"
20
+
21
+
22
+ enso_skip_diagnostics = [
23
+ "enso-teleconnections",
24
+ "enso-clivar-teleconnections",
25
+ ]
26
+
27
+ diagnostics = [
28
+ pytest.param(
29
+ diagnostic,
30
+ id=diagnostic.slug,
31
+ marks=[
32
+ *(
33
+ [pytest.mark.skip(reason="ENSO diagnostics do not support the sample data")]
34
+ if diagnostic.slug in enso_skip_diagnostics
35
+ else []
36
+ ),
37
+ ],
38
+ )
39
+ for diagnostic in provider.diagnostics()
40
+ ]
41
+
42
+ # Test case params for parameterized test_case tests
43
+ test_case_params = collect_test_case_params(provider)
44
+
45
+
46
+ @pytest.mark.slow
47
+ @pytest.mark.parametrize("diagnostic", diagnostics)
48
+ def test_diagnostics(diagnostic: Diagnostic, diagnostic_validation):
49
+ if "enso" in diagnostic.slug:
50
+ pytest.skip("ENSO diagnostics do not support the sample data")
51
+
52
+ validator = diagnostic_validation(diagnostic)
53
+
54
+ definition = validator.get_definition()
55
+ validator.execute(definition)
56
+
57
+
58
+ @pytest.mark.parametrize("diagnostic", diagnostics)
59
+ def test_build_results(diagnostic: Diagnostic, diagnostic_validation):
60
+ validator = diagnostic_validation(diagnostic)
61
+
62
+ definition = validator.get_regression_definition()
63
+ validator.validate(definition)
64
+ validator.execution_regression.check(definition.key, definition.output_directory)
65
+
66
+
67
+ @pytest.mark.parametrize("diagnostic,test_case_name", test_case_params)
68
+ def test_validate_test_case_regression(
69
+ diagnostic: Diagnostic,
70
+ test_case_name: str,
71
+ provider_test_data_dir: Path,
72
+ config,
73
+ tmp_path: Path,
74
+ ):
75
+ """
76
+ Validate pre-stored test case regression outputs as CMEC bundles.
77
+
78
+ Each diagnostic/test_case is a separate parameterized test.
79
+ """
80
+ diagnostic.provider.configure(config)
81
+
82
+ paths = TestCasePaths.from_test_data_dir(
83
+ provider_test_data_dir,
84
+ diagnostic.slug,
85
+ test_case_name,
86
+ )
87
+
88
+ if not paths.catalog.exists():
89
+ pytest.skip(f"No catalog file for {diagnostic.slug}/{test_case_name}")
90
+ if not paths.regression.exists():
91
+ pytest.skip(f"No regression data for {diagnostic.slug}/{test_case_name}")
92
+
93
+ validator = RegressionValidator(
94
+ diagnostic=diagnostic,
95
+ test_case_name=test_case_name,
96
+ test_data_dir=provider_test_data_dir,
97
+ )
98
+
99
+ definition = validator.load_regression_definition(tmp_path / diagnostic.slug / test_case_name)
100
+ validator.validate(definition)
101
+
102
+
103
+ @pytest.mark.slow
104
+ @pytest.mark.test_cases
105
+ @pytest.mark.parametrize("diagnostic,test_case_name", test_case_params)
106
+ def test_run_test_cases(
107
+ diagnostic: Diagnostic,
108
+ test_case_name: str,
109
+ provider_test_data_dir: Path,
110
+ config,
111
+ tmp_path: Path,
112
+ ):
113
+ """
114
+ Run diagnostic test cases end-to-end with ESGF data.
115
+
116
+ Requires: `ref test-cases fetch --provider pmp` to have been run first.
117
+ """
118
+ diagnostic.provider.configure(config)
119
+
120
+ paths = TestCasePaths.from_test_data_dir(
121
+ provider_test_data_dir,
122
+ diagnostic.slug,
123
+ test_case_name,
124
+ )
125
+
126
+ if not paths.catalog.exists():
127
+ pytest.skip(f"No catalog file for {diagnostic.slug}/{test_case_name}")
128
+
129
+ datasets = load_datasets_from_yaml(paths.catalog)
130
+
131
+ runner = TestCaseRunner(config=config, datasets=datasets)
132
+ output_dir = tmp_path / diagnostic.slug / test_case_name
133
+
134
+ result = runner.run(diagnostic, test_case_name, output_dir)
135
+
136
+ assert result.successful, f"Diagnostic {diagnostic.slug} failed"
137
+ validate_result(diagnostic, config, result)
@@ -0,0 +1,102 @@
1
+ from pathlib import Path
2
+
3
+ import pooch
4
+ from climate_ref_pmp import PMPDiagnosticProvider, __version__, provider
5
+
6
+
7
+ def test_provider():
8
+ assert provider.name == "PMP"
9
+ assert provider.slug == "pmp"
10
+ assert provider.version == __version__
11
+
12
+ assert len(provider) >= 1
13
+
14
+
15
+ class TestPMPProviderHooks:
16
+ """Tests for PMPDiagnosticProvider lifecycle hooks."""
17
+
18
+ def test_get_data_path(self):
19
+ """Test that get_data_path returns the pooch cache path."""
20
+ data_path = provider.get_data_path()
21
+ assert data_path is not None
22
+ assert isinstance(data_path, Path)
23
+ assert data_path == Path(pooch.os_cache("climate_ref"))
24
+
25
+ def test_fetch_data(self, mocker):
26
+ """Test that fetch_data calls fetch_all_files."""
27
+ mock_fetch = mocker.patch("climate_ref_pmp.fetch_all_files")
28
+ mock_config = mocker.Mock()
29
+
30
+ provider.fetch_data(mock_config)
31
+
32
+ mock_fetch.assert_called_once()
33
+ # Check it's using the right registry name
34
+ call_args = mock_fetch.call_args
35
+ assert call_args[0][1] == "pmp-climatology"
36
+ assert call_args[1]["output_dir"] is None
37
+
38
+ def test_validate_setup_env_missing(self, mocker):
39
+ """Test validate_setup returns False when conda env is missing."""
40
+ mock_config = mocker.Mock()
41
+ # Mock the parent class to return False
42
+ mocker.patch.object(
43
+ PMPDiagnosticProvider.__bases__[0],
44
+ "validate_setup",
45
+ return_value=False,
46
+ )
47
+
48
+ result = provider.validate_setup(mock_config)
49
+ assert result is False
50
+
51
+ def test_validate_setup_data_invalid(self, mocker):
52
+ """Test validate_setup returns False when data validation fails."""
53
+ mock_config = mocker.Mock()
54
+ # Mock parent class to return True (conda env exists)
55
+ mocker.patch.object(
56
+ PMPDiagnosticProvider.__bases__[0],
57
+ "validate_setup",
58
+ return_value=True,
59
+ )
60
+ # Mock data validation to return errors
61
+ mocker.patch(
62
+ "climate_ref_pmp.validate_registry_cache",
63
+ return_value=["File missing: test.nc"],
64
+ )
65
+
66
+ result = provider.validate_setup(mock_config)
67
+ assert result is False
68
+
69
+ def test_validate_setup_all_valid(self, mocker):
70
+ """Test validate_setup returns True when all validation passes."""
71
+ mock_config = mocker.Mock()
72
+ # Mock parent class to return True (conda env exists)
73
+ mocker.patch.object(
74
+ PMPDiagnosticProvider.__bases__[0],
75
+ "validate_setup",
76
+ return_value=True,
77
+ )
78
+ # Mock data validation to return no errors
79
+ mocker.patch(
80
+ "climate_ref_pmp.validate_registry_cache",
81
+ return_value=[],
82
+ )
83
+
84
+ result = provider.validate_setup(mock_config)
85
+ assert result is True
86
+
87
+ def test_configure_sets_env_vars(self, mocker, tmp_path):
88
+ """Test that configure sets the required environment variables."""
89
+ test_provider = PMPDiagnosticProvider("PMP-Test", "1.0")
90
+ mock_config = mocker.Mock()
91
+ mock_config.paths.software = tmp_path / "software"
92
+ mock_config.ignore_datasets_file = tmp_path / "ignore.yaml"
93
+ mock_config.ignore_datasets_file.touch()
94
+
95
+ mocker.patch.object(test_provider, "get_conda_exe", return_value=Path("/path/to/conda"))
96
+
97
+ test_provider.configure(mock_config)
98
+
99
+ assert "PCMDI_CONDA_EXE" in test_provider.env_vars
100
+ assert test_provider.env_vars["PCMDI_CONDA_EXE"] == "/path/to/conda"
101
+ assert "FI_PROVIDER" in test_provider.env_vars
102
+ assert test_provider.env_vars["FI_PROVIDER"] == "tcp"
@@ -1,27 +0,0 @@
1
- import pytest
2
- from climate_ref_pmp import provider as pmp_provider
3
-
4
- from climate_ref_core.diagnostics import Diagnostic
5
-
6
- diagnostics = [pytest.param(diagnostic, id=diagnostic.slug) for diagnostic in pmp_provider.diagnostics()]
7
-
8
-
9
- @pytest.mark.slow
10
- @pytest.mark.parametrize("diagnostic", diagnostics)
11
- def test_diagnostics(diagnostic: Diagnostic, diagnostic_validation):
12
- if "enso" in diagnostic.slug:
13
- pytest.skip("ENSO diagnostics do not support the sample data")
14
-
15
- validator = diagnostic_validation(diagnostic)
16
-
17
- definition = validator.get_definition()
18
- validator.execute(definition)
19
-
20
-
21
- @pytest.mark.parametrize("diagnostic", diagnostics)
22
- def test_build_results(diagnostic: Diagnostic, diagnostic_validation):
23
- validator = diagnostic_validation(diagnostic)
24
-
25
- definition = validator.get_regression_definition()
26
- validator.validate(definition)
27
- validator.execution_regression.check(definition.key, definition.output_directory)
@@ -1,9 +0,0 @@
1
- from climate_ref_pmp import __version__, provider
2
-
3
-
4
- def test_provider():
5
- assert provider.name == "PMP"
6
- assert provider.slug == "pmp"
7
- assert provider.version == __version__
8
-
9
- assert len(provider) >= 1
File without changes
File without changes