Skip to content
Closed
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
3 changes: 2 additions & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
# schedule:
# - cron: '45 6 * * 0' # At 06:45 on Sunday
pull_request:
branches: [main, deploy-test]
# No `branches:` filter — stacked migration PRs target feature branches, not
# main/deploy-test, and a filter would silently skip their CI (runbook Trap #14).

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
Expand Down
11 changes: 11 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
nodeLinker: node-modules

# Supply-chain hardening (runbook §4.6): refuse npm packages younger than 7 days,
# but allow project-internal packages through so dev velocity isn't blocked.
npmMinimalAgeGate: "7d"
npmPreapprovedPackages:
- "nshm-*"
- "nzshm-*"
- "solvis-*"
- "weka-*"
- "toshi-*"
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ RUN dnf install git-core -y &&\
dnf remove git-core -y &&\
dnf clean all

# lambda entry point
CMD ["solvis_graphql_api.solvis_graphql_api.app"]
ENTRYPOINT ["/bin/bash", "-c"]
# lambda entry point — Mangum handler (was the serverless-wsgi handler.py workaround).
# ENTRYPOINT is the base image's Runtime Interface Client (/lambda-entrypoint.sh); CMD is
# the handler. Matches serverless.yml functions.ecr-app.image.{entryPoint,command}.
CMD ["solvis_graphql_api.app.handler"]
23 changes: 16 additions & 7 deletions docs/MIGRATION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ Source: code exploration 2026-06-24 on `deploy-test` @ `93f6f62`. Version **0.9.
- kororaa-sourced (3): transformed gateway → solvis-native (real frontend traffic)
- [ ] Inventory `serverless.yml` (stages, ECR, IAM, warmup) + secrets (`TEST` env + repo keys)

