climate-ref 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.
- {climate_ref-0.8.0 → climate_ref-0.9.0}/.gitignore +10 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/Dockerfile +20 -11
- {climate_ref-0.8.0 → climate_ref-0.9.0}/PKG-INFO +2 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/conftest.py +10 -3
- {climate_ref-0.8.0 → climate_ref-0.9.0}/pyproject.toml +2 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/__init__.py +5 -2
- climate_ref-0.9.0/src/climate_ref/cli/_git_utils.py +112 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/_utils.py +24 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/datasets.py +1 -0
- climate_ref-0.9.0/src/climate_ref/cli/providers.py +183 -0
- climate_ref-0.9.0/src/climate_ref/cli/test_cases.py +729 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/config.py +1 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/database.py +23 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/__init__.py +15 -11
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/base.py +11 -17
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/cmip6.py +1 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/solver.py +1 -1
- climate_ref-0.9.0/src/climate_ref/testing.py +218 -0
- climate_ref-0.9.0/tests/integration/test_provider_setup.py +56 -0
- climate_ref-0.9.0/tests/unit/cli/test_git_utils.py +219 -0
- climate_ref-0.9.0/tests/unit/cli/test_providers.py +69 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_root.py +1 -1
- climate_ref-0.9.0/tests/unit/cli/test_test_cases.py +723 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_utils.py +42 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6.py +37 -29
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_datasets.py +2 -1
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/executor/test_result_handling.py +2 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_config.py +16 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_database.py +14 -13
- climate_ref-0.9.0/tests/unit/test_testing.py +174 -0
- climate_ref-0.8.0/src/climate_ref/cli/providers.py +0 -84
- climate_ref-0.8.0/src/climate_ref/testing.py +0 -116
- climate_ref-0.8.0/tests/unit/cli/test_providers.py +0 -23
- {climate_ref-0.8.0 → climate_ref-0.9.0}/LICENCE +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/NOTICE +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/README.md +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/__init__.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/_config_helpers.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/alembic.ini +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/config.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/executions.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/cli/solve.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/constants.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/dataset_registry/obs4ref_reference.txt +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/dataset_registry/sample_data.txt +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/cmip6_parsers.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/obs4mips.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/pmp_climatology.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/datasets/utils.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/__init__.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/hpc.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/local.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/pbs_scheduler.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/result_handling.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/executor/synchronous.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/README +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/env.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/script.py.mako +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-05-02T1418_341a4aa2551e_regenerate.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-05-09T2032_03dbb4998e49_series_metric_value.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-07-03T1505_795c1e6cf496_drop_unique_requirement_on_slug.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-07-20T1521_94beace57a9c_cmip6_finalised.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-08-05T0327_a1b2c3d4e5f6_finalised_on_base_dataset.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-09-05T2019_8d28e5e0f9c3_add_indexes.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-09-10T1358_2f6e36738e06_use_version_as_version_facet_for_.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/migrations/versions/2025-09-22T2359_20cd136a5b04_add_pmp_version.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/__init__.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/base.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/dataset.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/diagnostic.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/execution.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/metric_value.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/mixins.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/models/provider.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/provider_registry.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/py.typed +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/src/climate_ref/slurm.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_config.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_datasets.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_executions/test_inspect.txt +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_executions.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/cli/test_solve.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/conftest.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6/cmip6_catalog_db.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6/cmip6_catalog_db_complete.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6/cmip6_catalog_db_drs.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6/cmip6_catalog_local_complete.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_cmip6/cmip6_catalog_local_drs.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_obs4mips/obs4mips_catalog_db.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_obs4mips/obs4mips_catalog_local.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_obs4mips.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_pmp_climatology/pmp_catalog_local.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_pmp_climatology.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/datasets/test_utils.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/executor/test_hpc_executor.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/executor/test_local_executor.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/executor/test_synchronous_executor.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/models/test_metric_execution.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/models/test_metric_value.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_pbssmartprovider.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_provider_registry.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_slurm.py +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_solver/test_solve_metrics.yml +0 -0
- {climate_ref-0.8.0 → climate_ref-0.9.0}/tests/unit/test_solver.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/*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# This docker container packages up the REF cli tool with the default set of diagnostic providers
|
|
3
3
|
# used as part of the CMIP7 FastTrack process.
|
|
4
4
|
|
|
5
|
-
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS
|
|
5
|
+
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS base
|
|
6
6
|
|
|
7
7
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
8
|
git \
|
|
@@ -16,6 +16,7 @@ ENV UV_LINK_MODE=copy
|
|
|
16
16
|
|
|
17
17
|
WORKDIR /app
|
|
18
18
|
|
|
19
|
+
FROM base AS build
|
|
19
20
|
# Install the project's dependencies using the lockfile and settings
|
|
20
21
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
21
22
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
@@ -24,22 +25,18 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|
|
24
25
|
|
|
25
26
|
# Then, add the rest of the project source code and install it
|
|
26
27
|
# Installing separately from its dependencies allows optimal layer caching
|
|
27
|
-
|
|
28
|
+
COPY . /app
|
|
28
29
|
|
|
29
30
|
# Sync the project as non-editable installs
|
|
30
31
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
31
32
|
uv sync --frozen --no-editable --no-dev
|
|
32
33
|
|
|
33
34
|
# Runtime container
|
|
34
|
-
FROM
|
|
35
|
+
FROM base AS runtime
|
|
35
36
|
|
|
36
37
|
LABEL maintainer="Jared Lewis <jared.lewis@climate-resource.com>"
|
|
37
38
|
LABEL description="Base Docker image for the REF compute engine"
|
|
38
39
|
|
|
39
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
40
|
-
git \
|
|
41
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
42
|
-
|
|
43
40
|
# Create a non-root user
|
|
44
41
|
RUN useradd -m -u 1000 app
|
|
45
42
|
|
|
@@ -48,18 +45,30 @@ ENV PATH="/app/.venv/bin:${PATH}"
|
|
|
48
45
|
ENV VIRTUAL_ENV=/app/.venv
|
|
49
46
|
|
|
50
47
|
# Copy the installed packages from the build stage
|
|
51
|
-
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /app/.venv/bin/
|
|
52
48
|
COPY --from=build --chown=app:app /app/.venv /app/.venv
|
|
53
49
|
COPY --from=build --chown=app:app /app/scripts /app/scripts
|
|
54
50
|
|
|
55
51
|
# Location of the REF configuration files
|
|
56
52
|
ENV REF_CONFIGURATION=/ref
|
|
57
53
|
|
|
54
|
+
# Set matplotlib config directory to a location with write permissions
|
|
55
|
+
ENV MPLCONFIGDIR=$REF_CONFIGURATION/software/matplotlib
|
|
56
|
+
|
|
57
|
+
# Set micromamba root to locations with write permissions
|
|
58
|
+
ENV MAMBA_ROOT_PREFIX=$REF_CONFIGURATION/software/conda
|
|
59
|
+
ENV CONDA_PKGS_DIRS=$REF_CONFIGURATION/software/conda/pkgs
|
|
60
|
+
ENV XDG_CACHE_HOME=$REF_CONFIGURATION/cache
|
|
61
|
+
|
|
58
62
|
# Create necessary directories with proper permissions
|
|
59
|
-
RUN
|
|
63
|
+
RUN install --owner=app --group=app -d /ref /app/cache && \
|
|
64
|
+
install --mode=555 -d /home/app/.conda && \
|
|
65
|
+
touch /home/app/.conda/environments.txt # conda tries to write to ~/.conda/environments.txt despite its root being set elsewhere. See: https://github.com/conda/conda/issues/8804
|
|
66
|
+
|
|
67
|
+
# Switch to non-root user -- use numeric ID for k8s systems that enforce runAsUser
|
|
68
|
+
USER 1000
|
|
60
69
|
|
|
61
|
-
#
|
|
62
|
-
|
|
70
|
+
# Pre-cache matplotlib fonts and ilamb3 config
|
|
71
|
+
RUN /app/.venv/bin/python -c "import matplotlib; import ilamb3"
|
|
63
72
|
|
|
64
73
|
# Run the REF CLI tool by default
|
|
65
74
|
ENTRYPOINT ["/app/.venv/bin/ref"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: climate-ref
|
|
3
|
-
Version: 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,5 +1,6 @@
|
|
|
1
1
|
import importlib.resources
|
|
2
2
|
import shutil
|
|
3
|
+
from collections.abc import Generator
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from urllib import parse as urlparse
|
|
5
6
|
|
|
@@ -38,8 +39,10 @@ def _clone_db(target_db_url: str, template_db_path: Path) -> None:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
@pytest.fixture
|
|
41
|
-
def db(config) -> Database:
|
|
42
|
-
|
|
42
|
+
def db(config) -> Generator[Database, None, None]:
|
|
43
|
+
database = Database.from_config(config, run_migrations=True)
|
|
44
|
+
yield database
|
|
45
|
+
database.close()
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
@pytest.fixture(scope="session")
|
|
@@ -77,6 +80,8 @@ def db_seeded_template(tmp_path_session, cmip6_data_catalog, obs4mips_data_catal
|
|
|
77
80
|
with database.session.begin():
|
|
78
81
|
_register_provider(database, example_provider)
|
|
79
82
|
|
|
83
|
+
database.close()
|
|
84
|
+
|
|
80
85
|
return template_db_path
|
|
81
86
|
|
|
82
87
|
|
|
@@ -85,4 +90,6 @@ def db_seeded(db_seeded_template, config) -> Database:
|
|
|
85
90
|
# Copy the template database to the location in the config
|
|
86
91
|
_clone_db(config.db.database_url, db_seeded_template)
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
database = Database.from_config(config, run_migrations=True)
|
|
94
|
+
yield database
|
|
95
|
+
database.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "climate-ref"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Application which runs the CMIP Rapid Evaluation Framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -41,6 +41,7 @@ dependencies = [
|
|
|
41
41
|
"ecgtools>=2024.7.31",
|
|
42
42
|
"platformdirs>=4.3.6",
|
|
43
43
|
"tqdm>=4.67.1",
|
|
44
|
+
"gitpython>=3.1.0",
|
|
44
45
|
# parsl doesn't support Windows yet
|
|
45
46
|
# We don't target Windows either, but this __might__ allow Windows users to install the package
|
|
46
47
|
'parsl>=2025.5.19; sys_platform != "win32"'
|
|
@@ -11,7 +11,7 @@ from loguru import logger
|
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
|
|
13
13
|
from climate_ref import __version__
|
|
14
|
-
from climate_ref.cli import config, datasets, executions, providers, solve
|
|
14
|
+
from climate_ref.cli import config, datasets, executions, providers, solve, test_cases
|
|
15
15
|
from climate_ref.config import Config
|
|
16
16
|
from climate_ref.constants import CONFIG_FILENAME
|
|
17
17
|
from climate_ref.database import Database
|
|
@@ -104,6 +104,7 @@ def build_app() -> typer.Typer:
|
|
|
104
104
|
app.add_typer(datasets.app, name="datasets")
|
|
105
105
|
app.add_typer(executions.app, name="executions")
|
|
106
106
|
app.add_typer(providers.app, name="providers")
|
|
107
|
+
app.add_typer(test_cases.app, name="test-cases")
|
|
107
108
|
|
|
108
109
|
try:
|
|
109
110
|
celery_app = importlib.import_module("climate_ref_celery.cli").app
|
|
@@ -164,7 +165,9 @@ def main( # noqa: PLR0913
|
|
|
164
165
|
|
|
165
166
|
logger.debug(f"Configuration loaded from: {config._config_file!s}")
|
|
166
167
|
|
|
167
|
-
ctx.
|
|
168
|
+
# Use ctx.with_resource to ensure the database connection is closed when the CLI exits
|
|
169
|
+
database = ctx.with_resource(Database.from_config(config))
|
|
170
|
+
ctx.obj = CLIContext(config=config, database=database, console=_create_console())
|
|
168
171
|
|
|
169
172
|
|
|
170
173
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Git utilities for CLI commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from git import InvalidGitRepositoryError, Repo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_repo_for_path(path: Path) -> Repo | None:
|
|
10
|
+
"""
|
|
11
|
+
Get the git repository containing the given path.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
path
|
|
16
|
+
Path to a file or directory
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
:
|
|
21
|
+
The Repo object if path is within a git repository, None otherwise
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
return Repo(path, search_parent_directories=True)
|
|
25
|
+
except InvalidGitRepositoryError:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_git_status(file_path: Path, repo: Repo) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Get git status for a file using GitPython.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
file_path
|
|
36
|
+
Absolute path to the file
|
|
37
|
+
repo
|
|
38
|
+
GitPython Repo object
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
:
|
|
43
|
+
Status string: "new", "staged", "modified", "tracked", "untracked", or "unknown"
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
rel_path = str(file_path.relative_to(repo.working_dir))
|
|
47
|
+
|
|
48
|
+
# Check if untracked
|
|
49
|
+
if rel_path in repo.untracked_files:
|
|
50
|
+
return "new"
|
|
51
|
+
|
|
52
|
+
# Check staged changes (index vs HEAD)
|
|
53
|
+
staged_files = {item.a_path for item in repo.index.diff("HEAD")}
|
|
54
|
+
if rel_path in staged_files:
|
|
55
|
+
return "staged"
|
|
56
|
+
|
|
57
|
+
# Check unstaged changes (working tree vs index)
|
|
58
|
+
unstaged_files = {item.a_path for item in repo.index.diff(None)}
|
|
59
|
+
if rel_path in unstaged_files:
|
|
60
|
+
return "modified"
|
|
61
|
+
|
|
62
|
+
# Check if file is tracked
|
|
63
|
+
try:
|
|
64
|
+
repo.git.ls_files("--error-unmatch", rel_path)
|
|
65
|
+
return "tracked"
|
|
66
|
+
except Exception:
|
|
67
|
+
return "untracked"
|
|
68
|
+
except Exception:
|
|
69
|
+
return "unknown"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def collect_regression_file_info(
|
|
73
|
+
regression_dir: Path,
|
|
74
|
+
repo: Repo | None,
|
|
75
|
+
size_threshold_bytes: int,
|
|
76
|
+
) -> list[dict[str, Any]]:
|
|
77
|
+
"""
|
|
78
|
+
Collect file information from a regression directory.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
regression_dir
|
|
83
|
+
Path to the regression data directory
|
|
84
|
+
repo
|
|
85
|
+
Git repository object, or None if not in a repo
|
|
86
|
+
size_threshold_bytes
|
|
87
|
+
Files larger than this will be flagged as large
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
:
|
|
92
|
+
List of dicts with keys: rel_path, size, is_large, git_status
|
|
93
|
+
"""
|
|
94
|
+
files = sorted(regression_dir.rglob("*"))
|
|
95
|
+
files = [f for f in files if f.is_file()]
|
|
96
|
+
|
|
97
|
+
file_info: list[dict[str, Any]] = []
|
|
98
|
+
for file_path in files:
|
|
99
|
+
size = file_path.stat().st_size
|
|
100
|
+
rel_path = str(file_path.relative_to(regression_dir))
|
|
101
|
+
git_status = get_git_status(file_path, repo) if repo else "unknown"
|
|
102
|
+
|
|
103
|
+
file_info.append(
|
|
104
|
+
{
|
|
105
|
+
"rel_path": rel_path,
|
|
106
|
+
"size": size,
|
|
107
|
+
"is_large": size > size_threshold_bytes,
|
|
108
|
+
"git_status": git_status,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return file_info
|
|
@@ -4,6 +4,30 @@ from rich import box
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
|
|
7
|
+
_BYTES_PER_UNIT = 1024
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_size(size_bytes: int | float) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Format file size in human-readable form.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
size_bytes
|
|
17
|
+
Size in bytes
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
:
|
|
22
|
+
Human-readable size string (e.g., "1.5 MB")
|
|
23
|
+
"""
|
|
24
|
+
size = float(size_bytes)
|
|
25
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
26
|
+
if size < _BYTES_PER_UNIT:
|
|
27
|
+
return f"{size:.1f} {unit}"
|
|
28
|
+
size /= _BYTES_PER_UNIT
|
|
29
|
+
return f"{size:.1f} TB"
|
|
30
|
+
|
|
7
31
|
|
|
8
32
|
def parse_facet_filters(filters: list[str] | None) -> dict[str, str]:
|
|
9
33
|
"""
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage the REF providers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import typer
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from climate_ref.cli._utils import pretty_print_df
|
|
13
|
+
from climate_ref.provider_registry import ProviderRegistry
|
|
14
|
+
from climate_ref_core.providers import CondaDiagnosticProvider, DiagnosticProvider
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help=__doc__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command(name="list")
|
|
20
|
+
def list_(ctx: typer.Context) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Print the available providers.
|
|
23
|
+
"""
|
|
24
|
+
config = ctx.obj.config
|
|
25
|
+
db = ctx.obj.database
|
|
26
|
+
console = ctx.obj.console
|
|
27
|
+
provider_registry = ProviderRegistry.build_from_config(config, db)
|
|
28
|
+
|
|
29
|
+
def get_env(provider: DiagnosticProvider) -> str:
|
|
30
|
+
env = ""
|
|
31
|
+
if isinstance(provider, CondaDiagnosticProvider):
|
|
32
|
+
env = f"{provider.env_path}"
|
|
33
|
+
if not provider.env_path.exists():
|
|
34
|
+
env += " (not installed)"
|
|
35
|
+
return env
|
|
36
|
+
|
|
37
|
+
def get_data_path(provider: DiagnosticProvider) -> str:
|
|
38
|
+
"""Get the data cache path for a provider."""
|
|
39
|
+
data_path = provider.get_data_path()
|
|
40
|
+
if data_path is None:
|
|
41
|
+
return ""
|
|
42
|
+
path_str = str(data_path)
|
|
43
|
+
if not data_path.exists():
|
|
44
|
+
path_str += " (not fetched)"
|
|
45
|
+
return path_str
|
|
46
|
+
|
|
47
|
+
results_df = pd.DataFrame(
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"provider": provider.slug,
|
|
51
|
+
"version": provider.version,
|
|
52
|
+
"conda environment": get_env(provider),
|
|
53
|
+
"data path": get_data_path(provider),
|
|
54
|
+
}
|
|
55
|
+
for provider in provider_registry.providers
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
pretty_print_df(results_df, console=console)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command(deprecated=True)
|
|
62
|
+
def create_env(
|
|
63
|
+
ctx: typer.Context,
|
|
64
|
+
provider: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
typer.Option(help="Only install the environment for the named provider."),
|
|
67
|
+
] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Create a conda environment containing the provider software.
|
|
71
|
+
|
|
72
|
+
.. deprecated::
|
|
73
|
+
Use `ref providers setup` instead, which handles both environment creation
|
|
74
|
+
and data fetching in a single command.
|
|
75
|
+
|
|
76
|
+
If no provider is specified, all providers will be installed.
|
|
77
|
+
If the provider is up to date or does not use a conda environment, it will be skipped.
|
|
78
|
+
"""
|
|
79
|
+
warnings.warn(
|
|
80
|
+
"create-env is deprecated. Use 'ref providers setup' instead.",
|
|
81
|
+
DeprecationWarning,
|
|
82
|
+
stacklevel=2,
|
|
83
|
+
)
|
|
84
|
+
config = ctx.obj.config
|
|
85
|
+
db = ctx.obj.database
|
|
86
|
+
providers = ProviderRegistry.build_from_config(config, db).providers
|
|
87
|
+
|
|
88
|
+
if provider is not None:
|
|
89
|
+
available = ", ".join([f'"{p.slug}"' for p in providers])
|
|
90
|
+
providers = [p for p in providers if p.slug == provider]
|
|
91
|
+
if not providers:
|
|
92
|
+
msg = f'Provider "{provider}" not available. Choose from: {available}'
|
|
93
|
+
logger.error(msg)
|
|
94
|
+
raise typer.Exit(code=1)
|
|
95
|
+
|
|
96
|
+
for provider_ in providers:
|
|
97
|
+
txt = f"conda environment for provider {provider_.slug}"
|
|
98
|
+
if isinstance(provider_, CondaDiagnosticProvider):
|
|
99
|
+
logger.info(f"Creating {txt} in {provider_.env_path}")
|
|
100
|
+
provider_.create_env()
|
|
101
|
+
logger.info(f"Finished creating {txt}")
|
|
102
|
+
else:
|
|
103
|
+
logger.info(f"Skipping creating {txt} because it does not use conda environments.")
|
|
104
|
+
|
|
105
|
+
list_(ctx)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def setup(
|
|
110
|
+
ctx: typer.Context,
|
|
111
|
+
provider: Annotated[
|
|
112
|
+
str | None,
|
|
113
|
+
typer.Option(help="Only run setup for the named provider."),
|
|
114
|
+
] = None,
|
|
115
|
+
skip_env: Annotated[
|
|
116
|
+
bool,
|
|
117
|
+
typer.Option(help="Skip environment setup (e.g., conda)."),
|
|
118
|
+
] = False,
|
|
119
|
+
skip_data: Annotated[
|
|
120
|
+
bool,
|
|
121
|
+
typer.Option(help="Skip data fetching."),
|
|
122
|
+
] = False,
|
|
123
|
+
validate_only: Annotated[
|
|
124
|
+
bool,
|
|
125
|
+
typer.Option(help="Only validate setup, don't run it."),
|
|
126
|
+
] = False,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Run provider setup for offline execution.
|
|
130
|
+
|
|
131
|
+
This command prepares all providers for offline execution by:
|
|
132
|
+
|
|
133
|
+
1. Creating conda environments (if applicable)
|
|
134
|
+
|
|
135
|
+
2. Fetching required reference datasets to pooch cache
|
|
136
|
+
|
|
137
|
+
All operations are idempotent and safe to run multiple times.
|
|
138
|
+
Run this on a login node with internet access before solving on compute nodes.
|
|
139
|
+
"""
|
|
140
|
+
config = ctx.obj.config
|
|
141
|
+
db = ctx.obj.database
|
|
142
|
+
console = ctx.obj.console
|
|
143
|
+
providers = ProviderRegistry.build_from_config(config, db).providers
|
|
144
|
+
|
|
145
|
+
if provider is not None:
|
|
146
|
+
available = ", ".join([f'"{p.slug}"' for p in providers])
|
|
147
|
+
providers = [p for p in providers if p.slug == provider]
|
|
148
|
+
if not providers:
|
|
149
|
+
msg = f'Provider "{provider}" not available. Choose from: {available}'
|
|
150
|
+
logger.error(msg)
|
|
151
|
+
raise typer.Exit(code=1)
|
|
152
|
+
|
|
153
|
+
failed_providers: list[str] = []
|
|
154
|
+
|
|
155
|
+
for provider_ in providers:
|
|
156
|
+
if validate_only:
|
|
157
|
+
is_valid = provider_.validate_setup(config)
|
|
158
|
+
status = "[green]valid[/green]" if is_valid else "[red]invalid[/red]"
|
|
159
|
+
console.print(f"Provider {provider_.slug}: {status}")
|
|
160
|
+
if not is_valid:
|
|
161
|
+
failed_providers.append(provider_.slug)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
logger.info(f"Setting up provider {provider_.slug}")
|
|
165
|
+
try:
|
|
166
|
+
provider_.setup(config, skip_env=skip_env, skip_data=skip_data)
|
|
167
|
+
is_valid = provider_.validate_setup(config)
|
|
168
|
+
if not is_valid:
|
|
169
|
+
logger.error(f"Provider {provider_.slug} setup completed but validation failed")
|
|
170
|
+
failed_providers.append(provider_.slug)
|
|
171
|
+
else:
|
|
172
|
+
logger.info(f"Finished setting up provider {provider_.slug}")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.opt(exception=True).error(f"Failed to setup provider {provider_.slug}: {e}")
|
|
175
|
+
failed_providers.append(provider_.slug)
|
|
176
|
+
|
|
177
|
+
if failed_providers:
|
|
178
|
+
msg = f"Setup failed for providers: {', '.join(failed_providers)}"
|
|
179
|
+
logger.error(msg)
|
|
180
|
+
raise typer.Exit(code=1)
|
|
181
|
+
|
|
182
|
+
if not validate_only:
|
|
183
|
+
list_(ctx)
|