climate-ref 0.8.0__py3-none-any.whl → 0.9.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.
climate_ref/config.py CHANGED
@@ -364,8 +364,8 @@ def _get_default_ignore_datasets_file() -> Path:
364
364
  f"Downloading default ignore datasets file from {DEFAULT_IGNORE_DATASETS_URL} "
365
365
  f"to {ignore_datasets_file}"
366
366
  )
367
- response = requests.get(DEFAULT_IGNORE_DATASETS_URL, timeout=120)
368
367
  try:
368
+ response = requests.get(DEFAULT_IGNORE_DATASETS_URL, timeout=120)
369
369
  response.raise_for_status()
370
370
  except requests.RequestException as exc:
371
371
  logger.warning(f"Failed to download default ignore datasets file: {exc}")
climate_ref/database.py CHANGED
@@ -13,6 +13,7 @@ import importlib.resources
13
13
  import shutil
14
14
  from datetime import datetime
15
15
  from pathlib import Path
16
+ from types import TracebackType
16
17
  from typing import TYPE_CHECKING, Any
17
18
  from urllib import parse as urlparse
18
19
 
@@ -161,6 +162,28 @@ class Database:
161
162
  # TODO: Set autobegin=False
162
163
  self.session = Session(self._engine)
163
164
 
165
+ def __enter__(self) -> "Database":
166
+ return self
167
+
168
+ def __exit__(
169
+ self,
170
+ exc_type: type[BaseException] | None,
171
+ exc_value: BaseException | None,
172
+ traceback: TracebackType | None,
173
+ ) -> None:
174
+ self.close()
175
+
176
+ def close(self) -> None:
177
+ """
178
+ Close the database connection
179
+
180
+ This closes the session and disposes of the engine, releasing all connections.
181
+ """
182
+ try:
183
+ self.session.close()
184
+ finally:
185
+ self._engine.dispose()
186
+
164
187
  def alembic_config(self, config: "Config") -> AlembicConfig:
165
188
  """
166
189
  Get the Alembic configuration object for the database
@@ -2,15 +2,16 @@
2
2
  Dataset handling utilities
3
3
  """
4
4
 
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import Any
6
6
 
7
+ from climate_ref.datasets.base import DatasetAdapter
8
+ from climate_ref.datasets.cmip6 import CMIP6DatasetAdapter
9
+ from climate_ref.datasets.obs4mips import Obs4MIPsDatasetAdapter
10
+ from climate_ref.datasets.pmp_climatology import PMPClimatologyDatasetAdapter
7
11
  from climate_ref_core.datasets import SourceDatasetType
8
12
 
9
- if TYPE_CHECKING:
10
- from climate_ref.datasets.base import DatasetAdapter
11
13
 
12
-
13
- def get_dataset_adapter(source_type: str, **kwargs: Any) -> "DatasetAdapter":
14
+ def get_dataset_adapter(source_type: str, **kwargs: Any) -> DatasetAdapter:
14
15
  """
15
16
  Get the appropriate adapter for the specified source type
16
17
 
@@ -25,16 +26,19 @@ def get_dataset_adapter(source_type: str, **kwargs: Any) -> "DatasetAdapter":
25
26
  DatasetAdapter instance
26
27
  """
27
28
  if source_type.lower() == SourceDatasetType.CMIP6.value:
28
- from climate_ref.datasets.cmip6 import CMIP6DatasetAdapter # noqa: PLC0415
29
-
30
29
  return CMIP6DatasetAdapter(**kwargs)
31
30
  elif source_type.lower() == SourceDatasetType.obs4MIPs.value.lower():
32
- from climate_ref.datasets.obs4mips import Obs4MIPsDatasetAdapter # noqa: PLC0415
33
-
34
31
  return Obs4MIPsDatasetAdapter(**kwargs)
35
32
  elif source_type.lower() == SourceDatasetType.PMPClimatology.value.lower():
36
- from climate_ref.datasets.pmp_climatology import PMPClimatologyDatasetAdapter # noqa: PLC0415
37
-
38
33
  return PMPClimatologyDatasetAdapter(**kwargs)
