Skip to content

release: promote solvis Strawberry migration to prod#97

Merged
chrisbc merged 42 commits into
mainfrom
deploy-test
Jun 26, 2026
Merged

release: promote solvis Strawberry migration to prod#97
chrisbc merged 42 commits into
mainfrom
deploy-test

Conversation

@chrisbc

@chrisbc chrisbc commented Jun 25, 2026

Copy link
Copy Markdown
Member

Promotes the completed Graphene → Strawberry / FastAPI / Mangum migration from deploy-test to prod.

Ready to merge — conflict resolved, re-validated.

Validation

  • Test-stage deploy green (deploy smoke {about} ✓); real kororaa/weka traffic ✓; Mangum warmup-event fix verified live.
  • cli_ab_test -A prod -B test → 9/9 PASS, twice (before and after the main reconciliation below) — test Strawberry is byte-for-byte equal to prod Graphene.
  • Reconciled tree: ruff + mypy clean, SDL byte-identical, 90 passed / 10 skipped.

⚠️→✅ main had diverged (runbook §4.11) — now reconciled

main carried two un-forward-ported commits: fix deploy workflow (convergent) and Chore/upgrade #85 (a parallel poetry→uv on prod). Resolved by merging main into deploy-test (commit 1caad74):

  • stack/source files → the migration (tested superset; pynamodb stays removed); lockfiles regenerated.
  • main's net-new kept: scripts/smoke_test.py, CLAUDE.md edits.
    main is now an ancestor of deploy-test → this promote is a clean, conflict-free merge.

On merge → prod deploy

  1. Merging triggers the prod deploy (deploy-to-aws-uv.yml on main).
  2. Pre-stage the revert: draft revert: <this merge sha> against main; trigger: 5xx spike sustained or any cli_ab_test mismatch. Image-only rollback (no new write shapes).
  3. Watch prod ~30 min (aws logs tail … --follow + CloudWatch error rate / p95).
  4. Then the post-healthy legacy cleanup (see docs/MIGRATION_LOG.md § Cutover plan §5).

voj and others added 23 commits March 24, 2026 10:10
… sibling)

Captures the repo-specific inventory and runbook deltas before code:
- PynamoDB surface is a single wrapped model (BinaryLargeObject)
- repo ships its own cross-stage differential validator (cli_ab_test)
  -> reuse for Phase 5 instead of building drive_live.py
- still on poetry; yarn age gate missing; container entry is the
  serverless-wsgi handler.py workaround (-> Mangum)
- both serverless-s3-local + serverless-dynamodb load at boot (§4.7)

Epic: GNS-Science/nshm-toshi-api#359
chore: migrate Poetry → uv, flake8/black/isort → ruff
- add tools/dump_legacy_sdl.py (stdout->stderr guard; pyvista warns on import)
- commit schema.legacy.graphql (449 lines, 29 types)
- log: poetry2uv verified green (ruff + 62 tests); Phase 0 SDL done

