climate-ref 0.5.0__tar.gz → 0.5.1__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 (79) hide show
  1. {climate_ref-0.5.0 → climate_ref-0.5.1}/.gitignore +0 -1
  2. {climate_ref-0.5.0 → climate_ref-0.5.1}/Dockerfile +15 -6
  3. {climate_ref-0.5.0 → climate_ref-0.5.1}/PKG-INFO +3 -1
  4. {climate_ref-0.5.0 → climate_ref-0.5.1}/pyproject.toml +3 -1
  5. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/__init__.py +18 -8
  6. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/datasets.py +31 -27
  7. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/executions.py +1 -1
  8. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/providers.py +2 -4
  9. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/solve.py +1 -2
  10. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/config.py +5 -6
  11. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/constants.py +1 -1
  12. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/database.py +1 -0
  13. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/dataset_registry/sample_data.txt +14 -0
  14. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/base.py +43 -39
  15. climate_ref-0.5.1/src/climate_ref/executor/__init__.py +16 -0
  16. climate_ref-0.5.1/src/climate_ref/executor/local.py +222 -0
  17. climate_ref-0.5.0/src/climate_ref/executor/__init__.py → climate_ref-0.5.1/src/climate_ref/executor/result_handling.py +9 -52
  18. climate_ref-0.5.1/src/climate_ref/executor/synchronous.py +62 -0
  19. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/migrations/env.py +1 -0
  20. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/migrations/versions/2025-05-02T1418_341a4aa2551e_regenerate.py +0 -21
  21. climate_ref-0.5.1/src/climate_ref/migrations/versions/2025-05-09T2032_03dbb4998e49_series_metric_value.py +57 -0
  22. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/__init__.py +3 -1
  23. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/base.py +2 -0
  24. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/metric_value.py +138 -13
  25. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/provider_registry.py +1 -1
  26. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/solver.py +18 -30
  27. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/testing.py +11 -7
  28. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_datasets.py +22 -0
  29. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_root.py +25 -1
  30. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_cmip6/cmip6_catalog_db.yml +491 -0
  31. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_cmip6/cmip6_catalog_local.yml +505 -0
  32. climate_ref-0.5.1/tests/unit/executor/test_local_executor.py +93 -0
  33. climate_ref-0.5.0/tests/unit/executor/test_executor.py → climate_ref-0.5.1/tests/unit/executor/test_result_handling.py +39 -51
  34. climate_ref-0.5.0/tests/unit/executor/test_local_executor.py → climate_ref-0.5.1/tests/unit/executor/test_synchronous_executor.py +13 -10
  35. climate_ref-0.5.1/tests/unit/models/test_metric_value.py +124 -0
  36. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/test_config.py +2 -2
  37. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/test_solver.py +6 -11
  38. climate_ref-0.5.0/src/climate_ref/executor/local.py +0 -89
  39. climate_ref-0.5.0/tests/unit/models/test_metric_value.py +0 -67
  40. {climate_ref-0.5.0 → climate_ref-0.5.1}/LICENCE +0 -0
  41. {climate_ref-0.5.0 → climate_ref-0.5.1}/NOTICE +0 -0
  42. {climate_ref-0.5.0 → climate_ref-0.5.1}/README.md +0 -0
  43. {climate_ref-0.5.0 → climate_ref-0.5.1}/conftest.py +0 -0
  44. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/__init__.py +0 -0
  45. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/_config_helpers.py +0 -0
  46. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/alembic.ini +0 -0
  47. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/_utils.py +0 -0
  48. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/cli/config.py +0 -0
  49. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/dataset_registry/obs4ref_reference.txt +0 -0
  50. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/__init__.py +0 -0
  51. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/cmip6.py +0 -0
  52. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/obs4mips.py +0 -0
  53. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/pmp_climatology.py +0 -0
  54. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/datasets/utils.py +0 -0
  55. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/migrations/README +0 -0
  56. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/migrations/script.py.mako +0 -0
  57. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/dataset.py +0 -0
  58. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/diagnostic.py +0 -0
  59. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/execution.py +0 -0
  60. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/models/provider.py +0 -0
  61. {climate_ref-0.5.0 → climate_ref-0.5.1}/src/climate_ref/py.typed +0 -0
  62. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_config.py +0 -0
  63. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_executions/test_inspect.txt +0 -0
  64. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_executions.py +0 -0
  65. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_providers.py +0 -0
  66. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/cli/test_solve.py +0 -0
  67. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/conftest.py +0 -0
  68. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_cmip6.py +0 -0
  69. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_datasets.py +0 -0
  70. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_obs4mips/obs4mips_catalog_db.yml +0 -0
  71. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_obs4mips/obs4mips_catalog_local.yml +0 -0
  72. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_obs4mips.py +0 -0
  73. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_pmp_climatology/pmp_catalog_local.yml +0 -0
  74. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_pmp_climatology.py +0 -0
  75. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/datasets/test_utils.py +0 -0
  76. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/models/test_metric_execution.py +0 -0
  77. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/test_database.py +0 -0
  78. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/test_provider_registry.py +0 -0
  79. {climate_ref-0.5.0 → climate_ref-0.5.1}/tests/unit/test_solver/test_solve_metrics.yml +0 -0
