#!/usr/bin/env python3 """Extract Flask routes from Python handler files.""" import argparse import ast import csv import sys from glob import glob from pathlib import Path def extract_routes(filepath: str) -> list[dict]: """Extract Flask routes from a Python file. Args: filepath: Path to the Python file to parse Returns: List of dicts with route info: path, methods, function_name """ source = Path(filepath).read_text() tree = ast.parse(source, filename=filepath) routes = [] for node in ast.walk(tree): if not isinstance(node, ast.FunctionDef): continue for decorator in node.decorator_list: route_info = _parse_route_decorator(decorator) if route_info: route_info['function_name'] = node.name route_info['line'] = node.lineno routes.append(route_info) return routes def _parse_route_decorator(decorator: ast.expr) -> dict | None: """Parse a decorator to extract route information.""" # Handle @app.route(...) or @blueprint.route(...) if isinstance(decorator, ast.Call): func = decorator.func # Check if it's a .route() call if isinstance(func, ast.Attribute) and func.attr == 'route': return _extract_route_args(decorator) return None def _extract_route_args(call: ast.Call) -> dict: """Extract route path and methods from a route() call.""" result = {'path': None, 'methods': ['GET']} # First positional arg is the path if call.args: arg = call.args[0] if isinstance(arg, ast.Constant): result['path'] = arg.value # Check keyword arguments for methods for keyword in call.keywords: if keyword.arg == 'methods': if isinstance(keyword.value, ast.List): result['methods'] = [elt.value for elt in keyword.value.elts if isinstance(elt, ast.Constant)] return result def collect_files(pattern: str) -> list[Path]: """Collect Python files matching a glob pattern or from a directory.""" path = Path(pattern) if path.is_dir(): return list(path.rglob('*.py')) # Treat as glob pattern matches = glob(pattern, recursive=True) return [Path(m) for m in matches if Path(m).is_file()] def main(): parser = argparse.ArgumentParser(description='Extract Flask routes from Python handler files to CSV.') parser.add_argument( 'pattern', help='Glob pattern (e.g., "*.handler*.py") or directory path', ) parser.add_argument( 'output', help='Output CSV file', ) parser.add_argument( '--include-filename', action='store_true', help='Include source filename in output', ) args = parser.parse_args() files = collect_files(args.pattern) if not files: print(f'No files found matching: {args.pattern}', file=sys.stderr) sys.exit(1) all_routes = [] for filepath in files: try: routes = extract_routes(str(filepath)) for route in routes: for method in route['methods']: row = {'verb': method, 'route': route['path']} if args.include_filename: row['file'] = filepath.name all_routes.append(row) except Exception as e: print(f'Error parsing {filepath}: {e}', file=sys.stderr) if not all_routes: print('No routes found.', file=sys.stderr) sys.exit(1) all_routes.sort(key=lambda r: r['route'] or '') fieldnames = ['verb', 'route'] if args.include_filename: fieldnames.append('file') with open(args.output, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(all_routes) print(f'Wrote {len(all_routes)} routes to {args.output}') if __name__ == '__main__': main()