From 1aa2435bcd1344b3985f6e52c49cd1c7e67545b7 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sat, 31 Jan 2026 17:10:43 +0000 Subject: [PATCH] Add Python detector --- src/docgen/detectors/python.py | 153 +++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/docgen/detectors/python.py diff --git a/src/docgen/detectors/python.py b/src/docgen/detectors/python.py new file mode 100644 index 0000000..8d85327 --- /dev/null +++ b/src/docgen/detectors/python.py @@ -0,0 +1,153 @@ +"""Python endpoint detector for FastAPI, Flask, and Django.""" + +import re +from pathlib import Path +from typing import Optional +from docgen.models import Endpoint, HTTPMethod, Parameter, ParameterIn +from docgen.detectors.base import BaseDetector + + +class PythonDetector(BaseDetector): + """Detector for Python web frameworks.""" + + extensions = [".py"] + framework_name = "python" + + FASTAPI_PATTERN = re.compile( + r'@([a-zA-Z_][a-zA-Z0-9_]*)\.(get|post|put|patch|delete|options|head)\s*\(\s*["\']([^"\']+)["\']', + re.MULTILINE, + ) + + FASTAPI_DECORATOR_PATTERN = re.compile( + r'@([a-zA-Z_][a-zA-Z0-9_]*)\.(route|api_route)\s*\(\s*["\']([^"\']+)["\']', + re.MULTILINE, + ) + + FLASK_PATTERN = re.compile( + r'@([a-zA-Z_][a-zA-Z0-9_]*)\.route\s*\(\s*["\']([^"\']+)["\']', + re.MULTILINE, + ) + + FLASK_METHOD_PATTERN = re.compile( + r'@([a-zA-Z_][a-zA-Z0-9_]*)\.(get|post|put|patch|delete|options|head)\s*\(\s*["\']([^"\']+)["\']', + re.MULTILINE, + ) + + DJANGO_PATTERN = re.compile( + r'path\s*\(\s*["\']([^"\']+)["\']\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)', + re.MULTILINE, + ) + + METHOD_MAP = { + "get": HTTPMethod.GET, + "post": HTTPMethod.POST, + "put": HTTPMethod.PUT, + "patch": HTTPMethod.PATCH, + "delete": HTTPMethod.DELETE, + "options": HTTPMethod.OPTIONS, + "head": HTTPMethod.HEAD, + } + + def __init__(self): + self.framework: Optional[str] = None + + def detect_endpoints(self, file_path: Path) -> list[Endpoint]: + """Detect endpoints in a Python file.""" + content = file_path.read_text() + endpoints = [] + + framework = self._detect_framework(content) + self.framework = framework + + if framework == "fastapi": + endpoints.extend(self._detect_fastapi(content, file_path)) + elif framework == "flask": + endpoints.extend(self._detect_flask(content, file_path)) + elif framework == "django": + endpoints.extend(self._detect_django(content, file_path)) + + return endpoints + + def _detect_framework(self, content: str) -> Optional[str]: + """Auto-detect the Python framework used.""" + if "from fastapi import" in content or "import FastAPI" in content: + return "fastapi" + if "from flask import" in content or "import Flask" in content: + return "flask" + if "from django.urls import" in content or "import django" in content: + return "django" + return None + + def _detect_fastapi(self, content: str, file_path: Path) -> list[Endpoint]: + """Detect FastAPI endpoints.""" + endpoints = [] + + for match in self.FASTAPI_PATTERN.finditer(content): + app_name, method, path = match.groups() + endpoint = Endpoint( + path=path, + method=self.METHOD_MAP.get(method.lower(), HTTPMethod.GET), + summary=f"{method.upper()} {path}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + for match in self.FASTAPI_DECORATOR_PATTERN.finditer(content): + app_name, _, path = match.groups() + endpoint = Endpoint( + path=path, + method=HTTPMethod.GET, + summary=f"GET {path}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + return endpoints + + def _detect_flask(self, content: str, file_path: Path) -> list[Endpoint]: + """Detect Flask endpoints.""" + endpoints = [] + + for match in self.FLASK_METHOD_PATTERN.finditer(content): + app_name, method, path = match.groups() + endpoint = Endpoint( + path=path, + method=self.METHOD_MAP.get(method.lower(), HTTPMethod.GET), + summary=f"{method.upper()} {path}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + for match in self.FLASK_PATTERN.finditer(content): + app_name, path = match.groups() + endpoint = Endpoint( + path=path, + method=HTTPMethod.GET, + summary=f"GET {path}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + return endpoints + + def _detect_django(self, content: str, file_path: Path) -> list[Endpoint]: + """Detect Django URL patterns.""" + endpoints = [] + + for match in self.DJANGO_PATTERN.finditer(content): + path, view_name = match.groups() + endpoint = Endpoint( + path=path, + method=HTTPMethod.GET, + summary=f"GET {path}", + description=f"Django view: {view_name}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + return endpoints