### Phase 1 — Bootstrap (poetry→uv, FastAPI/Mangum, container)
- [ ] `poetry2uv` (dockerbash skill): drop poetry, generate `uv.lock`, ruff config, README/docs
- [ ] Add `strawberry-graphql`/`fastapi`/`mangum`/`pydantic`; keep legacy graphene/flask alongside until cutover
- [ ] New `app.py` (FastAPI + `GraphQLRouter` + Mangum); `StrawberryConfig(auto_camel_case=False)`
- [ ] Replace `handler.py` with Mangum; update `Dockerfile` CMD + serverless `image.command`; drop `serverless-wsgi` plugin + `custom.wsgi`
- [ ] `.yarnrc.yml`: add age gate + preapproved scopes; pin `packageManager`
- [ ] Verify container boots locally (`docker build` + invoke `{ __typename }`)
### Phase 1 — Bootstrap (poetry→uv, FastAPI/Mangum, container)
- [x] `poetry2uv` (external run, PR #87): poetry→uv, ruff, age gate, CI→`-uv`
- [x] Add `strawberry-graphql` 0.316 / `fastapi` 0.137 / `mangum` 0.21 / `pydantic` 2.13; legacy graphene/flask kept alongside until cutover
- [x] New `app.py` (FastAPI + `GraphQLRouter` + Mangum + CORS); `strawberry_schema.py` minimal `QueryRoot.about`, `StrawberryConfig(auto_camel_case=False)`
- [x] Container entry → Mangum: `serverless.yml` `image.command` = `solvis_graphql_api.app.handler`; dropped `serverless-wsgi` plugin + `custom.wsgi`; `Dockerfile` CMD = `app.handler` (removed bogus `/bin/bash -c` entrypoint). `handler.py` left in place (unused; deleted at cutover)
- [x] `.yarnrc.yml`: tracked + age gate + preapproved scopes (`packageManager` already pinned `yarn@4.10.3`)
- [x] Container verified locally: amd64 `docker build` OK; `app.handler` imports inside the image; `uvicorn` TestClient `{ about }` byte-matches legacy. Removed `branches:` filter (Trap #14) so the stack gets CI.

### Phase 2 — Data layer + schema migration
- [ ] `BinaryLargeObjectModel` (PynamoDB) → pydantic + boto3 CRUD; **preserve `BinaryLargeObject` wrapper API**; keep `migrate()`/`drop_tables()`
Expand Down Expand Up @@ -156,4 +156,13 @@ Source: code exploration 2026-06-24 on `deploy-test` @ `93f6f62`. Version **0.9.
- All green: corpus gate 15 passed; full suite **77 passed / 10 skipped**; ruff clean.
- **Next:** Phase 1 — FastAPI/Mangum scaffold + container `handler.py` → Mangum (verify via local `docker build`), `.yarnrc` age gate, drop `serverless-wsgi`.

### 2026-06-24 — Phase 1 bootstrap (FastAPI/Mangum + container swap)
- **Stack added** alongside legacy: `strawberry-graphql` 0.316, `fastapi` 0.137, `mangum` 0.21, `pydantic` 2.13 (`uv lock`/`sync` clean). `app.py` = FastAPI + `GraphQLRouter` + Mangum + CORS; `strawberry_schema.py` = minimal `QueryRoot.about` at SDL parity (`auto_camel_case=False`, `about: String` nullable, description matches).
- **Root type named `QueryRoot`** (not Strawberry's default `Query`) — legacy is `query: QueryRoot` and the vendored kororaa corpus fragments are `on QueryRoot` (Model G4/relay-parity territory).
- **`<pkg>/schema.py` collision** (Model G4): legacy Graphene `schema_root` occupies `solvis_graphql_api.schema`, so the new schema is `strawberry_schema.py`; rename at cutover.
- **Container entry → Mangum.** `serverless.yml` `functions.ecr-app.image.command` = `solvis_graphql_api.app.handler`; dropped `serverless-wsgi` plugin + `custom.wsgi`; `Dockerfile` CMD = `app.handler` (and removed the bogus `ENTRYPOINT ["/bin/bash","-c"]` — the base image's `/lambda-entrypoint.sh` RIC stands). `handler.py` (the serverless-wsgi container workaround) is now unused — deleted at cutover.
- **Verified the container for real** (your steer): `requirements.txt` regenerated from `uv.lock` (`uv export --no-dev`, gitignored); **amd64 `docker build` succeeded**; `docker run … python -c "import solvis_graphql_api.app"` → Mangum handler + FastAPI app import clean inside the image. Local `uvicorn`/TestClient: `{ about }` byte-matches legacy. Full legacy suite still **77 passed / 10 skipped**; ruff clean.
- **CI:** removed the `dev.yml` `branches:` filter so stacked PRs get CI (Trap #14); added `.yarnrc.yml` age gate (§4.6).
- **Next:** Phase 2 — the headline work: `BinaryLargeObjectModel` (PynamoDB) → pydantic + boto3 behind the `BinaryLargeObject` wrapper, then port the ~25 graphene types to Strawberry at SDL parity.

<!-- Append new dated entries above this line as the migration proceeds. -->
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ classifiers=[
'Programming Language :: Python :: 3.12',
]
dependencies = [
# --- new Strawberry / FastAPI / Mangum stack ---
"strawberry-graphql>=0.243",
"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",
# --- data + compute (unchanged) ---
"matplotlib>=3.10.7,<4.0.0",
"nzshm-common>=0.9.0,<1.0",
"nzshm-model>=0.14.0,<1.0",
"pynamodb>=6.0.0",
"pyyaml>=6.0.1",
"serverless-wsgi>=3.0",
"solvis>=1.2.0,<2.0",
]

Expand All @@ -38,13 +45,15 @@ dev = [
"bump2version",
"click",
"deepdiff",
"httpx",
"moto",
"mypy",
"pip-audit",
"pre-commit",
"pytest-cov",
"pytest",
"ruff",
"uvicorn",
"safety",
"sgqlc",
"shapely",
Expand Down
9 changes: 1 addition & 8 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ service: nzshm22-solvis-graphql-api
configValidationMode: error

plugins:
- serverless-wsgi
- serverless-plugin-warmup

package:
Expand Down Expand Up @@ -59,12 +58,6 @@ custom:
s3_role_resource: ${self:custom.s3_bucket_arn}
url_default_ttl: 60 # presigned URL TTL

#serverless-wsgi settings
wsgi:
app: solvis_graphql_api.solvis_graphql_api.app
packRequirements: false
pythonBin: python3.12

#serverless-s3-local settings
s3:
host: localhost
Expand Down Expand Up @@ -159,7 +152,7 @@ functions:
image:
name: app_image_0
command:
- solvis_graphql_api.handler.handler
- solvis_graphql_api.app.handler # Mangum (was serverless-wsgi handler.handler)
entryPoint:
- '/lambda-entrypoint.sh'

Expand Down
45 changes: 45 additions & 0 deletions solvis_graphql_api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""FastAPI + Mangum entry point — migration scaffold.

Replaces the Flask + ``serverless-wsgi`` container entry (``handler.py``). Mangum is
packaging-agnostic, so it runs inside the ECR container without the serverless-wsgi
workaround that ``handler.py`` exists for. Serves the Strawberry schema at ``/graphql``.

The Lambda handler is ``solvis_graphql_api.app.handler`` (wired in ``serverless.yml``
``functions.ecr-app.image.command`` and the ``Dockerfile`` CMD). The legacy Flask app
(``solvis_graphql_api.solvis_graphql_api:app``) stays importable until cutover.
"""

import logging
import logging.config
import os

import yaml
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from mangum import Mangum
from strawberry.fastapi import GraphQLRouter

from solvis_graphql_api.strawberry_schema import schema

LOGGING_CFG = os.getenv("LOGGING_CFG", "solvis_graphql_api/logging_aws.yaml")
logger = logging.getLogger(__name__)


def _configure_logging() -> None:
if os.path.exists(LOGGING_CFG): # pragma: no cover
with open(LOGGING_CFG) as f:
logging.config.dictConfig(yaml.safe_load(f.read()))
else: # pragma: no cover
logging.basicConfig(level=logging.INFO)


_configure_logging()

app = FastAPI()
# mirror the legacy flask_cors CORS(app) default (allow all origins)
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
app.include_router(GraphQLRouter(schema), prefix="/graphql")

handler = Mangum(app)
32 changes: 32 additions & 0 deletions solvis_graphql_api/strawberry_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Strawberry schema — migration scaffold.

Phase 1 stands up a **minimal** Query so the FastAPI/Mangum app boots and serves a real
field at SDL parity; the full type port lands in Phase 2.

Named ``strawberry_schema.py`` (not ``schema.py``) because the legacy Graphene schema
occupies the ``solvis_graphql_api.schema`` module path during the migration — a Phase 1
trap surfaced by the Model pilot. Rename to ``schema.py`` at cutover once the Graphene
schema is removed.
"""

import strawberry
from strawberry.schema.config import StrawberryConfig

import solvis_graphql_api


@strawberry.type
class QueryRoot:
# root type MUST be named QueryRoot — legacy schema is `query: QueryRoot`, and the
# vendored kororaa corpus fragments are `on QueryRoot`
@strawberry.field(description="About this Solvis API ")
def about(self) -> str | None:
# nullable (-> str | None) to match graphene's nullable String (Model T2)
return f"Hello World, I am solvis_graphql_api! Version: {solvis_graphql_api.__version__}"


schema = strawberry.Schema(
query=QueryRoot,
# legacy schema is auto_camelcase=False — required for snake_case field parity (Trap #2)
config=StrawberryConfig(auto_camel_case=False),
)
Loading
Loading