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 3 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.
|
📝 WalkthroughWalkthroughThis PR bumps the backend to require Python 3.12+ and climate-ref 0.16, pinned via a Git tag source. A new ChangesReader layer migration
Sequence Diagram(s)sequenceDiagram
participant Client
participant Route as diagnostics/executions route
participant Filter as MetricValueFilter/OutlierPolicy
participant Reader as app_context.reader.values
participant Model as MetricValueCollection
Client->>Route: GET .../values?query_params
Route->>Filter: build filter (dimensions, isolate/exclude ids)
Route->>Reader: scalar_values(filter, outlier_policy) / series_values(filter)
Reader-->>Route: ScalarValueCollection / SeriesValueCollection
alt format=csv
Route->>Route: generate_csv_response_scalar/series(collection)
Route-->>Client: StreamingResponse (CSV)
else JSON
Route->>Model: build_scalar_from_reader / build_series_from_reader(collection)
Model-->>Route: MetricValueCollection
Route-->>Client: MetricValueCollection (JSON)
end
🚥 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
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
Breaking Changes
New Features
Bug Fixes