From afa956867374fed62bb4ecf124e2adb444ea7885 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Sun, 26 Jul 2026 20:03:50 -0400 Subject: [PATCH 1/5] fix: backporting a few more things pyrefly setup for circuit drivers and ignore for a code path that wont happen since we would never have an app with no routes --- copier.yml | 10 ++++++++++ .../src/backend_api/fast_api_exception_handlers.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/copier.yml b/copier.yml index 01458c4f1..2682ff8b1 100644 --- a/copier.yml +++ b/copier.yml @@ -391,6 +391,16 @@ _tasks: echo "pyrightconfig.json not found" fi when: "{{ is_circuit_python_driver }}" + - command: | + echo "Checking .config/pyrefly.toml..." + if [ -f .config/pyrefly.toml ]; then + if ! grep -q 'firmware/src' .config/pyrefly.toml; then + sed -i 's#search-path = \["../backend/src", "../backend"\]#search-path = ["../backend/src", "../backend", "../backend/src/backend_api/firmware/src"]#' .config/pyrefly.toml + fi + else + echo ".config/pyrefly.toml not found" + fi + when: "{{ is_circuit_python_driver }}" - command: | echo "Checking for .coveragerc in repo root config..." if [ -f .config/.coveragerc ]; then diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py b/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py index 5d8479532..03b9ec4f9 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py @@ -203,7 +203,9 @@ def custom_openapi(app: FastAPI) -> dict[str, Any]: }, } for schema_name, schema_metadata in generated_schema_metadata.items(): - if schema_name in comps: + if ( + schema_name in comps + ): # pragma: no branch # every app has at least one validating route (even just the health check), so HTTPValidationError/ValidationError are always already in comps by the time this runs for key, value in schema_metadata.items(): _ = comps[schema_name].setdefault(key, value) From bf4d97c53ca0a08d227c49fef2d15d73e72ab889 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Sun, 26 Jul 2026 22:43:59 -0400 Subject: [PATCH 2/5] fix: correct app_def.py.jinja import order and blank lines The .camel_case_model import was placed after the is_circuit_python_driver-gated .common/.driver_routes block, so when that flag is true it rendered out of alphabetical order (ruff-src flagged it as a fixable isort violation in the circuit-backend E2E lint job). Also had an extra blank line before API_DESCRIPTION (ruff-format flagged it). Moved the import above the conditional block so it always sorts correctly regardless of is_circuit_python_driver, and removed the stray blank line. Verified against a real Jinja2 render (trim_blocks/lstrip_blocks/ keep_trailing_newline=True, matching copier's defaults) with both ruff check --select I and ruff format run against the project's actual .config/ruff.toml. --- .../src/backend_api/app_def.py.jinja | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja index b10fb5611..0feafa9bc 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja @@ -16,14 +16,14 @@ from fastapi.staticfiles import StaticFiles from fastapi_offline import FastAPIOffline from pydantic import Field{% endraw %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} from zeroconf.asyncio import AsyncZeroconf{% endraw %}{% endif %}{% raw %} -{% endraw %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} + +from .camel_case_model import CamelCaseModel{% endraw %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} from .common.bridges import router as bridges_router from .common.mdns import SimpleBrowser from .common.mdns import register_driver from .common.mdns import router as mdns_router from .common.rfc_servers_jinja import get_servers_container from .driver_routes import router as driver_router{% endraw %}{% endif %}{% raw %} -from .camel_case_model import CamelCaseModel from .entrypoint.parser import get_version from .fast_api_exception_handlers import register_exception_handlers{% endraw %}{% if backend_uses_graphql %}{% raw %} from .graphql.schema import schema{% endraw %}{% endif %}{% raw %} @@ -75,7 +75,6 @@ async def lifespan(app: FastAPI): logger.info("Shutting down FastAPI application") {% endraw %}{% endif %}{% raw %} - API_DESCRIPTION = {% endraw %}{{ backend_rest_api_description | tojson }}{% raw %} OPENAPI_APP_SPECIFIC_TAGS: list[dict[str, str]] = [ From 79ba29c8f93dce4331c24567eb3b717b80d45bd4 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Sun, 26 Jul 2026 23:01:42 -0400 Subject: [PATCH 3/5] fix: exempt driver-specific vacuum rules and register the driver tag is_circuit_python_driver was already a template variable, so gate the resources-plural and no-request-body exemptions directly in vacuum-ignore.yaml.jinja instead of a post-generation copier _task - matches weight-sensor-driver's own local vacuum-ignore.yaml (both exemptions, same justifications), just applied once at the template level since every circuit-python driver hits the same route shapes. resources-plural: a generated driver only ever manages one physical device connection, so /api/driver/* is intentionally singular, not a REST collection. no-request-body: DELETE /device-connection removes a device by mDNS service name with no resource ID in the path, so the body is unavoidable. Also register the "driver" tag in OPENAPI_TAGS, next to debug/mDNS under the same has_circuit_python_backend_template_been_instantiated gate - circuit-backend's driver_routes.py already tags routes with tags=["driver"], but nothing had ever registered that tag, so vacuum's operation-tag-defined fired on every driver route. --- template/.config/vacuum-ignore.yaml.jinja | 22 ++++++++++++++++++- .../src/backend_api/app_def.py.jinja | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/template/.config/vacuum-ignore.yaml.jinja b/template/.config/vacuum-ignore.yaml.jinja index 408adce98..66af2c07a 100644 --- a/template/.config/vacuum-ignore.yaml.jinja +++ b/template/.config/vacuum-ignore.yaml.jinja @@ -7,7 +7,27 @@ owasp-define-error-validation: - "$.paths['/api/shutdown'].get.responses" -{% endraw %}{% if backend_uses_graphql %}{% raw %}# /api/graphql's 200 response body is arbitrary JSON shaped by the caller's GraphQL query/GraphiQL page; +{% endraw %}{% if is_circuit_python_driver %}{% raw %}# A generated driver only ever manages a single physical device connection at a time, so its +# resource paths are intentionally singular ("driver", not "drivers") rather than REST-collection plural. +resources-plural: + - "$.paths['/api/driver/data-from-serial-port']" + - "$.paths['/api/driver/device-info-from-serial-port']" + - "$.paths['/api/driver/device-connection']" + - "$.paths['/api/driver/send-raw-command']" + - "$.paths['/api/driver/measurement-types']" + - "$.paths['/api/driver/{sensor_id}']" + - "$.paths['/api/driver/{sensor_id}/name']" + - "$.paths['/api/driver/{sensor_id}/reset']" + - "$.paths['/api/driver/{sensor_id}/clear-memory']" + - "$.paths['/api/driver/{sensor_id}/measure/export']" + +# no-request-body flags DELETE with a body as non-RESTful. DELETE /device-connection removes a +# device by mDNS service name with no resource ID in the path, so the service name must travel +# in the body. The rule stays enabled for future endpoints; only this one is exempted. +no-request-body: + - "$.paths['/api/driver/device-connection'].delete" + +{% endraw %}{% endif %}{% raw %}{% endraw %}{% if backend_uses_graphql %}{% raw %}# /api/graphql's 200 response body is arbitrary JSON shaped by the caller's GraphQL query/GraphiQL page; # there is no single representative example to attach without misleadingly implying a fixed response shape. response-body-example: - "$.paths['/api/graphql'].get.responses['200']" diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja index 0feafa9bc..7d5353c04 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja @@ -85,6 +85,7 @@ OPENAPI_TAGS = [ {"name": "system", "description": "Server health and lifecycle operations."},{% endraw %}{% if backend_uses_graphql %}{% raw %} {"name": "graphql", "description": "GraphQL endpoint"},{% endraw %}{% endif %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} {"name": "debug", "description": "Debug and diagnostic operations."}, + {"name": "driver", "description": "Sensor device operations."}, {"name": "mDNS", "description": "mDNS service discovery operations."},{% endraw %}{% endif %}{% raw %} *OPENAPI_APP_SPECIFIC_TAGS, ] From c26519f5062339b115e72655ba5da2785889c345 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Mon, 27 Jul 2026 20:16:09 -0400 Subject: [PATCH 4/5] fix: update description --- .../src/backend_api/app_def.py.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja index 7d5353c04..2901b6bc7 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja @@ -85,7 +85,7 @@ OPENAPI_TAGS = [ {"name": "system", "description": "Server health and lifecycle operations."},{% endraw %}{% if backend_uses_graphql %}{% raw %} {"name": "graphql", "description": "GraphQL endpoint"},{% endraw %}{% endif %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} {"name": "debug", "description": "Debug and diagnostic operations."}, - {"name": "driver", "description": "Sensor device operations."}, + {"name": "driver", "description": "Operations for reading measurements from and controlling a connected device driver."}, {"name": "mDNS", "description": "mDNS service discovery operations."},{% endraw %}{% endif %}{% raw %} *OPENAPI_APP_SPECIFIC_TAGS, ] From d5727807bc322535b5a192e7a6412d7eaf8cf96b Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Mon, 27 Jul 2026 20:25:28 -0400 Subject: [PATCH 5/5] chore: formatting --- .../src/backend_api/app_def.py.jinja | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja index 2901b6bc7..1db88f387 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/app_def.py.jinja @@ -85,7 +85,10 @@ OPENAPI_TAGS = [ {"name": "system", "description": "Server health and lifecycle operations."},{% endraw %}{% if backend_uses_graphql %}{% raw %} {"name": "graphql", "description": "GraphQL endpoint"},{% endraw %}{% endif %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} {"name": "debug", "description": "Debug and diagnostic operations."}, - {"name": "driver", "description": "Operations for reading measurements from and controlling a connected device driver."}, + { + "name": "driver", + "description": "Operations for reading measurements from and controlling a connected device driver.", + }, {"name": "mDNS", "description": "mDNS service discovery operations."},{% endraw %}{% endif %}{% raw %} *OPENAPI_APP_SPECIFIC_TAGS, ]