refactor(api): complete fastapi migration and related features
Finish native FastAPI controllers, drop vendored microdot, and add Wi-Fi driver runtime, beat SSE, simulated BPM, sequence playback improvements, bridge ESP-NOW sources, UI updates, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
146
scripts/migrate_controllers_native_fastapi.py
Normal file
146
scripts/migrate_controllers_native_fastapi.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Migrate Microdot controllers to native FastAPI (no compat layer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONTROLLERS = Path(__file__).resolve().parents[1] / "src" / "controllers"
|
||||
|
||||
IMPORT_BLOCK = """from fastapi import APIRouter, Request
|
||||
from http_responses import J, html_response, plain, read_json, send_file
|
||||
from http_session import with_session
|
||||
|
||||
"""
|
||||
|
||||
_JSON_HEADERS = re.compile(
|
||||
r"return json\.dumps\(([\s\S]*?)\),\s*(\d+)\s*,\s*\{\s*"
|
||||
r"['\"]Content-Type['\"]\s*:\s*['\"]application/json['\"]\s*,?\s*\}",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_JSON_PLAIN = re.compile(
|
||||
r"^(\s*)return json\.dumps\((.+)\),\s*(\d+)\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_HTML_LINE = re.compile(
|
||||
r"^(\s*)return ([^,\n]+),\s*(\d+),\s*\{['\"]Content-Type['\"]: ['\"]text/html['\"]\}\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_PLAIN_LINE = re.compile(
|
||||
r"^(\s*)return ([^,\n]+),\s*(\d+),\s*\{['\"]Content-Type['\"]: ['\"]text/plain[^'\"]*['\"]\}\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_PAREN_JSON = re.compile(
|
||||
r"return \(\s*json\.dumps\(([\s\S]*?)\)\s*,\s*(\d+)\s*,\s*"
|
||||
r"\{['\"]Content-Type['\"]: ['\"]application/json['\"]\}\s*,?\s*\)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_PAREN_JSON_NOHDR = re.compile(
|
||||
r"return \(\s*json\.dumps\(([\s\S]*?)\)\s*,\s*(\d+)\s*,?\s*\)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
_PAREN_HTML = re.compile(
|
||||
r"return \(\s*([^,]+?)\s*,\s*(\d+)\s*,\s*"
|
||||
r"\{['\"]Content-Type['\"]: ['\"]text/html['\"]\}\s*,?\s*\)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _insert_imports(text: str) -> str:
|
||||
if "from fastapi import APIRouter" in text:
|
||||
return text
|
||||
fut = re.search(r"^from __future__ import annotations\n", text, re.M)
|
||||
if fut:
|
||||
return text[: fut.end()] + "\n" + IMPORT_BLOCK + text[fut.end() :]
|
||||
doc = re.match(r'("""[\s\S]*?"""\n+|\'\'\'[\s\S]*?\'\'\'\n+)', text)
|
||||
if doc:
|
||||
return text[: doc.end()] + "\n" + IMPORT_BLOCK + text[doc.end() :]
|
||||
return IMPORT_BLOCK + text
|
||||
|
||||
|
||||
def _strip_microdot(text: str) -> str:
|
||||
text = re.sub(r"from microdot import Microdot(?:, send_file)?\n", "", text)
|
||||
text = re.sub(r"from microdot\.session import with_session\n", "", text)
|
||||
text = re.sub(r"from microdot import send_file\n", "", text)
|
||||
text = text.replace("controller = Microdot()", "router = APIRouter()")
|
||||
text = text.replace("@controller.", "@router.")
|
||||
return text
|
||||
|
||||
|
||||
def _convert_paths(text: str) -> str:
|
||||
def fix(m: re.Match) -> str:
|
||||
method, path = m.group(1), m.group(2)
|
||||
if path == "":
|
||||
path = "/"
|
||||
path = re.sub(r"<(\w+)>", r"{\1}", path)
|
||||
return f'@router.{method}("{path}")'
|
||||
|
||||
return re.sub(
|
||||
r"@router\.(get|post|put|delete|patch)\(['\"]([^'\"]*)['\"]\)",
|
||||
fix,
|
||||
text,
|
||||
)
|
||||
|
||||
|
||||
def _convert_request_access(text: str) -> str:
|
||||
text = text.replace("request.json or {}", "await read_json(request)")
|
||||
text = re.sub(r"(?<![.\w])request\.json(?!\w)", "await read_json(request)", text)
|
||||
text = text.replace("request.args.get", "request.query_params.get")
|
||||
return text
|
||||
|
||||
|
||||
def _convert_request_annotations(text: str) -> str:
|
||||
text = re.sub(r"async def (\w+)\(request,", r"async def \1(request: Request,", text)
|
||||
text = re.sub(r"async def (\w+)\(request\)", r"async def \1(request: Request)", text)
|
||||
return text
|
||||
|
||||
|
||||
def _convert_returns(text: str) -> str:
|
||||
text = _PAREN_JSON.sub(r"return J(\1, \2)", text)
|
||||
text = _PAREN_JSON_NOHDR.sub(r"return J(\1, \2)", text)
|
||||
text = _PAREN_HTML.sub(r"return html_response(\1, \2)", text)
|
||||
text = _JSON_HEADERS.sub(r"return J(\1, \2)", text)
|
||||
text = _JSON_PLAIN.sub(r"\1return J(\2, \3)", text)
|
||||
text = _HTML_LINE.sub(r"\1return html_response(\2, \3)", text)
|
||||
text = _PLAIN_LINE.sub(r"\1return plain(\2, \3)", text)
|
||||
text = re.sub(
|
||||
r'^(\s*)return "([^"]+)",\s*(\d+)\s*$',
|
||||
r'\1return plain("\2", \3)',
|
||||
text,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def convert_file(path: Path) -> str:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "Microdot" not in text:
|
||||
return text
|
||||
text = _strip_microdot(text)
|
||||
text = _insert_imports(text)
|
||||
text = _convert_paths(text)
|
||||
text = _convert_request_access(text)
|
||||
text = _convert_request_annotations(text)
|
||||
text = _convert_returns(text)
|
||||
return text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for path in sorted(CONTROLLERS.glob("*.py")):
|
||||
if path.name == "__init__.py":
|
||||
continue
|
||||
path.write_text(convert_file(path), encoding="utf-8")
|
||||
print(path.name)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user