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