Add Python detector
Some checks failed
CI / test (push) Failing after 11s

This commit is contained in:
2026-01-31 17:10:43 +00:00
parent 718d6aad70
commit 1aa2435bcd

View File

@@ -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