Skip to content

feat(backend): adopt climate_ref.results.Reader for metric values#39

Open
lewisjared wants to merge 7 commits into
mainfrom
feat/adopt-results-reader
Open

feat(backend): adopt climate_ref.results.Reader for metric values#39
lewisjared wants to merge 7 commits into
mainfrom
feat/adopt-results-reader

Conversation

@lewisjared

@lewisjared lewisjared commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the backend's two /values endpoints off hand-rolled SQLAlchemy queries and onto the new climate_ref.results.Reader read-layer facade — the consumer that read layer was explicitly designed for. This is the first, keystone slice of a phased adoption (plan tracked separately); it targets the biggest duplication: metric-value filtering, source-id-aware IQR outlier detection, facet collection, and the load-all-then-paginate rule now all live upstream.

  • GET /executions/{group_id}/values and GET /diagnostics/{provider}/{diagnostic}/values now query app_context.reader.values.{scalar,series}_values(...) via MetricValueFilter + OutlierPolicy.
  • New get_reader dependency on AppContext (additive; the raw session dependency is untouched and still used by the not-yet-migrated routes).
  • New core/reader_values.py for CSV rendering from reader collections and CV-dimension query-param gating; MetricValueCollection.build_{scalar,series}_from_reader adapters in models.py.

Response shapes are unchanged. promoted_only=False and include_retracted=True are set explicitly to preserve the previous unfiltered behaviour rather than adopt the reader's stricter defaults — so this is behaviour-preserving, which the existing route tests confirm.

Dependency / environment changes (please review)

  • Requires climate-ref >= 0.16.0 (>=0.16.0,<0.17). v0.16.0 is tagged and GitHub-released but is not yet on PyPI (PyPI currently tops out at 0.14.6 — 0.14.7/0.15.0/0.16.0 all have green Release runs but no published artifact). It is therefore sourced from the v0.16.0 git tag via [tool.uv.sources]; the version constraint is already correct, so that source line can simply be deleted once 0.16.0 lands on PyPI.
  • Python floor moves 3.11 → 3.12, because climate-ref 0.16 requires >=3.12.
  • The decimated test-fixture DB is migrated to the current schema (it was 4 alembic revisions behind; diagnostic.promoted_version was missing). This was done with climate-ref's own migrations — the same path a deployed 0.13-era database will take. Deployed REF databases must be alembic-migrated to head before a reader-based backend can read them.

Testing

  • Full backend suite green: 185 passed, 1 xfailed (unchanged from the pre-migration baseline), against climate-ref v0.16.0.
  • mypy (strict) and ruff clean.

Follow-ups (later phases)

  • Migrate the executions/diagnostics list routes, datasets, and artifacts endpoints.
  • Delete the now-dead core/outliers.py and the query half of core/metric_values.py once nothing references them.
  • Harden the scalar adapter against null-valued scalars (float(item.value) would raise; none in the current fixture).

Summary by CodeRabbit

  • New Features

    • Added consistent metric-value retrieval for diagnostic and execution endpoints.
    • Added CSV downloads for scalar and series values, including dimensions, metadata and outlier details where applicable.
    • Added support for dimension filters, ID inclusion/exclusion, pagination and outlier detection.
    • Preserved existing API response formats while improving value and series handling.
  • Refactor

    • Centralised metric-value reading and response construction for more consistent results.

Route the two /values endpoints (executions and diagnostics) through the new
climate_ref.results read-layer facade instead of hand-rolled SQLAlchemy
queries, and add a get_reader dependency on AppContext for the migration.

- Query scalar/series values via MetricValueFilter + OutlierPolicy, with a
  new core/reader_values.py helper for CSV rendering and dimension-param
  gating. Response shapes are preserved: promoted_only=False and
  include_retracted=True keep the previous unfiltered behaviour rather than
  taking the reader's stricter defaults.
- Pin climate-ref to unreleased main for the read layer and relax the
  constraint to >=0.15,<0.16. climate-ref 0.15 requires Python >=3.12, so the
  backend floor moves 3.11 -> 3.12.
- Migrate the decimated test-fixture database to the 0.15 schema
  (adds diagnostic.promoted_version and later revisions); deployed databases
  will need the same alembic upgrade.
@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for climate-ref canceled.

Name Link
🔨 Latest commit e7f862c
🔍 Latest deploy log https://app.netlify.com/projects/climate-ref/deploys/6a54db5380e8230008b36270

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5beab3ed-ab74-4975-b489-ed6680db9339

📥 Commits

Reviewing files that changed from the base of the PR and between e9a6ca3 and e7f862c.

📒 Files selected for processing (4)
  • backend/src/ref_backend/api/routes/diagnostics.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/models.py
  • backend/tests/test_core/test_metric_values.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/src/ref_backend/api/routes/diagnostics.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/models.py

📝 Walkthrough

Walkthrough

The change wires a shared Reader into application context, adds reader-based filtering, CSV generation, and response builders, and refactors diagnostic and execution metric-value endpoints to use the reader workflow.

Changes

Reader-backed metric values