@@ -74,7 +74,6 @@ coverage.xml
74
74
  *.pot
75
75
 
76
76
  # Django stuff:
77
- *.log
78
77
  local_settings.py
79
78
  db.sqlite3
80
79
  db.sqlite3-journal
@@ -31,26 +31,35 @@ RUN --mount=type=cache,target=/root/.cache/uv \
31
31
  uv sync --frozen --no-editable --no-dev
32
32
 
33
33
  # Runtime container
34
- # Copy the installed packages from the build stage to decrease the size of the final image
35
34
  FROM python:3.11-slim AS runtime
36
35
 
37
36
  LABEL maintainer="Jared Lewis <jared.lewis@climate-resource.com>"
38
- LABEL description="Docker image for the REF compute engine"
37
+ LABEL description="Base Docker image for the REF compute engine"
38
+
39
+ RUN apt-get update && apt-get install -y --no-install-recommends \
40
+ git \
41
+ && rm -rf /var/lib/apt/lists/*
42
+
43
+ # Create a non-root user
44
+ RUN useradd -m -u 1000 app
39
45
 
40
46
  # Allow celery to run as root
41
- ENV C_FORCE_ROOT=false
42
47
  ENV PATH="/app/.venv/bin:${PATH}"
48
+ ENV VIRTUAL_ENV=/app/.venv
43
49
 
44
50
  # Copy the installed packages from the build stage
51
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /app/.venv/bin/
45
52
  COPY --from=build --chown=app:app /app/.venv /app/.venv
46
53
  COPY --from=build --chown=app:app /app/scripts /app/scripts
47
54
 
48
55
  # Location of the REF configuration files
49
56
  ENV REF_CONFIGURATION=/ref
50
- ENV REF_METRICS_ESMVALTOOL_DATA_DIR=/app/cache/esmvaltool
51
57
 
52
- #RUN ref datasets fetch-data --registry ilamb
53
- #RUN ref datasets fetch-data --registry iomb
58
+ # Create necessary directories with proper permissions
59
+ RUN mkdir -p /ref /app/cache && chown -R app:app /ref /app/cache
60
+
61
+ # Switch to non-root user
62
+ USER app
54
63
 
55
64
  # Run the REF CLI tool by default
56
65
  ENTRYPOINT ["/app/.venv/bin/ref"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: climate-ref
3
- Version: 0.5.0
3
+ Version: 0.5.1
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: Apache-2.0
@@ -28,6 +28,7 @@ Requires-Dist: platformdirs>=4.3.6
28
28
  Requires-Dist: setuptools>=75.8.0
29
29
  Requires-Dist: sqlalchemy>=2.0.36
30
30
  Requires-Dist: tomlkit>=0.13.2
31
+ Requires-Dist: tqdm>=4.67.1
31
32
  Requires-Dist: typer>=0.12.5
32
33
  Provides-Extra: celery
33
34
  Requires-Dist: climate-ref-celery>=0.5.0; extra == 'celery'
@@ -36,6 +37,7 @@ Requires-Dist: climate-ref-esmvaltool>=0.5.0; extra == 'metrics'
36
37
  Requires-Dist: climate-ref-ilamb>=0.5.0; extra == 'metrics'
37
38
  Requires-Dist: climate-ref-pmp>=0.5.0; extra == 'metrics'
38
39
  Provides-Extra: postgres
40
+ Requires-Dist: alembic-postgresql-enum>=1.7.0; extra == 'postgres'
39
41
  Requires-Dist: psycopg2-binary>=2.9.2; extra == 'postgres'
40
42
  Description-Content-Type: text/markdown
41
43
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "climate-ref"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Application which runs the CMIP Rapid Evaluation Framework"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -38,11 +38,13 @@ dependencies = [
38
38
  "ecgtools>=2024.7.31",
39
39
  "platformdirs>=4.3.6",
40
40
  "setuptools>=75.8.0",
41
+ "tqdm>=4.67.1"
41
42
  ]
42
43
 
43
44
  [project.optional-dependencies]
44
45
  postgres = [
45
46
  "psycopg2-binary>=2.9.2",
47
+ "alembic-postgresql-enum>=1.7.0",
46
48
  ]
47
49
  celery = [
48
50
  "climate-ref-celery>=0.5.0",
@@ -12,7 +12,7 @@ from loguru import logger
12
12
  from climate_ref import __version__
13
13
  from climate_ref.cli import config, datasets, executions, providers, solve
14
14
  from climate_ref.config import Config
15
- from climate_ref.constants import config_filename
15
+ from climate_ref.constants import CONFIG_FILENAME
16
16
  from climate_ref.database import Database
17
17
  from climate_ref_core import __version__ as __core_version__
18
18
  from climate_ref_core.logging import add_log_handler
@@ -23,7 +23,8 @@ class LogLevel(str, Enum):
23
23
  Log levels for the CLI
24
24
  """
25
25
 
26
- Normal = "WARNING"
26
+ Error = "ERROR"
27
+ Warning = "WARNING"
27
28
  Debug = "DEBUG"
28
29
  Info = "INFO"
29
30
 
@@ -65,7 +66,7 @@ def _load_config(configuration_directory: Path | None = None) -> Config:
65
66
  """
66
67
  try:
67
68
  if configuration_directory:
68
- config = Config.load(configuration_directory / config_filename, allow_missing=False)
69
+ config = Config.load(configuration_directory / CONFIG_FILENAME, allow_missing=False)
69
70
  else:
70
71
  config = Config.default()
71
72
  except FileNotFoundError:
@@ -109,19 +110,28 @@ app = build_app()
109
110
 
110
111
 
111
112
  @app.callback()
112
- def main(
113
+ def main( # noqa: PLR0913
113
114
  ctx: typer.Context,
114
115
  configuration_directory: Annotated[Path | None, typer.Option(help="Configuration directory")] = None,
115
- verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False,
116
- log_level: Annotated[LogLevel, typer.Option(case_sensitive=False)] = LogLevel.Normal,
116
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Set the log level to DEBUG")] = False,
117
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Set the log level to WARNING")] = False,
118
+ log_level: Annotated[
119
+ LogLevel, typer.Option(case_sensitive=False, help="Set the level of logging information to display")
120
+ ] = LogLevel.Info,
117
121
  version: Annotated[
118
122
  Optional[bool],
119
- typer.Option("--version", callback=_version_callback, is_eager=True),
123
+ typer.Option(
124
+ "--version", callback=_version_callback, is_eager=True, help="Print the version and exit"
125
+ ),
120
126
  ] = None,
121
127
  ) -> None:
122
128
  """
123
- climate_ref: A CLI for the CMIP Rapid Evaluation Framework
129
+ climate_ref: A CLI for the Assessment Fast Track Rapid Evaluation Framework
130
+
131
+ This CLI provides a number of commands for managing and executing diagnostics.
124
132
  """
133
+ if quiet:
134
+ log_level = LogLevel.Warning
125
135
  if verbose:
126
136
  log_level = LogLevel.Debug
127
137
 
@@ -90,7 +90,7 @@ def list_columns(
90
90
  @app.command()
91
91
  def ingest( # noqa: PLR0913
92
92
  ctx: typer.Context,
93
- file_or_directory: Path,
93
+ file_or_directory: list[Path],
94
94
  source_type: Annotated[SourceDatasetType, typer.Option(help="Type of source dataset")],
95
95
  solve: Annotated[bool, typer.Option(help="Solve for new diagnostic executions after ingestion")] = False,
96
96
  dry_run: Annotated[bool, typer.Option(help="Do not ingest datasets into the database")] = False,
@@ -107,40 +107,44 @@ def ingest( # noqa: PLR0913
107
107
  config = ctx.obj.config
108
108
  db = ctx.obj.database
109
109
 
110
- file_or_directory = Path(file_or_directory).expanduser()
111
- logger.info(f"ingesting {file_or_directory}")
112
-
113
110
  kwargs = {}
114
111
 
115
112
  if n_jobs is not None:
116
113
  kwargs["n_jobs"] = n_jobs
117
114
 
115
+ # Create a data catalog from the specified file or directory
118
116
  adapter = get_dataset_adapter(source_type.value, **kwargs)
119
117
 
120
- # Create a data catalog from the specified file or directory
121
- if not file_or_directory.exists():
122
- logger.error(f"File or directory {file_or_directory} does not exist")
123
- raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file_or_directory)
124
-
125
- data_catalog = adapter.find_local_datasets(file_or_directory)
126
- data_catalog = adapter.validate_data_catalog(data_catalog, skip_invalid=skip_invalid)
127
-
128
- logger.info(
129
- f"Found {len(data_catalog)} files for {len(data_catalog[adapter.slug_column].unique())} datasets"
130
- )
131
- pretty_print_df(adapter.pretty_subset(data_catalog), console=console)
132
-
133
- for instance_id, data_catalog_dataset in data_catalog.groupby(adapter.slug_column):
134
- logger.info(f"Processing dataset {instance_id}")
135
-
136
- if dry_run:
137
- dataset = db.session.query(Dataset).filter_by(slug=instance_id, dataset_type=source_type).first()
138
- if not dataset:
139
- logger.info(f"Would save dataset {instance_id} to the database")
140
- continue
141
- else:
118
+ for _dir in file_or_directory:
119
+ _dir = Path(_dir).expanduser()
120
+ logger.info(f"Ingesting {_dir}")
121
+
122
+ if not _dir.exists():
123
+ logger.error(f"File or directory {_dir} does not exist")
124
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), _dir)
125
+
126
+ data_catalog = adapter.find_local_datasets(_dir)
127
+ data_catalog = adapter.validate_data_catalog(data_catalog, skip_invalid=skip_invalid)
128
+
129
+ logger.info(
130
+ f"Found {len(data_catalog)} files for {len(data_catalog[adapter.slug_column].unique())} datasets"
131
+ )
132
+ pretty_print_df(adapter.pretty_subset(data_catalog), console=console)
133
+
134
+ for instance_id, data_catalog_dataset in data_catalog.groupby(adapter.slug_column):
135
+ logger.debug(f"Processing dataset {instance_id}")
142
136
  with db.session.begin():
143
- adapter.register_dataset(config, db, data_catalog_dataset)
137
+ if dry_run:
138
+ dataset = (
139
+ db.session.query(Dataset)
140
+ .filter_by(slug=instance_id, dataset_type=source_type)
141
+ .first()
142
+ )
143
+ if not dataset:
144
+ logger.info(f"Would save dataset {instance_id} to the database")
145
+ continue
146
+ else:
147
+ adapter.register_dataset(config, db, data_catalog_dataset)
144
148
 
145
149
  if solve:
146
150
  solve_required_executions(
@@ -20,7 +20,7 @@ from climate_ref.cli._utils import df_to_table, pretty_print_df
20
20
  from climate_ref.config import Config
21
21
  from climate_ref.models import Execution, ExecutionGroup
22
22
  from climate_ref.models.execution import get_execution_group_and_latest
23
- from climate_ref_core.executor import EXECUTION_LOG_FILENAME
23
+ from climate_ref_core.logging import EXECUTION_LOG_FILENAME
24
24
 
25
25
  app = typer.Typer(help=__doc__)
26
26
  console = Console()
@@ -24,8 +24,7 @@ def list_(ctx: typer.Context) -> None:
24
24
  """
25
25
  config = ctx.obj.config
26
26
  db = ctx.obj.database
27
- with db.session.begin():
28
- provider_registry = ProviderRegistry.build_from_config(config, db)
27
+ provider_registry = ProviderRegistry.build_from_config(config, db)
29
28
 
30
29
  def get_env(provider: DiagnosticProvider) -> str:
31
30
  env = ""
@@ -61,8 +60,7 @@ def create_env(
61
60
  """
62
61
  config = ctx.obj.config
63
62
  db = ctx.obj.database
64
- with db.session.begin():
65
- providers = ProviderRegistry.build_from_config(config, db).providers
63
+ providers = ProviderRegistry.build_from_config(config, db).providers
66
64
 
67
65
  if provider is not None:
68
66
  available = ", ".join([f'"{p.slug}"' for p in providers])
@@ -19,5 +19,4 @@ def solve(
19
19
  """
20
20
  config = ctx.obj.config
21
21
  db = ctx.obj.database
22
- with ctx.obj.database.session.begin():
23
- solve_required_executions(config=config, db=db, dry_run=dry_run, timeout=timeout)
22
+ solve_required_executions(config=config, db=db, dry_run=dry_run, timeout=timeout)
@@ -34,11 +34,10 @@ from climate_ref._config_helpers import (
34
34
  env_field,
35
35
  transform_error,
36
36
  )
37
- from climate_ref.constants import config_filename
38
- from climate_ref.executor import import_executor_cls
37
+ from climate_ref.constants import CONFIG_FILENAME
39
38
  from climate_ref_core.env import env
40
39
  from climate_ref_core.exceptions import InvalidExecutorException
41
- from climate_ref_core.executor import Executor
40
+ from climate_ref_core.executor import Executor, import_executor_cls
42
41
 
43
42
  if TYPE_CHECKING:
44
43
  from climate_ref.database import Database
@@ -156,12 +155,12 @@ class ExecutorConfig:
156
155
  Configuration to define the executor to use for running diagnostics
157
156
  """
158
157
 
159
- executor: str = env_field(name="EXECUTOR", default="climate_ref.executor.local.LocalExecutor")
158
+ executor: str = env_field(name="EXECUTOR", default="climate_ref.executor.LocalExecutor")
160
159
  """
161
160
  Executor to use for running diagnostics
162
161
 
163
162
  This should be the fully qualified name of the executor class
164
- (e.g. `climate_ref.executor.local.LocalExecutor`).
163
+ (e.g. `climate_ref.executor.LocalExecutor`).
165
164
  The default is to use the local executor.
166
165
  The environment variable `REF_EXECUTOR` takes precedence over this configuration value.
167
166
 
@@ -405,7 +404,7 @@ class Config:
405
404
  The default configuration
406
405
  """
407
406
  root = env.path("REF_CONFIGURATION")
408
- path_to_load = root / config_filename
407
+ path_to_load = root / CONFIG_FILENAME
409
408
 
410
409
  logger.debug(f"Loading default configuration from {path_to_load}")
411
410
  return cls.load(path_to_load)
@@ -2,7 +2,7 @@
2
2
  Constants used by the REF
3
3
  """
4
4
 
5
- config_filename = "ref.toml"
5
+ CONFIG_FILENAME = "ref.toml"
6
6
  """
7
7
  Default name of the configuration file
8
8
  """
@@ -106,6 +106,7 @@ class Database:
106
106
  logger.info(f"Connecting to database at {url}")
107
107
  self.url = url
108
108
  self._engine = sqlalchemy.create_engine(self.url)
109
+ # TODO: Set autobegin=False
109
110
  self.session = Session(self._engine)
110
111
 
111
112
  def alembic_config(self, config: "Config") -> AlembicConfig:
@@ -18,12 +18,26 @@ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/abrupt-4xCO2/r1i1p1f1/fx/areacella/gn/v20191115/a
18
18
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/pr/gn/v20191115/pr_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_185001-201412.nc acc821dc400f53166379d2e23095bc2690d7ca7db6c7a6f88ae29a8771b3c65a
19
19
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/psl/gn/v20191115/psl_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc b63a3d4051cf17568df808836b189826da580ca8e1db949b1e93a71c80756c8d
20
20
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/rlut/gn/v20191115/rlut_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 44a3c90a41744101afb00344f50947fe46444fe5d6bd3623c0c19aa02a378c86
21
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/rlutcs/gn/v20191115/rlutcs_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200501-201412.nc 50b54337ffcf581236929c4eb904bc8240f848a5fa646de75129ed5cbddbbc23
21
22
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/rsdt/gn/v20191115/rsdt_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc a4e1fc3a4a4d00c2fa18ec616338426eb3d91165db3bc57e565ffdc8d6bd9d34
22
23
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/rsut/gn/v20191115/rsut_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 8d492ef1f2bb654220fe64977d9942a33af0962ee9afa4017dcc75b6f0103015
24
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/rsutcs/gn/v20191115/rsutcs_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200501-201412.nc 8bae5124b8fe5040853e9b02f2942ee4c81cfd972b314f2828c04a9965804357
23
25
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/tas/gn/v20191115/tas_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_185001-201412.nc 38e055e57aea5a9ae76ed3fc5325be6783b5694a9edc28aafd24dd462b32e5ce
24
26
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Amon/ts/gn/v20191115/ts_Amon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc e02530449c92e0ffc72e9edeba57f5d38ab8652a28486c1c2b9ddada1f38fbd9
27
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Emon/cSoil/gn/v20191115/cSoil_Emon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc bebda54ca0518630e90b79585910fc38b7edfe118ecf1cf4cb4a8de0950a911e
25
28
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Lmon/gpp/gn/v20191115/gpp_Lmon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc da36ed1653f7aafe40a4fc9b99004a46cb45231697ce6b3413dfc171980c37df
29
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Lmon/lai/gn/v20191115/lai_Lmon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 972c11880af2cf06c2e1489968b1ac4829d8b00afd7586499814c3ddcfd95ed2
30
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Lmon/mrro/gn/v20191115/mrro_Lmon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc aaaf89f7a1dedf79e8cda71ab345a6809cfb698a63dcc638ccf7a316d13e6920
31
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Lmon/mrsos/gn/v20191115/mrsos_Lmon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 4b78059c4f899a48ae6f3a3cf68e95d76e3603044744521c4aadd992dec93995
32
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Lmon/nbp/gn/v20191115/nbp_Lmon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 75e559842e9081ea7563f2590d2c1e8a22af72abc78e37a012b9d56da532569e
26
33
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Ofx/areacello/gn/v20191115/areacello_Ofx_ACCESS-ESM1-5_historical_r1i1p1f1_gn.nc 6808b64c7328bd118537bfb7cfd35748b4e84cae3f6a5586403aa9d8040e4d0b
34
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Ofx/sftof/gn/v20191115/sftof_Ofx_ACCESS-ESM1-5_historical_r1i1p1f1_gn.nc 9bc037566546b8a65d063c4e8225b43b56151856f5a8adde5992f44c85b7c727
35
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/msftmz/gn/v20191115/msftmz_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_197901-197912.nc f068351200f0afb451a0e39e13d5e3ddeb48b1f1812b97a1a786d802659c969c
36
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/msftmz/gn/v20191115/msftmz_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_198001-198912.nc 703b495bf2effa5cae369893e2868ae1f38b69510366404e236a4605e6560ae6
37
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/msftmz/gn/v20191115/msftmz_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_199001-199912.nc 79a7ef5b61962da66abd14598412ad4f1ba0c9b531e7ecaf5a6190e0a4f9589c
38
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/msftmz/gn/v20191115/msftmz_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-200912.nc 001b3e7c639cae224e4de4f14601f492bec7aeb028cd02e9f07812c1db05abb7
39
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/msftmz/gn/v20191115/msftmz_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_201001-201412.nc 6c6cc432feb33552d643b2a57d0552ac563ec5a90ad462b6daeacdf3e7a9158f
40
+ CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/sos/gn/v20191115/sos_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_197901-201412.nc 92384dba703a6575d696bf8c04337c3a1d4f538bcd9ca28bf61ab058f8038b30
27
41
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/Omon/tos/gn/v20191115/tos_Omon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_200001-201412.nc 94121a2233aff78ef8799c5d59b6b6f3e7d3f2fb7ceb3a4a1645943ef3e88040
28
42
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/SImon/siconc/gn/v20200817/siconc_SImon_ACCESS-ESM1-5_historical_r1i1p1f1_gn_197901-201412.nc 886c62a24797e893fe11b8de4d16c8a277bdee931b692d533f2fb3fa39820aa1
29
43
  CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/historical/r1i1p1f1/fx/areacella/gn/v20191115/areacella_fx_ACCESS-ESM1-5_historical_r1i1p1f1_gn.nc 4587a7b0110a226a805f681ee9fe456d20ec310302b2c120334c21595e4e96cb
@@ -156,6 +156,43 @@ class DatasetAdapter(Protocol):
156
156
  )
157
157
  return dataset
158
158
 
159
+ def _get_dataset_files(self, db: Database, limit: int | None = None) -> pd.DataFrame:
160
+ dataset_type = self.dataset_cls.__mapper_args__["polymorphic_identity"]
161
+
162
+ result = (
163
+ db.session.query(DatasetFile)
164
+ # The join is necessary to be able to order by the dataset columns
165
+ .join(DatasetFile.dataset)
166
+ .where(Dataset.dataset_type == dataset_type)
167
+ # The joinedload is necessary to avoid N+1 queries (one for each dataset)
168
+ # https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#the-zen-of-joined-eager-loading
169
+ .options(joinedload(DatasetFile.dataset.of_type(self.dataset_cls)))
170
+ .order_by(Dataset.updated_at.desc())
171
+ .limit(limit)
172
+ .all()
173
+ )
174
+
175
+ return pd.DataFrame(
176
+ [
177
+ {
178
+ **{k: getattr(file, k) for k in self.file_specific_metadata},
179
+ **{k: getattr(file.dataset, k) for k in self.dataset_specific_metadata},
180
+ }
181
+ for file in result
182
+ ],
183
+ index=[file.dataset.id for file in result],
184
+ )
185
+
186
+ def _get_datasets(self, db: Database, limit: int | None = None) -> pd.DataFrame:
187
+ result_datasets = (
188
+ db.session.query(self.dataset_cls).order_by(Dataset.updated_at.desc()).limit(limit).all()
189
+ )
190
+
191
+ return pd.DataFrame(
192
+ [{k: getattr(dataset, k) for k in self.dataset_specific_metadata} for dataset in result_datasets],
193
+ index=[file.id for file in result_datasets],
194
+ )
195
+
159
196
  def load_catalog(
160
197
  self, db: Database, include_files: bool = True, limit: int | None = None
161
198
  ) -> pd.DataFrame:
@@ -173,42 +210,9 @@ class DatasetAdapter(Protocol):
173
210
  :
174
211
  Data catalog containing the metadata for the currently ingested datasets
175
212
  """
176
- DatasetModel = self.dataset_cls
177
- dataset_type = DatasetModel.__mapper_args__["polymorphic_identity"]
178
- # TODO: Paginate this query to avoid loading all the data at once
179
- if include_files:
180
- result = (
181
- db.session.query(DatasetFile)
182
- # The join is necessary to be able to order by the dataset columns
183
- .join(DatasetFile.dataset)
184
- .where(Dataset.dataset_type == dataset_type)
185
- # The joinedload is necessary to avoid N+1 queries (one for each dataset)
186
- # https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#the-zen-of-joined-eager-loading
187
- .options(joinedload(DatasetFile.dataset.of_type(DatasetModel)))
188
- .order_by(Dataset.updated_at.desc())
189
- .limit(limit)
190
- .all()
191
- )
192
-
193
- return pd.DataFrame(
194
- [
195
- {
196
- **{k: getattr(file, k) for k in self.file_specific_metadata},
197
- **{k: getattr(file.dataset, k) for k in self.dataset_specific_metadata},
198
- }
199
- for file in result
200
- ],
201
- index=[file.dataset.id for file in result],
202
- )
203
- else:
204
- result_datasets = (
205
- db.session.query(DatasetModel).order_by(Dataset.updated_at.desc()).limit(limit).all()
206
- )
207
-
208
- return pd.DataFrame(
209
- [
210
- {k: getattr(dataset, k) for k in self.dataset_specific_metadata}
211
- for dataset in result_datasets
212
- ],
213
- index=[file.id for file in result_datasets],
214
- )
213
+ with db.session.begin():
214
+ # TODO: Paginate this query to avoid loading all the data at once
215
+ if include_files:
216
+ return self._get_dataset_files(db, limit)
217
+ else:
218
+ return self._get_datasets(db, limit)
@@ -0,0 +1,16 @@
1
+ """
2
+ Execute diagnostics in different environments
3
+
4
+ We support running diagnostics in different environments, such as locally,
5
+ in a separate process, or in a container.
6
+ These environments are represented by `climate_ref.executor.Executor` classes.
7
+
8
+ The simplest executor is the `LocalExecutor`, which runs the diagnostic in the same process.
9
+ This is useful for local testing and debugging.
10
+ """
11
+
12
+ from .local import LocalExecutor
13
+ from .result_handling import handle_execution_result
14
+ from .synchronous import SynchronousExecutor
15
+
16
+ __all__ = ["LocalExecutor", "SynchronousExecutor", "handle_execution_result"]