Refs #86
- 11 real queries lifted from the test suite into fixtures/corpus/*.graphql
- test_corpus_replay.py validates each against schema_root (no fixtures needed);
  re-points to the Strawberry schema later to catch field/type drift
- kororaa UI queries to follow (real frontend traffic)

Refs #86
- vendor 3 pure-solvis kororaa Relay ops, transformed from gateway shape
  (SOLVIS_<f> -> <f>, on Query -> on QueryRoot); validated against schema
- adds coverage the tests lacked: locations_by_id + radius_geojson(style:),
  rich rupture node fields (fault_surfaces, magnitude, rate_weighted_mean, ...)
- ComboRuptureMap* skipped (multi-API: also fetch KORORAA_textual_content)
- log the stale-checkout lesson: kororaa was 9mo behind and showed retired
  inversion_solution/analyse_solution usage -> false parity alarm

Refs #86
- dev.yml: drop the pull_request branches:[main,deploy-test] filter so
  stacked migration PRs get CI (runbook Trap #14)
- .yarnrc.yml: now tracked (was untracked); add npmMinimalAgeGate 7d +
  preapproved project scopes (runbook §4.6); yarn install --immutable OK

Refs #86
- add strawberry/fastapi/mangum/pydantic (legacy graphene/flask kept until cutover)
- app.py: FastAPI + GraphQLRouter + Mangum + CORS
- strawberry_schema.py: minimal QueryRoot.about at SDL parity
  (auto_camel_case=False, root type QueryRoot to match legacy + kororaa corpus)
- serverless.yml: image.command -> solvis_graphql_api.app.handler; drop
  serverless-wsgi plugin + custom.wsgi
- Dockerfile: CMD -> app.handler; drop bogus /bin/bash -c entrypoint
- verified: amd64 docker build OK + handler imports in-image; uvicorn {about}
  byte-matches legacy; full legacy suite 77 passed

Refs #86
…r API)

- BinaryLargeObjectModel(pynamodb.Model) -> pydantic BaseModel + thin boto3
  DynamoDB CRUD; public BinaryLargeObject wrapper API unchanged so cli +
  composite_solution.cached are untouched
- object_meta stored as a JSON string (JSONAttribute parity; ints not Decimals)
- migrate()/drop_tables()/tables preserved (create_table wait -> boto3 waiter)
- drop pynamodb dependency; uv re-locked (164 pkgs)
- verified: moto contract 4/4, full suite 77 passed, ruff + mypy clean

Refs #86
…rity

- strawberry_schema.py: full 29-type schema; tools/schema_parity.py gate confirms
  byte-identical vs schema.legacy.graphql
- custom Node interface (id: ID!, not strawberry.relay/GlobalID); QueryRoot root
  type; auto_camel_case=False; relay PageInfo camelCase via name=
- nullability (X|None for nullable-with-default), UNSET vs =null args, and the
  partial 3-key style-arg default all matched
- resolvers reuse the graphene-free compute helpers; light query-root resolvers
  verified vs legacy output
- corpus replay re-pointed at the Strawberry schema (15 pass); add strawberry
  mypy plugin (new code mypy-clean)
- archive-dependent runtime resolvers stubbed -> Phase 3 (need fixtures + tests)

Refs #86
- test_strawberry_parity.py: run each query vs Graphene AND Strawberry, assert
  identical data (in-process analogue of cli_ab_test)
- implement CompositeRuptureDetail computed fields (magnitude/area/length/
  rake_mean/rate_*) via rupture_detail; fix get_parent_fault_names shape
- harness caught 2 bugs the SDL gate can't: relay global-id encoding (id ->
  to_global_id, Model C1) and float opacity defaults (1.0 not 1); switch the
  style-arg default to a mapping (instance default breaks graphql-core coercion)
- SDL parity still byte-identical; suite 85 passed; ruff + mypy(new) clean

Refs #86
- reuse legacy paginated_filtered_ruptures via a dict adapter (_legacy_filter)
  that exposes the dict+attr access it expects; convert the graphene connection
  to the Strawberry RuptureDetailConnection
- fix: filter_set_options default must be a plain mapping, not an input instance
  (an instance default trips strawberry argument coercion) - same class of fix
  as the style-arg default; SDL parity preserved
- test_filter_ruptures_parity: pagination + cursors + node global-id + magnitude
  all byte-match legacy (note: legacy can't serve pageInfo.hasPreviousPage)

Refs #86
- CompositeRuptureSections: delegate every resolver to the legacy graphene
  static resolvers (aggregates/geojson/MFD/colour reused verbatim) via a graphene
  root built from the input; colour-scale normalisation converted to string value
- node(): legacy has no get_node -> null on both schemas (parity-correct stub)
- mypy: None-safe _fso_dict, edges/normalisation narrowing, type:ignore on
  untyped graphene constructors -> new code clean
- 11 differential parity checks (full surface) + suite 88 passed; SDL byte-identical

Refs #86
- container: amd64 build green with migrated deps; 360MB (< 500MB aim, no
  multi-stage); Mangum handler + full schema import in-image
- pip-audit runtime deps: no known vulnerabilities
- plugin chain (s3rver + serverless-dynamodb) loads; no resolutions needed
- drop dead serverless-wsgi npm plugin + stale requirements_* scripts;
  yarn lockfile updated, --immutable clean (§4.6)
- memory stays 2096; CI already on -uv test+deploy workflows

Refs #86
- docs/PHASE5_CUTOVER.md: in-place replacement, rollback (image-only, no new
  write shapes), differential validation via the built-in cli_ab_test
  (prod-vs-test, covers the full migrated surface), promote + 30-min watch,
  post-healthy legacy-cleanup scope
- reuse cli_ab_test (no drive_live.py needed); in-process parity already locked
  by test_strawberry_parity.py
- actual deploy/promote needs AWS creds + prod go-ahead

Refs #86
…ationEnum type stub

CI gates on mypy. poetry2uv added follow_untyped_imports=true, making mypy
type-check into the legacy graphene + solvis code (29 errors). Restore the
original lenient posture (ignore_missing_imports only); add a TYPE_CHECKING
stub enum for SetOperationEnum so it stays a valid type without follow.
Whole package mypy-clean (30 files); parity + 11 differential tests + ruff green.
Migrate Graphene → Strawberry (Phases 0–5, combined)
Live kororaa-test smoke surfaced a RuntimeError every 5 min: the warmup plugin
pings with a non-HTTP event Mangum can't infer a handler for. The legacy
serverless-wsgi handler swallowed these. app.handler now short-circuits the
warmup event ({statusCode:200}) before delegating to Mangum.

- tests/test_app_handler.py: warmup guard + delegation
- all real POST /graphql traffic was already 200; this only silences the
  5-minute warmup error in the logs
- full suite 90 passed; ruff + mypy clean
…armup fix

- fold the cutover plan into MIGRATION_LOG.md (## Cutover plan section); delete
  the standalone docs/PHASE5_CUTOVER.md
- add 2026-06-26 entry: warmup-event fix verified live (clean warmup ping, no
  RuntimeError) + cli_ab_test prod-vs-test 9/9 PASS (cutover gate green) + weka/
  kororaa animation clean
- refresh front-matter status + Phase 5 checklist (test done + validated)
docs: consolidate cutover plan into MIGRATION_LOG + log A/B 9/9
…§4.11)

# Conflicts:
#	.yarnrc.yml
#	DEPLOYMENT.md
#	README.md
#	package.json
#	pyproject.toml
#	serverless.yml
#	setup.cfg
#	solvis_graphql_api/data_store/model.py
#	solvis_graphql_api/solution_schema.py
#	uv.lock
#	yarn.lock
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.07692% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.54%. Comparing base (cdd9dfe) to head (c8d6b51).

Files with missing lines Patch % Lines
solvis_graphql_api/data_store/model.py 84.90% 5 Missing and 3 partials ⚠️
solvis_graphql_api/schema.py 99.25% 0 Missing and 3 partials ⚠️
solvis_graphql_api/geojson_style_util.py 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #97      +/-   ##
==========================================
+ Coverage   95.73%   96.54%   +0.80%     
==========================================
  Files          19       12       -7     
  Lines        1126     1099      -27     
  Branches       53       63      +10     
==========================================
- Hits         1078     1061      -17     
+ Misses         34       25       -9     
+ Partials       14       13       -1     
Flag Coverage Δ
unittests 96.54% <98.07%> (+0.80%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

chrisbc added 6 commits June 26, 2026 10:50
The Phase 1 removal (for stacked-PR CI, Trap #14) was the wrong call here — the
branch rule is a deliberate team choice. The stack is collapsed/merged now, so
restore it. Log: notes the §4.11 forward-port + this correction.
First step of the post-cutover legacy cleanup. handler.py (serverless-wsgi
container workaround) and solvis_graphql_api.py (Flask app) are dead — app.py's
Mangum handler is the entry. Removed them + the create_app test. Graphene schema
+ the strawberry delegation stay until the resolver de-graphene (checklist in PR).
Suite still green (86 passed).
color_scale/compute.py: graphene-free compute (get_normaliser, log_intervals,
get_colour_values, compute_colour_scale -> ColourScaleResult dataclass).
strawberry QueryRoot.color_scale now uses it instead of the graphene
get_colour_scale. Differential parity green (vs live graphene).
Build the LocationDetail connection directly from location_by_id +
graphql_relay.offset_to_cursor (matches graphene's array cursors) instead of the
graphene get_location_detail_list. Differential parity green (incl. Node id +
radius_geojson).
Replace the legacy paginated_filtered_ruptures + _to_strawberry_rupture_connection
path with graphene-free helpers built directly in strawberry_schema:
- _ATTRIBUTE_COLUMN_MAP (was CompositeRuptureDetail.ATTRIBUTE_COLUMN_MAP)
- _auto_sorted (port of composite_solution.schema.auto_sorted_dataframe)
- _paginated_ruptures (cached.matched_rupture_sections_gdf + graphql_relay cursors)

filter_ruptures resolver now builds RuptureDetailConnection itself. Drops the
_LegacyFilter/_legacy_filter shims and the composite_solution.schema import.

SDL byte-identical; 11/11 differential parity green.
Port the section compute off the legacy graphene CompositeRuptureSections root.
The strawberry type now carries the filter input directly (filter_input Private)
and resolves graphene-free via new module helpers:
- _fault_section_aggregates / _matched_rupture_sections (cached.* wrappers)
- _mfd_histogram_rows (build_mfd port)
- _fault_surfaces_geojson / _fault_traces_geojson (get_colour_values from compute)
- section color_scale via compute_colour_scale with participation-rate fallback

Drops the _GrapheneSections/_GrapheneFilterArgs imports and _graphene_sections_root
shim; _to_strawberry_color_scale no longer needs the legacy .color_map branch.

Adds test_filter_rupture_sections_colour_parity covering the colour paths the plain
sections query skips (section color_scale fallback, fault_surfaces/fault_traces +
color_scale). SDL byte-identical; 12/12 differential parity green.
chrisbc added 10 commits June 26, 2026 11:50
…free

The strawberry schema imported two pure (pandas/dict) helpers through graphene-
importing modules. Move them to graphene-free homes so the schema names no graphene
module:
- apply_geojson_style -> new geojson_style_util.py (geojson_style.py re-exports it)
- rupture_detail -> cached.py (composite_rupture_detail.py re-exports it)

strawberry_schema now imports both from the graphene-free modules. The legacy graphene
modules keep working via re-export shims until they are deleted at cutover (the package
__init__ still eager-loads them, which is removed then).

SDL byte-identical; 12/12 differential parity green.
…om tests

The 14 legacy graphene-Client behavioural tests now drive the Strawberry schema via a
graphene.test.Client drop-in (tests/_strawberry_client.py). This keeps their broad coverage
of the migrated resolvers — sorting, filter-set logic, pagination, geojson, node ids — which
the parity corpus alone does not reach. 83 pass, 10 skipped.

test_strawberry_parity converted from an in-process differential (needed the graphene oracle)
to a Strawberry-only snapshot guard; golden outputs captured under tests/__snapshots__ from
the proven-identical schema (regenerate with SNAPSHOT_UPDATE=1).

Fixes one resolver bug the broader suite caught: filter_ruptures passed strawberry.UNSET
through as the sortby 'ascending' value (pandas rejected it); now UNSET fields are omitted so
the legacy '.get("ascending", True)' default applies — matching graphene.

No test imports graphene now. SDL byte-identical; ruff clean.
…e-free

Removes the graphene schema entirely now that the Strawberry schema, the test suite, and the
SDL gate no longer depend on it:

deleted: schema.py (root), location_schema.py, solution_schema.py (dead), geojson_style.py,
color_scale/color_scale.py, composite_solution/{composite_rupture_detail,
composite_rupture_sections,composite_solution,filter_set_logic_options,filtered_ruptures_args,
schema}.py, tools/dump_legacy_sdl.py (the legacy SDL baseline is frozen in schema.legacy.graphql).

Two graphene-free helpers the deleted modules held were relocated to stable homes first:
- auto_sorted_dataframe -> new composite_solution/ruptures.py (also used by the sort unit test)
- _solvis_join -> cached.py (its only consumer)
color compute already lived in color_scale/compute.py.

Gutted composite_solution/__init__.py and color_scale/__init__.py (they eager-loaded the
deleted graphene types). Repointed the two compute unit tests (sort, colour) at the new homes
and dropped the stale get_composite_solution monkeypatches in conftest (everything resolves
through cached now).

No 'import graphene' remains under solvis_graphql_api/. SDL byte-identical; 83 pass, 10 skipped.
With the legacy graphene root schema.py deleted, the canonical name is free. Rename the
Strawberry module to solvis_graphql_api/schema.py and repoint importers (app.py, the SDL
parity gate, _strawberry_client shim, the snapshot + corpus-replay tests). Module path only —
local 'as strawberry_schema' aliases kept. Refreshed the module docstring to point at the
graphene-free compute homes (cached, ruptures, color_scale.compute, geojson_style_util).

app + handler import OK; SDL byte-identical; 83 pass, 10 skipped; ruff clean.
Remove graphene, graphql-server, flask, flask-cors and serverless-wsgi from project
dependencies now that no source imports them. Add graphql-relay as an explicit dep — the
schema's relay global-id/cursor encoding used it, but it was only present transitively via
graphene. Relock: graphene 3.4.3, flask, flask-cors, graphql-server, serverless-wsgi (and
blinker/itsdangerous) all removed from the resolved env; graphql-relay 3.2.0 + graphql-core
3.2.11 (via strawberry) remain.

Also narrows the section color_scale normalization default with explicit is-checks (mypy
couldn't narrow 'not in (None, UNSET)').

Verified with graphene physically uninstalled: app imports, 83 pass / 10 skipped, SDL
byte-identical, mypy + ruff clean.
Add the 2026-06-26 cleanup entry (chunks 1-9: graphene removed from source, tests and deps;
test suites repointed at Strawberry; differential parity → snapshot guard) and mark the §5
post-healthy cleanup checklist item.
… (CI cross-platform)

The snapshot conversion (chunk 6) made three parity tests platform-fragile: byte-exact golden
files captured on macOS didn't match ubuntu CI, because shapely buffer coords and matplotlib
hex colours differ in their low float digits. The original differential test was immune (both
schemas ran in one env, so the difference cancelled) — snapshots lost that property.

Keep byte-exact snapshots for the 9 deterministic queries (rounded scalars), and assert the 3
geometry/geojson queries structurally instead: locations_by_id checks the bug-prone global-id
encoding decodes to ('LocationDetail', id) + a non-empty buffer; sections checks counts/extents/
mfd shape + that styling/colour reached every feature. cli_ab_test remains the authoritative
same-data byte-for-byte geojson parity gate (already 9/9).

83 pass / 10 skipped locally; ruff clean.
…apshot)

The locations snapshot re-stored the entire nzshm_common LOCATIONS constant — a ~2MB,
118k-line fixture of library data. Assert structurally instead (count == len(LOCATIONS) +
a known entry). 12 parity tests pass.
chore: remove legacy Graphene/Flask (post-cutover cleanup) [WIP]
The cleanup churned a lot of source (new graphene-free compute + ported resolvers), leaving
codecov patch/project red on PR #97. Cover the new lines directly:

- tests/test_compute.py — color_scale/compute.py: get_normaliser (log/lin/error),
  log_intervals (narrow/wide/same-decade/slim+ensure_max), compute_colour_scale (log/lin/
  unknown-normalisation error), get_colour_values (incl. the None->"x000000" sentinel).
- tests/test_schema_helpers.py — _style_dict(None), _fso_dict(falsy), and the null-sortby-item
  skip in filter_ruptures.

compute.py/schema.py/ruptures.py/geojson_style_util.py now 100%. One genuinely unreachable
legacy branch in log_intervals.slim (caller passes the min as 'max', so largest < it never
holds) is marked '# pragma: no cover' with a note.

96 pass / 10 skipped; ruff + mypy clean.
chrisbc added 2 commits June 26, 2026 13:42
test: close the de-graphene coverage gap
tools/schema_parity.py (the Graphene→Strawberry SDL byte-parity gate) ran as a standalone
script, so it sat at 0% coverage and dragged the migration's codecov patch/project just under
the auto threshold on promote PR #97. Wrap it in tests/test_sdl_parity.py — happy path
(diff()=='' , main()==0) and a drift case (different baseline → non-empty diff, main()==1).

Bonus: the SDL parity gate is now a permanent CI test, not a manual invocation.
schema_parity.py 0% → 100%; package coverage 96% → 98%.
test: SDL parity gate as a CI test (close remaining codecov gap)
@chrisbc chrisbc requested review from chrisdicaprio and voj and removed request for voj June 26, 2026 02:38

@voj voj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@chrisbc chrisbc merged commit 5ed8586 into main Jun 26, 2026
5 checks passed
chrisbc added a commit that referenced this pull request Jun 26, 2026
Record the deploy-test→main promote (#97, 5ed8586), prod deploy success, the pre-staged+closed
rollback #101, the 30-min watch (6/6, 0 failures), the #99/#100 coverage epilogue, and the
non-regression KeyError note (empty fault_system in unchanged shared compute).
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.

2 participants