Layer / File(s) Summary
Reader dependency wiring
backend/src/ref_backend/api/deps.py
Constructs a Reader from the database and results path, then exposes it through AppContext.
Reader value helpers and response builders
backend/src/ref_backend/core/reader_values.py, backend/src/ref_backend/models.py, backend/tests/test_core/test_metric_values.py
Adds dimension parsing, scalar/series CSV generation, reader-to-response mapping, and tests for kinds, outliers, attributes, and references.
Diagnostics metric-value retrieval
backend/src/ref_backend/api/routes/diagnostics.py
Delegates filtering, outlier handling, retrieval, pagination, CSV generation, and JSON construction to reader-layer APIs.
Execution metric-value retrieval
backend/src/ref_backend/api/routes/executions.py, changelog/39.trivial.md
Applies the reader workflow to execution-scoped values and records the data-access change in the changelog.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MetricValueRoute
  participant Reader
  participant MetricValueCollection

  Client->>MetricValueRoute: request metric values
  MetricValueRoute->>Reader: apply MetricValueFilter and retrieve values
  Reader-->>MetricValueRoute: scalar or series collection
  MetricValueRoute->>MetricValueCollection: build API response
  MetricValueCollection-->>Client: JSON or CSV response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: switching backend metric-value endpoints to climate_ref.results.Reader.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adopt-results-reader

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Bump the constraint to >=0.16.0,<0.17 and source climate-ref from the v0.16.0
release tag. v0.16.0 is tagged and GitHub-released but has not been published
to PyPI yet (PyPI tops out at 0.14.6), so the uv source pin stays until it
lands; the version constraint is already correct and the pin can then be
deleted outright.

v0.16.0 introduces no new alembic revisions (head remains f6a7b8c9d0e1), so
the migrated test fixture is unchanged. Python floor stays at 3.12.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
backend/src/ref_backend/api/routes/executions.py (1)

341-375: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared scalar/series/CSV branching.

This block is essentially identical to diagnostics.py's list_metric_values (filter build aside). A shared helper taking a MetricValueFilter plus the value-type/format/pagination/outlier args would remove the duplication and keep the two endpoints' response behaviour in lock-step as the reader layer evolves.

backend/src/ref_backend/core/reader_values.py (1)

37-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

CSV generators buffer the entire output in memory before yielding.

Both generate_csv closures write all rows into a single io.StringIO and yield once at the end. Since CSV export deliberately returns all results without pagination, the full collection is already in memory, and the buffer roughly doubles that footprint. For large diagnostics this can cause significant memory pressure on the worker.

Consider yielding rows (or batches of rows) as they are written so StreamingResponse can flush incrementally:

♻️ Proposed refactor: yield per-row instead of buffering
     def generate_csv() -> Generator[str]:
         output = io.StringIO()
         writer = csv.writer(output)

         items = collection.items
         if not items:
             yield ""
             return

         dimensions = sorted(items[0].dimensions.keys())
         header = [*dimensions, "value", "type"]
         if detection_ran:
             header.extend(["is_outlier", "verification_status"])
         writer.writerow(header)
+        yield output.getvalue()
+        output.seek(0)
+        output.truncate(0)

         for item in items:
             row = [item.dimensions.get(d) for d in dimensions] + [
                 sanitize_float_value(item.value),
                 "scalar",
             ]
             if detection_ran:
                 row.extend([item.is_outlier, item.verification_status])
             writer.writerow(row)
+            yield output.getvalue()
+            output.seek(0)
+            output.truncate(0)
-
-        output.seek(0)
-        yield output.read()

The same pattern applies to generate_csv_response_series (lines 87-112). For fewer, larger chunks, accumulate N rows before yielding.

Also applies to: 87-112


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62c6e7e3-e968-4cd4-a930-25a867d6132a

📥 Commits

Reviewing files that changed from the base of the PR and between e501f1d and e9a6ca3.

⛔ Files ignored due to path filters (2)
  • backend/tests/test-data/tests.integration.test_cmip7_aft/test_solve_cmip7_aft/db/climate_ref.db is excluded by !**/*.db
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • backend/pyproject.toml
  • backend/src/ref_backend/api/deps.py
  • backend/src/ref_backend/api/routes/diagnostics.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/core/reader_values.py
  • backend/src/ref_backend/models.py
  • changelog/39.breaking.md
  • changelog/39.trivial.md

…ader

* origin/main:
  chore(deps): pin climate-ref to the v0.16.0 tag instead of a commit sha
  fix: force reference rows non-outlier on the no-source_id fallback path
  fix: align index-less series by row ordinal in mixed charts
  docs(changelog): add news fragments for the metric-value contract PR
  fix: correct series index alignment and null-safe presentation units
  feat: honor per-series units and calendar and align series by index value
  feat: render model-vs-reference role from kind and dedup references by reference_id
  chore(client): regenerate API client for metric-value contract fields
  chore(test): migrate fixture database to climate-ref 0.15 schema
  refactor: key outlier detection off metric-value kind, not the source_id sentinel
  feat: surface kind, reference_id and presentation units on metric-value DTOs

# Conflicts:
#	backend/tests/test-data/tests.integration.test_cmip7_aft/test_solve_cmip7_aft/db/climate_ref.db
Both /values endpoints now set promoted_only=True, so metric values from
superseded diagnostic versions are hidden. This drops the earlier
behaviour-preserving promoted_only=False. Exposing previous versions is left
for a separate design.

Note: the execution-scoped endpoint now returns no values when the selected
execution belongs to a non-promoted version. Retracted executions are still
included (unchanged).
The Python 3.12 requirement is already recorded in 32.breaking.md (merged via
PR #32). The reader adoption itself introduces no breaking change, so #39 keeps
only its trivial fragment.
build_series_from_reader dropped kind, reference_id and the presentation
attributes (value_units, value_long_name, index_units, calendar), so the
reader-based /values series path silently lost the model/reference role,
reference dedup key and per-series axis units that the frontend charts rely on.
Populate them from the reader DTO, mirroring the previous build_series.

Also removes the now-dead build_scalar/build_series builders (production used
only the *_from_reader variants) and the _normalize_kind helper they depended
on, and moves their field-mapping coverage onto the reader builders.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant