Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/MIGRATION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Source: code exploration 2026-06-24 on `deploy-test` @ `93f6f62`. Version **0.9.
- [x] **Live validation:** real kororaa-test UI traffic (incl. the rupture animation) all `200`; weka green; warmup-event fix verified live
- [x] **`cli_ab_test -A prod -B test` → 9/9 PASS** — the cutover gate (prod legacy vs test Strawberry, byte-for-byte)
- [ ] Promote `deploy-test → main` (prod) + pre-staged revert PR + ~30-min watch **← needs prod go-ahead**
- [ ] Post-healthy cleanup (delete graphene `schema.py`/Flask/`handler.py`, rename `strawberry_schema.py` → `schema.py`, drop legacy deps); file runbook-feedback PR
- [x] Legacy graphene cleanup drafted on `chore/remove-legacy-graphene` (PR #98 → `deploy-test`) — graphene removed from source, tests and deps; see the [2026-06-26 cleanup entry](#2026-06-26--legacy-graphene-removed-pr-98)

---

Expand Down Expand Up @@ -245,6 +245,28 @@ Source: code exploration 2026-06-24 on `deploy-test` @ `93f6f62`. Version **0.9.
- **Reverted the Phase 1 `dev.yml` `branches:` filter removal — the filter is a deliberate team choice.** Trap #14 (stacked PRs get no CI under the filter) is real, but the cost was only relevant while the migration ran as a stack; now it's collapsed/merged, the team's `pull_request: branches: [main, deploy-test]` rule is restored. *(Correction to the Phase 1 decision; the runbook's "remove the filter" advice should be applied knowingly, not reflexively.)*
- **Next:** merge promote #97 → prod (pre-stage the revert first; ~30-min watch).

### 2026-06-26 — legacy graphene removed (PR #98)
Drafted the §5 post-healthy cleanup on `chore/remove-legacy-graphene` (→ `deploy-test`), as
verified chunks each gated on **SDL byte-identical + the test suite + ruff/mypy**:
1. colour-scale compute extracted graphene-free (`color_scale/compute.py`); 2. `locations_by_id`;
3. `filter_ruptures` pagination (`composite_solution/ruptures.py::auto_sorted_dataframe`);
4. `CompositeRuptureSections` de-delegated (was reusing the graphene root) — aggregates/MFD/
geojson/colour ported onto `cached`/`compute`/`get_colour_values`; 5. `rupture_detail` +
`apply_geojson_style` relocated to graphene-free homes (`cached.py`, `geojson_style_util.py`).
- **Tests**: the 14 graphene-`Client` behavioural suites now run against Strawberry via a
`graphene.test.Client` drop-in (`tests/_strawberry_client.py`) — kept, not deleted, so they
still cover the migrated resolvers (this caught a real bug: `filter_ruptures` passed
`strawberry.UNSET` as the sortby `ascending`, which pandas rejected). The differential parity
test became a Strawberry-only **snapshot** guard (`tests/__snapshots__/`, `SNAPSHOT_UPDATE=1`).
- **Deleted** ~1740 lines: graphene root `schema.py`, `location_schema`, `solution_schema`,
`geojson_style`, `color_scale/color_scale`, and the `composite_solution` graphene modules +
`tools/dump_legacy_sdl.py`. **Renamed** `strawberry_schema.py` → `schema.py`. **Dropped deps**
graphene/graphql-server/flask/flask-cors/serverless-wsgi; added explicit `graphql-relay` (was
transitive via graphene). No `import graphene` remains; verified with graphene uninstalled
(83 pass / 10 skipped, SDL byte-identical, mypy + ruff clean).
- **Next:** CI green on #98, then merge to `deploy-test` (auto-deploys to test) + re-run
`cli_ab_test` 9/9 before it rides along in the #97 prod promote.

<!-- Append new dated entries above this line as the migration proceeds. -->

---
Expand Down
8 changes: 2 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ dependencies = [
"fastapi>=0.115",
"mangum>=0.18",
"pydantic>=2.0",
# --- legacy Graphene / Flask stack (removed at cutover) ---
"flask>=3.0.3",
"flask-cors>=6.0",
"graphene>=3.3",
"graphql-server==3.0.0b7",
"serverless-wsgi>=3.0",
# relay global-id / cursor encoding used by the schema (was transitive via graphene)
"graphql-relay>=3.2",
# --- data + compute (unchanged) ---
"matplotlib>=3.10.7,<4.0.0",
"nzshm-common>=0.9.0,<1.0",
Expand Down
2 changes: 1 addition & 1 deletion solvis_graphql_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from mangum import Mangum
from strawberry.fastapi import GraphQLRouter

from solvis_graphql_api.strawberry_schema import schema
from solvis_graphql_api.schema import schema

LOGGING_CFG = os.getenv("LOGGING_CFG", "solvis_graphql_api/logging_aws.yaml")
logger = logging.getLogger(__name__)
Expand Down
14 changes: 6 additions & 8 deletions solvis_graphql_api/color_scale/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from .color_scale import (
ColorScale,
ColorScaleArgs,
ColorScaleArgsInput,
ColourScaleNormaliseEnum,
get_colour_scale,
get_colour_values,
)
"""Colour-scale compute (graphene-free).

The legacy graphene ``color_scale`` types/resolvers were removed at the Strawberry cutover.
Import the graphene-free maths from the ``compute`` submodule directly
(``compute_colour_scale``, ``get_colour_values``, ``log_intervals``).
"""
186 changes: 0 additions & 186 deletions solvis_graphql_api/color_scale/color_scale.py

This file was deleted.

119 changes: 119 additions & 0 deletions solvis_graphql_api/color_scale/compute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Graphene-free colour-scale compute.

The matplotlib maths extracted out of the legacy graphene `color_scale.py` so the Strawberry
schema can build colour scales without importing graphene. The graphene module imports these
during the transition; it (and its graphene types) are deleted at cutover.
"""

import logging
import math
from collections.abc import Iterable
from dataclasses import dataclass
from functools import lru_cache

import matplotlib as mpl

log = logging.getLogger(__name__)


@dataclass
class ColourScaleResult:
name: str
min_value: float
max_value: float
normalisation: str # "log" | "lin"
levels: list[float]
hexrgbs: list[str]


@lru_cache
def get_normaliser(color_scale_vmax: float, color_scale_vmin: float, color_scale_normalise: str):
if color_scale_normalise == "log":
return mpl.colors.LogNorm(vmin=color_scale_vmin, vmax=color_scale_vmax)
if color_scale_normalise == "lin":
color_scale_vmin = color_scale_vmin or 0
return mpl.colors.Normalize(vmin=color_scale_vmin, vmax=color_scale_vmax)
raise RuntimeError(f"unknown normalisation option: {color_scale_normalise} ")


@lru_cache
def log_intervals(vmin, vmax):
"""get a manageable set of sensible levels between upper and lower exponential bounds"""
min_exponent = int(math.floor(math.log10(abs(vmin))))
max_exponent = int(math.floor(math.log10(abs(vmax))))
if min_exponent == max_exponent:
max_exponent += 1

intervals = [math.pow(10, power) for power in range(min_exponent, max_exponent + 1)]
max_val = intervals[-1]
MIN_LEN = 6
MAX_LEN = 8

def interpolate(intervals):
new_intervals = intervals.copy()
for sub_interval in [0.5, 0.2, 0.1]:
for interval in intervals:
new_interval = interval * sub_interval
if intervals[0] < new_interval < intervals[-1]:
new_intervals.append(round(new_interval, abs(min_exponent)))
if len(new_intervals) >= MIN_LEN:
return new_intervals
return new_intervals

def slim(new_intervals, max):
while len(new_intervals) > MAX_LEN:
new_intervals = new_intervals[::2]
new_intervals = sorted(new_intervals)
if new_intervals[-1] < max:
new_intervals.append(max)
return new_intervals

def ensure_max(intervals, max_value):
if max_value not in intervals:
intervals.append(max_value)
return intervals

if len(intervals) > MAX_LEN:
intervals = ensure_max(slim(intervals, intervals[0]), max_val)
if len(intervals) < MIN_LEN:
intervals = ensure_max(interpolate(intervals), max_val)
return sorted(intervals)


@lru_cache
def compute_colour_scale(color_scale: str, color_scale_normalise: str, vmax: float, vmin: float) -> ColourScaleResult:
levels: list[float] = []
hexrgbs: list[str] = []
cmap = mpl.colormaps[color_scale]
if color_scale_normalise == "log":
intervals = log_intervals(vmin, vmax)
norm = get_normaliser(max(intervals), min(intervals), color_scale_normalise)
for level in intervals:
levels.append(level)
hexrgbs.append(mpl.colors.to_hex(cmap(norm(level))))
elif color_scale_normalise == "lin":
assert vmax * 2 == int(vmax * 2) # make sure we have a value on a 0.5 interval
norm = get_normaliser(vmax, vmin, color_scale_normalise)
for level in range(int(vmin * 10), int(vmax * 10) + 1):
levels.append(level / 10)
hexrgbs.append(mpl.colors.to_hex(cmap(norm(level / 10))))
else:
raise RuntimeError(f"unknown normalisation option: {color_scale_normalise} ")
return ColourScaleResult(
name=color_scale, min_value=vmin, max_value=vmax, normalisation=color_scale_normalise,
levels=levels, hexrgbs=hexrgbs,
)


@lru_cache
def get_colour_values(
color_scale: str, color_scale_vmax: float, color_scale_vmin: float, color_scale_normalise: str,
values: tuple[float | None],
) -> Iterable[str]:
intervals = log_intervals(color_scale_vmin, color_scale_vmax)
norm = get_normaliser(max(intervals), min(intervals), color_scale_normalise)
cmap = mpl.colormaps[color_scale]
colors = []
for v in values:
colors.append("x000000" if v is None else mpl.colors.to_hex(cmap(norm(v)), keep_alpha=False))
return colors
Loading
Loading