-
Notifications
You must be signed in to change notification settings - Fork 0
Paform #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Paform #7
Changes from all commits
c18ee4b
c25f7e8
b0b5648
cba636e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| name: assets | ||
| on: | ||
| pull_request: {} | ||
|
|
||
| jobs: | ||
| validate-assets: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node 20 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
| cache-dependency-path: paform/frontend/package-lock.json | ||
|
|
||
| - name: Install frontend deps | ||
| run: | | ||
| cd paform/frontend | ||
| npm ci | ||
| - name: Generate reference GLBs | ||
| run: | | ||
| cd paform | ||
| python3 scripts/generate_reference_glbs.py | ||
| - name: Pack GLBs (LOD0/LOD1) with KTX2 | ||
| run: | | ||
| cd paform | ||
| bash scripts/pack_models.sh | ||
| - name: Validate GLBs (fail on warnings) | ||
| run: | | ||
| cd paform | ||
| python3 scripts/glb_validate.py frontend/public/models/*.glb --fail-on-warning | ||
| - name: Generate manifest.json | ||
| run: | | ||
| cd paform | ||
| python3 scripts/gen_glb_manifest.py > frontend/public/models/manifest.json | ||
| - name: Upload manifest | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: assets-artifacts | ||
| path: paform/frontend/public/models/manifest.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| name: perf-light | ||
|
|
||
| on: | ||
| pull_request: {} | ||
|
|
||
| jobs: | ||
| k6: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| concurrency: | ||
| group: perf-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install k6 | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install -y k6 | ||
| - name: Start stack | ||
| run: | | ||
| cd paform | ||
| docker compose --env-file .env.development -f docker-compose.dev.yml up -d --build | ||
|
Comment on lines
+23
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the failing After checkout the runner is already inside the repository root ( 🤖 Prompt for AI Agents |
||
| - name: Wait for API | ||
| run: | | ||
| cd paform | ||
| for i in {1..60}; do curl -sf http://localhost:8000/healthcheck && break || sleep 2; done | ||
| - name: Wait for Frontend | ||
| run: | | ||
| for i in {1..60}; do curl -sf http://localhost:3000/models/manifest.json && break || sleep 2; done | ||
| - name: Seed backend for perf | ||
| env: | ||
| BASE_URL: http://localhost:8000 | ||
| run: | | ||
| cd paform | ||
| python - <<'PY' | ||
| import json | ||
| import os | ||
| import urllib.error | ||
| import urllib.request | ||
|
|
||
| BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000") | ||
|
|
||
| def post(path: str, payload: dict) -> dict: | ||
| req = urllib.request.Request( | ||
| f"{BASE_URL}{path}", | ||
| data=json.dumps(payload).encode("utf-8"), | ||
| headers={"Content-Type": "application/json"}, | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(req, timeout=10) as resp: | ||
| return json.loads(resp.read().decode("utf-8")) | ||
| except urllib.error.HTTPError as exc: | ||
| detail = exc.read().decode("utf-8", "ignore") | ||
| raise SystemExit(f"Seed request failed ({exc.code}): {detail}") | ||
|
|
||
| material = post( | ||
| "/api/materials/", | ||
| {"name": "Walnut", "texture_url": None, "cost_per_sq_ft": 12.5}, | ||
| ) | ||
| material_id = material.get("id") | ||
| if not material_id: | ||
| raise SystemExit("Material creation failed; missing id") | ||
|
|
||
| post( | ||
| "/api/modules/", | ||
| { | ||
| "name": "Base600", | ||
| "width": 600.0, | ||
| "height": 720.0, | ||
| "depth": 580.0, | ||
| "base_price": 100.0, | ||
| "material_id": material_id, | ||
| }, | ||
| ) | ||
| PY | ||
|
|
||
| - name: Run k6 light profile | ||
| env: | ||
| BASE_URL: http://localhost:8000 | ||
| FRONTEND_BASE_URL: http://localhost:3000 | ||
| run: | | ||
| cd paform | ||
| k6 run --summary-export k6-summary.json tests/perf/k6-quote-cnc.js | ||
| - name: Upload k6 summary | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: k6-summary | ||
| path: paform/k6-summary.json | ||
|
|
||
| - name: Shutdown stack | ||
| if: always() | ||
| run: | | ||
| cd paform | ||
| docker compose --env-file .env.development -f docker-compose.dev.yml down | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from alembic import op | ||
| import sqlalchemy as sa | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = "cfe1d8e4e001_add_sync_events" | ||
| down_revision = "0001" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.create_table( | ||
| "sync_events", | ||
| sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), | ||
| sa.Column("event_id", sa.String(length=255), nullable=True), | ||
| sa.Column("source", sa.String(length=100), nullable=False), | ||
| sa.Column("body_sha256", sa.String(length=64), nullable=False), | ||
| sa.Column( | ||
| "received_at", | ||
| sa.TIMESTAMP(timezone=True), | ||
| server_default=sa.text("CURRENT_TIMESTAMP"), | ||
| nullable=False, | ||
| ), | ||
| sa.UniqueConstraint("source", "event_id", name="uq_sync_events_src_event"), | ||
| sa.UniqueConstraint("source", "body_sha256", name="uq_sync_events_src_body"), | ||
| sa.UniqueConstraint("body_sha256", name="uq_sync_events_body"), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_table("sync_events") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,9 @@ | |
| import logging | ||
| from typing import Dict | ||
|
|
||
| from fastapi import FastAPI | ||
| from fastapi import FastAPI, Response, Request | ||
| from fastapi.exceptions import RequestValidationError | ||
| from fastapi.responses import JSONResponse | ||
| from starlette.middleware.cors import CORSMiddleware | ||
|
|
||
| from api.config import Settings | ||
|
|
@@ -13,6 +15,7 @@ | |
| from api.routes_quote import router as quote_router | ||
| from api.routes_cnc import router as cnc_router | ||
| from api.routes_sync import router as sync_router | ||
| from prometheus_client import CONTENT_TYPE_LATEST, generate_latest | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The API now imports Useful? React with 👍 / 👎. |
||
|
|
||
| # Configure logging | ||
| logging.basicConfig( | ||
|
|
@@ -30,6 +33,15 @@ | |
| version="0.1.0", | ||
| ) | ||
|
|
||
| from api.db import Base, engine | ||
| from api.config import Settings as _Settings | ||
|
|
||
| # Ensure tables exist in SQLite (dev/test); migrations still handle Postgres | ||
| if _Settings().database_url.startswith("sqlite"): | ||
| @app.on_event("startup") | ||
| async def _ensure_sqlite_tables() -> None: | ||
| Base.metadata.create_all(bind=engine) | ||
|
|
||
| # Add CORS middleware | ||
| app.add_middleware( | ||
| CORSMiddleware, | ||
|
|
@@ -48,6 +60,22 @@ | |
| app.include_router(sync_router) | ||
|
|
||
|
|
||
| @app.exception_handler(RequestValidationError) | ||
| async def handle_validation_error(request: Request, exc: RequestValidationError) -> JSONResponse: | ||
| # Unified error envelope for malformed JSON / validation errors | ||
| return JSONResponse( | ||
| status_code=422, | ||
| content={ | ||
| "ok": False, | ||
| "error": { | ||
| "code": "BAD_REQUEST", | ||
| "message": "invalid request", | ||
| "details": exc.errors(), | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| @app.get("/") | ||
| async def root() -> Dict[str, str]: | ||
| """Root endpoint of the API. | ||
|
|
@@ -62,8 +90,7 @@ async def root() -> Dict[str, str]: | |
|
|
||
| @app.get("/healthcheck") | ||
| async def healthcheck() -> Dict[str, str]: | ||
| """Health check endpoint. | ||
| """ | ||
| This endpoint can be used to verify that the API is running and responsive. | ||
| Returns | ||
|
|
@@ -72,3 +99,9 @@ async def healthcheck() -> Dict[str, str]: | |
| A dictionary indicating the health status of the API. | ||
| """ | ||
| return {"status": "healthy"} | ||
|
|
||
|
|
||
| @app.get("/metrics") | ||
| async def metrics() -> Response: | ||
| # Expose Prometheus metrics including default process/python collectors | ||
| return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from __future__ import annotations | ||
| from prometheus_client import Counter | ||
|
|
||
| # Hygraph sync counters | ||
| sync_success_total = Counter( | ||
| "sync_success_total", | ||
| "Successful Hygraph sync operations", | ||
| labelnames=("type",), | ||
| ) | ||
|
|
||
| sync_failure_total = Counter( | ||
| "sync_failure_total", | ||
| "Failed Hygraph sync operations", | ||
| labelnames=("type",), | ||
| ) | ||
|
|
||
| sync_records_upserted_total = Counter( | ||
| "sync_records_upserted_total", | ||
| "Records upserted during Hygraph sync", | ||
| labelnames=("type",), | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider pinning the Python version.
The workflow uses
python3without specifying a version, which relies on the Ubuntu runner's default Python installation (currently 3.12). For reproducibility and to prevent unexpected breakage, consider usingactions/setup-pythonto pin a specific Python version.Add a Python setup step before line 24:
📝 Committable suggestion
🤖 Prompt for AI Agents