feat(backend): adopt climate_ref.results.Reader for metric values#39
feat(backend): adopt climate_ref.results.Reader for metric values#39lewisjared wants to merge 7 commits into
Conversation
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.
✅ Deploy Preview for climate-ref canceled.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe change wires a shared ChangesReader-backed metric values
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
backend/src/ref_backend/api/routes/executions.py (1)
341-375: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the shared scalar/series/CSV branching.
This block is essentially identical to
diagnostics.py'slist_metric_values(filter build aside). A shared helper taking aMetricValueFilterplus 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 winCSV generators buffer the entire output in memory before yielding.
Both
generate_csvclosures write all rows into a singleio.StringIOand 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
StreamingResponsecan 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
⛔ Files ignored due to path filters (2)
backend/tests/test-data/tests.integration.test_cmip7_aft/test_solve_cmip7_aft/db/climate_ref.dbis excluded by!**/*.dbbackend/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
backend/pyproject.tomlbackend/src/ref_backend/api/deps.pybackend/src/ref_backend/api/routes/diagnostics.pybackend/src/ref_backend/api/routes/executions.pybackend/src/ref_backend/core/reader_values.pybackend/src/ref_backend/models.pychangelog/39.breaking.mdchangelog/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).
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.
Summary
Migrates the backend's two
/valuesendpoints off hand-rolled SQLAlchemy queries and onto the newclimate_ref.results.Readerread-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}/valuesandGET /diagnostics/{provider}/{diagnostic}/valuesnow queryapp_context.reader.values.{scalar,series}_values(...)viaMetricValueFilter+OutlierPolicy.get_readerdependency onAppContext(additive; the raw session dependency is untouched and still used by the not-yet-migrated routes).core/reader_values.pyfor CSV rendering from reader collections and CV-dimension query-param gating;MetricValueCollection.build_{scalar,series}_from_readeradapters inmodels.py.Response shapes are unchanged.
promoted_only=Falseandinclude_retracted=Trueare 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)
>=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 thev0.16.0git 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.>=3.12.diagnostic.promoted_versionwas 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
mypy(strict) andruffclean.Follow-ups (later phases)
core/outliers.pyand the query half ofcore/metric_values.pyonce nothing references them.float(item.value)would raise; none in the current fixture).Summary by CodeRabbit
New Features
Refactor