Files
dss/.dss/doc-sync/generators/api_extractor.py
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

235 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
API Extractor
Extract FastAPI route definitions from server.py files.
"""
import ast
import re
from pathlib import Path
from typing import Dict, List, Any, Optional
import logging
from .base_generator import DocGenerator
logger = logging.getLogger(__name__)
class APIExtractor(DocGenerator):
"""
Extract FastAPI endpoints from server.py files.
Extracts:
- Route paths and HTTP methods
- Function names and docstrings
- Route parameters
- Response models
"""
def extract(self, source_path: Path) -> Dict[str, Any]:
"""
Extract API endpoints from FastAPI server file.
Args:
source_path: Path to server.py file
Returns:
Dictionary with extracted endpoint data
"""
logger.info(f"Extracting API endpoints from {source_path}")
with open(source_path, 'r') as f:
source_code = f.read()
tree = ast.parse(source_code)
endpoints = []
app_mounts = []
# Find @app.get, @app.post, etc. decorators
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
endpoint = self._extract_endpoint(node, source_code)
if endpoint:
endpoints.append(endpoint)
# Find app.mount() calls for static files
if isinstance(node, ast.Expr):
mount = self._extract_mount(node)
if mount:
app_mounts.append(mount)
return {
"source_file": str(source_path),
"endpoints": endpoints,
"mounts": app_mounts,
"total_endpoints": len(endpoints),
"total_mounts": len(app_mounts)
}
def _extract_endpoint(
self,
func_node: ast.FunctionDef,
source_code: str
) -> Optional[Dict[str, Any]]:
"""
Extract endpoint information from function with decorator.
Args:
func_node: AST function definition node
source_code: Full source code (for extracting decorator args)
Returns:
Endpoint data or None
"""
for decorator in func_node.decorator_list:
# Check if decorator is app.get, app.post, etc.
if isinstance(decorator, ast.Call):
if isinstance(decorator.func, ast.Attribute):
# app.get("/path")
if decorator.func.attr in ['get', 'post', 'put', 'delete', 'patch']:
method = decorator.func.attr.upper()
path = self._extract_route_path(decorator)
return {
"path": path,
"method": method,
"function": func_node.name,
"docstring": ast.get_docstring(func_node),
"parameters": self._extract_parameters(func_node),
"line_number": func_node.lineno
}
return None
def _extract_route_path(self, decorator: ast.Call) -> str:
"""
Extract route path from decorator arguments.
Args:
decorator: AST Call node for decorator
Returns:
Route path string
"""
if decorator.args:
first_arg = decorator.args[0]
if isinstance(first_arg, ast.Constant):
return first_arg.value
elif isinstance(first_arg, ast.Str): # Python 3.7 compatibility
return first_arg.s
return "/"
def _extract_parameters(self, func_node: ast.FunctionDef) -> List[Dict[str, str]]:
"""
Extract function parameters.
Args:
func_node: AST function definition node
Returns:
List of parameter dictionaries
"""
params = []
for arg in func_node.args.args:
param = {"name": arg.arg}
# Extract type annotation if present
if arg.annotation:
param["type"] = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else str(arg.annotation)
params.append(param)
return params
def _extract_mount(self, expr_node: ast.Expr) -> Optional[Dict[str, Any]]:
"""
Extract app.mount() call for static files.
Args:
expr_node: AST expression node
Returns:
Mount data or None
"""
if isinstance(expr_node.value, ast.Call):
call = expr_node.value
# Check if it's app.mount()
if isinstance(call.func, ast.Attribute):
if call.func.attr == 'mount' and len(call.args) >= 2:
path_arg = call.args[0]
mount_path = None
if isinstance(path_arg, ast.Constant):
mount_path = path_arg.value
elif isinstance(path_arg, ast.Str):
mount_path = path_arg.s
if mount_path:
return {
"path": mount_path,
"type": "StaticFiles",
"line_number": expr_node.lineno
}
return None
def transform(self, extracted_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Transform extracted API data to .knowledge/dss-architecture.json schema.
Args:
extracted_data: Raw extracted endpoint data
Returns:
Transformed data for knowledge base
"""
# Read existing architecture.json
target_path = self.project_root / ".knowledge" / "dss-architecture.json"
existing = self.read_existing_target(target_path)
if not existing:
# Create new structure
existing = {
"$schema": "dss-knowledge-v1",
"type": "architecture",
"version": "1.0.0",
"last_updated": None,
"modules": []
}
# Ensure modules list exists
if "modules" not in existing:
existing["modules"] = []
# Create REST API module data
rest_api_module = {
"name": "rest_api",
"path": extracted_data["source_file"],
"purpose": "FastAPI server providing REST API and static file serving",
"port": 3456,
"endpoints": extracted_data["endpoints"],
"mounts": extracted_data["mounts"],
"total_endpoints": extracted_data["total_endpoints"]
}
# Update or append REST API module
rest_api_index = next(
(i for i, m in enumerate(existing["modules"]) if m.get("name") == "rest_api"),
None
)
if rest_api_index is not None:
existing["modules"][rest_api_index] = rest_api_module
else:
existing["modules"].append(rest_api_module)
existing["last_updated"] = self.metadata["generated_at"]
return existing