This commit is contained in:
153
src/docgen/detectors/python.py
Normal file
153
src/docgen/detectors/python.py
Normal 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
|
||||
Reference in New Issue
Block a user