39
34
  else:
40
35
  raise ValueError(f"Unknown source type: {source_type}")
36
+
37
+
38
+ __all__ = [
39
+ "CMIP6DatasetAdapter",
40
+ "DatasetAdapter",
41
+ "Obs4MIPsDatasetAdapter",
42
+ "PMPClimatologyDatasetAdapter",
43
+ "get_dataset_adapter",
44
+ ]
@@ -219,7 +219,9 @@ class DatasetAdapter(Protocol):
219
219
  slug = unique_slugs[0]
220
220
 
221
221
  # Upsert the dataset (create a new dataset or update the metadata)
222
- dataset_metadata = data_catalog_dataset[list(self.dataset_specific_metadata)].iloc[0].to_dict()
222
+ dataset_metadata = cast(
223
+ dict[str, Any], data_catalog_dataset[list(self.dataset_specific_metadata)].iloc[0].to_dict()
224
+ )
223
225
  dataset, dataset_state = db.update_or_create(DatasetModel, defaults=dataset_metadata, slug=slug)
224
226
  if dataset_state == ModelState.CREATED:
225
227
  logger.info(f"Created new dataset: {dataset}")
@@ -381,23 +383,15 @@ class DatasetAdapter(Protocol):
381
383
  else:
382
384
  catalog = self._get_datasets(db, limit)
383
385
 
384
- def _get_latest_version(dataset_catalog: pd.DataFrame) -> pd.DataFrame:
385
- """
386
- Get the latest version of each dataset based on the version metadata.
387
-
388
- This assumes that the version can be sorted lexicographically.
389
- """
390
- latest_version = dataset_catalog[self.version_metadata].max()
391
-
392
- return cast(
393
- pd.DataFrame, dataset_catalog[dataset_catalog[self.version_metadata] == latest_version]
394
- )
395
-
396
386
  # If there are no datasets, return an empty DataFrame
397
387
  if catalog.empty:
398
388
  return pd.DataFrame(columns=self.dataset_specific_metadata + self.file_specific_metadata)
399
389
 
400
- # Group by the dataset ID and get the latest version for each dataset
401
- return catalog.groupby(
402
- list(self.dataset_id_metadata), group_keys=False, as_index=False, sort=False
403
- ).apply(_get_latest_version)
390
+ # Get the latest version for each dataset group
391
+ # Uses transform to compute max version per group, then filters rows matching the max
392
+ # This assumes version can be sorted lexicographically
393
+ max_version_per_group = catalog.groupby(list(self.dataset_id_metadata), sort=False)[
394
+ self.version_metadata
395
+ ].transform("max")
396
+
397
+ return catalog[catalog[self.version_metadata] == max_version_per_group]
@@ -53,7 +53,7 @@ def _apply_fixes(data_catalog: pd.DataFrame) -> pd.DataFrame:
53
53
  if "parent_variant_label" in data_catalog:
54
54
  data_catalog = (
55
55
  data_catalog.groupby("instance_id")
56
- .apply(_fix_parent_variant_label, include_groups=False)
56
+ .apply(_fix_parent_variant_label, include_groups=False) # type: ignore[call-overload]
57
57
  .reset_index(level="instance_id")
58
58
  )
59
59
 
