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>
147 lines
4.4 KiB
Python
147 lines
4.4 KiB
Python
#!/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())
|