#!/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"(? 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())