climate_ref/solver.py CHANGED
@@ -130,7 +130,7 @@ def extract_covered_datasets(
130
130
  # Use a single group
131
131
  groups = [((), subset)]
132
132
  else:
133
- groups = list(subset.groupby(list(requirement.group_by)))
133
+ groups = list(subset.groupby(list(requirement.group_by))) # type: ignore[arg-type]
134
134
 
135
135
  results = {}
136
136
 
climate_ref/testing.py CHANGED
@@ -1,21 +1,30 @@
1
1
  """
2
- Testing utilities
2
+ Testing utilities for running and validating diagnostic test cases.
3
+
4
+ This module provides:
5
+ - Path resolution for package-local test data (catalogs, regression data)
6
+ - Sample data fetching utilities
7
+ - TestCaseRunner for executing diagnostics with test data
8
+ - Result validation helpers
3
9
  """
4
10
 
5
11
  import shutil
6
12
  from pathlib import Path
7
13
 
14
+ from attrs import define
8
15
  from loguru import logger
9
16
 
10
17
  from climate_ref.config import Config
11
18
  from climate_ref.database import Database
12
- from climate_ref.executor import handle_execution_result
13
19
  from climate_ref.models import Execution, ExecutionGroup
14
20
  from climate_ref_core.dataset_registry import dataset_registry_manager, fetch_all_files
15
- from climate_ref_core.diagnostics import Diagnostic, ExecutionResult
21
+ from climate_ref_core.datasets import ExecutionDatasetCollection
22
+ from climate_ref_core.diagnostics import Diagnostic, ExecutionDefinition, ExecutionResult
16
23
  from climate_ref_core.env import env
17
- from climate_ref_core.pycmec.metric import CMECMetric
18
- from climate_ref_core.pycmec.output import CMECOutput
24
+ from climate_ref_core.exceptions import DatasetResolutionError, NoTestDataSpecError, TestCaseNotFoundError
25
+ from climate_ref_core.testing import (
26
+ validate_cmec_bundles,
27
+ )
19
28
 
20
29
 
21
30
  def _determine_test_directory() -> Path | None:
@@ -27,6 +36,7 @@ def _determine_test_directory() -> Path | None:
27
36
 
28
37
 
29
38
  TEST_DATA_DIR = _determine_test_directory()
39
+ """Path to the centralised test data directory (for sample data)."""
30
40
  SAMPLE_DATA_VERSION = "v0.7.4"
31
41
 
32
42
 
@@ -76,13 +86,16 @@ def fetch_sample_data(force_cleanup: bool = False, symlink: bool = False) -> Non
76
86
  fh.write(SAMPLE_DATA_VERSION)
77
87
 
78
88
 
79
- def validate_result(diagnostic: Diagnostic, config: Config, result: ExecutionResult) -> None:
89
+ def validate_result(
90
+ diagnostic: Diagnostic, config: Config, result: ExecutionResult
91
+ ) -> None: # pragma: no cover
80
92
  """
81
93
  Asserts the correctness of the result of a diagnostic execution
82
94
 
83
95
  This should only be used by the test suite as it will create a fake
84
96
  database entry for the diagnostic execution result.
85
97
  """
98
+ # TODO: Remove this function once we have moved to using RegressionValidator
86
99
  # Add a fake execution/execution group in the Database
87
100
  database = Database.from_config(config)
88
101
  execution_group = ExecutionGroup(
@@ -101,16 +114,105 @@ def validate_result(diagnostic: Diagnostic, config: Config, result: ExecutionRes
101
114
 
102
115
  assert result.successful
103
116
 
104
- # Validate bundles
105
- metric_bundle = CMECMetric.load_from_json(result.to_output_path(result.metric_bundle_filename))
106
- CMECMetric.model_validate(metric_bundle)
107
- bundle_dimensions = tuple(metric_bundle.DIMENSIONS.root["json_structure"])
108
- assert diagnostic.facets == bundle_dimensions
109
- CMECOutput.load_from_json(result.to_output_path(result.output_bundle_filename))
117
+ # Validate CMEC bundles
118
+ validate_cmec_bundles(diagnostic, result)
110
119
 
111
120
  # Create a fake log file if one doesn't exist
112
121
  if not result.to_output_path("out.log").exists():
113
122
  result.to_output_path("out.log").touch()
114
123
 
115
- # This checks if the bundles are valid
124
+ # Import late to avoid importing executors,
125
+ # some of which have on-import side effects, at package load time
126
+ from climate_ref.executor import handle_execution_result # noqa: PLC0415
127
+
128
+ # Process and store the result
116
129
  handle_execution_result(config, database=database, execution=execution, result=result)
130
+
131
+
132
+ @define
133
+ class TestCaseRunner:
134
+ """
135
+ Helper class for running diagnostic test cases.
136
+
137
+ This runner handles:
138
+ - Running the diagnostic with pre-resolved datasets
139
+ - Setting up the execution definition
140
+ """
141
+
142
+ config: Config
143
+ datasets: ExecutionDatasetCollection | None = None
144
+
145
+ def run(
146
+ self,
147
+ diagnostic: Diagnostic,
148
+ test_case_name: str = "default",
149
+ output_dir: Path | None = None,
150
+ clean: bool = False,
151
+ ) -> ExecutionResult:
152
+ """
153
+ Run a specific test case for a diagnostic.
154
+
155
+ Parameters
156
+ ----------
157
+ diagnostic
158
+ The diagnostic to run
159
+ test_case_name
160
+ Name of the test case to run (default: "default")
161
+ output_dir
162
+ Optional output directory for results
163
+ clean
164
+ If True, delete the output directory before running
165
+
166
+ Returns
167
+ -------
168
+ ExecutionResult
169
+ The result of running the diagnostic
170
+
171
+ Raises
172
+ ------
173
+ NoTestDataSpecError
174
+ If the diagnostic has no test_data_spec
175
+ TestCaseNotFoundError
176
+ If the test case doesn't exist
177
+ DatasetResolutionError
178
+ If datasets cannot be resolved
179
+ """
180
+ if diagnostic.test_data_spec is None:
181
+ raise NoTestDataSpecError(f"Diagnostic {diagnostic.slug} has no test_data_spec")
182
+
183
+ if not diagnostic.test_data_spec.has_case(test_case_name):
184
+ raise TestCaseNotFoundError(
185
+ f"Test case {test_case_name!r} not found. Available: {diagnostic.test_data_spec.case_names}"
186
+ )
187
+
188
+ if self.datasets is None:
189
+ raise DatasetResolutionError(
190
+ "No datasets provided. Run 'ref test-cases fetch' first to build the catalog."
191
+ )
192
+
193
+ # Determine output directory
194
+ if output_dir is None:
195
+ output_dir = (
196
+ self.config.paths.results
197
+ / "test-cases"
198
+ / diagnostic.provider.slug
199
+ / diagnostic.slug
200
+ / test_case_name
201
+ )
202
+
203
+ if clean and output_dir.exists():
204
+ shutil.rmtree(output_dir)
205
+
206
+ output_dir.mkdir(parents=True, exist_ok=True)
207
+
208
+ # Create execution definition
209
+ definition = ExecutionDefinition(
210
+ diagnostic=diagnostic,
211
+ key=f"test-{test_case_name}",
212
+ datasets=self.datasets,
213
+ output_directory=output_dir,
214
+ root_directory=output_dir.parent,
215
+ )
216
+
217
+ # Run the diagnostic
218
+ return diagnostic.run(definition)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: climate-ref
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Application which runs 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
@@ -25,6 +25,7 @@ Requires-Dist: cattrs>=24.1.2
25
25
  Requires-Dist: climate-ref-core
26
26
  Requires-Dist: ecgtools>=2024.7.31
27
27
  Requires-Dist: environs>=11.0.0
28
+ Requires-Dist: gitpython>=3.1.0
28
29
  Requires-Dist: loguru>=0.7.2
29
30
  Requires-Dist: parsl>=2025.5.19; sys_platform != 'win32'
30
31
  Requires-Dist: platformdirs>=4.3.6
@@ -1,26 +1,28 @@
1
1
  climate_ref/__init__.py,sha256=M45QGfl0KCPK48A8MjI08weNvZHMYH__GblraQMxsoM,808
2
2
  climate_ref/_config_helpers.py,sha256=-atI5FX7SukhLE_jz_rL-EHQ7s0YYqKu3dSFYWxSyMU,6632
3
3
  climate_ref/alembic.ini,sha256=WRvbwSIFuZ7hWNMnR2-yHPJAwYUnwhvRYBzkJhtpGdg,3535
4
- climate_ref/config.py,sha256=MyAQQP0LCiO20e20C1GSz-W1o1CFW5XRYINL89oRFWM,19686
4
+ climate_ref/config.py,sha256=26P4TSPDoSXi0w3HM-pp3UWbAvm9yk-JhajH9nh172Y,19690
5
5
  climate_ref/constants.py,sha256=9RaNLgUSuQva7ki4eRW3TjOKeVP6T81QNiu0veB1zVk,111
6
- climate_ref/database.py,sha256=stO0K61D8Jh6zRXpjq8rTTeuz0aSi2ZEmeb_9ZqUHJc,10707
6
+ climate_ref/database.py,sha256=fAUABLgVsN5IajPB99f0VMFjX3fguSA2FKprnFTqHGQ,11274
7
7
  climate_ref/provider_registry.py,sha256=NJssVC0ws7BqaYnAPy-1jSxwdFSXl1VCId67WXMUeGU,4230
8
8
  climate_ref/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  climate_ref/slurm.py,sha256=N2L1pZ1A79dtkASEFU4TUjrVg2qtYUf7HMeoGXErTyA,7338
10
- climate_ref/solver.py,sha256=ssmzQuxoEhO8Lc62cUJmW6FzM82t08jMoCZ9AzSIfLc,17367
11
- climate_ref/testing.py,sha256=5zte0H4trwxOgRpzAJJ8by8aGuDSDOQAlUqFBgqjhWg,4256
12
- climate_ref/cli/__init__.py,sha256=iVgsOBnf4YNgLwxoR-VCrrcnMWM90wtLBFxvI7AaHB0,4576
13
- climate_ref/cli/_utils.py,sha256=KQDp-0YDLXqljpcUFUGoksloJocMWbwh3-kxoUqMbgo,3440
10
+ climate_ref/solver.py,sha256=gPnTl1hNq4-jHit12Y8W084panumIIVbF-wKPhs_z8c,17393
11
+ climate_ref/testing.py,sha256=GG_qWL5V6LQp5Jq6JKoEDRLaxU6Eitd7oVS2vGVHAmE,7308
12
+ climate_ref/cli/__init__.py,sha256=kpEErweCG4TX_QXWbevqtpGNKYGC8mcWeOYu_DQB3r4,4775
13
+ climate_ref/cli/_git_utils.py,sha256=dAAQwBlwzT3FOjyZOCNBqAscys25RJbYzaDp5ZaKWYk,2878
14
+ climate_ref/cli/_utils.py,sha256=n-wn1z1Uxg0Uk-vIRP88PpUC5wwoELTuML3TrO5fTjU,3932
14
15
  climate_ref/cli/config.py,sha256=ak4Rn9S6fH23PkHHlI-pXuPiZYOvUB4r26eu3p525-M,532
15
- climate_ref/cli/datasets.py,sha256=8dZUNJuLJLkhuEHP7BOANM3Wxx2Gy0DZ7k3DvMeC4D0,9912
16
+ climate_ref/cli/datasets.py,sha256=GfSbin9t76EBj71qCSpvICxc6iP--upecbhKoHYBVTg,9931
16
17
  climate_ref/cli/executions.py,sha256=O1xm89r9cI4e99VzLgc5pbhrp71OjROEHa6-GRjaFg4,18191
17
- climate_ref/cli/providers.py,sha256=8C3xSDBdzfUMil6HPG9a7g0_EKQEmlfPbI3VnN_NmMI,2590
18
+ climate_ref/cli/providers.py,sha256=nMxS41Jb_eRG-tdWxiHSnd8BB7pgmJMin2CKN6Re41o,6021
18
19
  climate_ref/cli/solve.py,sha256=ZTXrwDFDXNrX5GLMJTN9tFnpV3zlcZbEu2aF3JDJVxI,2367
20
+ climate_ref/cli/test_cases.py,sha256=nJCDHTdjC8vTmZI1sZlLheV8UkzzdzkJjC6iEArh6K8,26608
19
21
  climate_ref/dataset_registry/obs4ref_reference.txt,sha256=szXNhA5wx1n2XfYiXPl4FipmjE1Kp5Jrj2UuBLHqNvw,7351
20
22
  climate_ref/dataset_registry/sample_data.txt,sha256=w3heh6MMbZ9Jp_SAwLcwWVkq91IRxZh3spwKz-YFRt0,43869
21
- climate_ref/datasets/__init__.py,sha256=oC0fhdMfie7IfzBwU7BtD5Tv4bJP4zQOK3XgnMQXtSw,1222
22
- climate_ref/datasets/base.py,sha256=M0hGMlRn1iiridnp7Kc0gckapdEjSSKZyQ1lv8USYIU,15383
23
- climate_ref/datasets/cmip6.py,sha256=0R79QfRuqTa_wUHHkYCYf8zpJXHzJw2CXbuDKNlY3Bg,7212
23
+ climate_ref/datasets/__init__.py,sha256=Kv27xWlXw7bz_W8lF5hiGtBv18r5feEJobfhznVfdKY,1262
24
+ climate_ref/datasets/base.py,sha256=JMGy0YSgXRjzqC1Y-FnhUV3p7q24d4SDczSfNtbDxQI,15150
25
+ climate_ref/datasets/cmip6.py,sha256=Z2SD8Ckg4HlC2Y_AgZyzNsb0qyPCJzCgjmK_zUDRIy8,7243
24
26
  climate_ref/datasets/cmip6_parsers.py,sha256=wH4WKQAR2_aniXwsW7nch6nIpXk2pSpPxkT4unjV4hQ,6041
25
27
  climate_ref/datasets/obs4mips.py,sha256=AerO5QaISiRYPzBm_C6lGsKQgE_Zyzo4XoOOKrpB-TE,6586
26
28
  climate_ref/datasets/pmp_climatology.py,sha256=goHDc_3B2Wdiy_hmpERNvWDdDYZACPOyFDt3Du6nGc0,534
@@ -50,9 +52,9 @@ climate_ref/models/execution.py,sha256=gfkrs0wyySNVQpfob1Bc-26iLDV99K6aSPHs0GZmd
50
52
  climate_ref/models/metric_value.py,sha256=NFvduNVyB5aOj8rwn8KiPDzIjomBzIroAyFACkSSEUw,7400
51
53
  climate_ref/models/mixins.py,sha256=1EAJU2RlhY-9UUIN8F5SZOg5k5uD9r1rG6isvrjQF0o,4683
52
54
  climate_ref/models/provider.py,sha256=OnoacwAa50XBS9CCgxJnylIfsGXFP4EqTlLhBXmh6So,991
53
- climate_ref-0.8.0.dist-info/METADATA,sha256=0r3IKptPBXnT64stgtHiIqEGttfPmTKZo4BNAgXNimU,4507
54
- climate_ref-0.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
55
- climate_ref-0.8.0.dist-info/entry_points.txt,sha256=IaggEJlDIhoYWXdXJafacWbWtCcoEqUKceP1qD7_7vU,44
56
- climate_ref-0.8.0.dist-info/licenses/LICENCE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
57
- climate_ref-0.8.0.dist-info/licenses/NOTICE,sha256=4qTlax9aX2-mswYJuVrLqJ9jK1IkN5kSBqfVvYLF3Ws,128
58
- climate_ref-0.8.0.dist-info/RECORD,,
55
+ climate_ref-0.9.0.dist-info/METADATA,sha256=WdqF8U3nKFhrKaGWRnTc_lqefWpYBxb9AKVBdnTP5LI,4539
56
+ climate_ref-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
57
+ climate_ref-0.9.0.dist-info/entry_points.txt,sha256=IaggEJlDIhoYWXdXJafacWbWtCcoEqUKceP1qD7_7vU,44
58
+ climate_ref-0.9.0.dist-info/licenses/LICENCE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
59
+ climate_ref-0.9.0.dist-info/licenses/NOTICE,sha256=4qTlax9aX2-mswYJuVrLqJ9jK1IkN5kSBqfVvYLF3Ws,128
60
+ climate_ref-0.9.0.dist-info/RECORD,,