# Copyright (c) 2025 Stephen G. Pope # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import sys import json import requests import time from pathlib import Path from datetime import datetime, timedelta def load_config(): """Load configuration from env_shell.json file.""" config_path = Path(__file__).parent / '.env_shell.json' try: with open(config_path, 'r') as f: config = json.load(f) return config.get('ANTHROPIC_API_KEY'), config.get('API_DOC_OUTPUT_DIR') except FileNotFoundError: print(f"Error: Configuration file not found at: {config_path}") sys.exit(1) except json.JSONDecodeError: print(f"Error: Invalid JSON in configuration file: {config_path}") sys.exit(1) except Exception as e: print(f"Error loading configuration: {str(e)}") sys.exit(1) def load_app_context(): """Load the app.py file from the root of the repository.""" try: # Get the root directory by going up from the current file's location root_dir = Path(__file__).parent.parent app_path = Path(__file__).parent / 'app.py' if not app_path.exists(): print("Warning: app.py not found in repository root. Documentation will be generated without API context.") return None with open(app_path, 'r', encoding='utf-8') as f: return f.read() except Exception as e: print(f"Warning: Could not load app.py: {str(e)}") return None # The prompt template to send to Claude CLAUDE_PROMPT = ''' I am providing you with a Python file containing API endpoint definitions. First, here is the main application context from app.py that shows how the API is structured and handled: ** app.py below {app_context} ** app.py DONE Now, please read through the following endpoint code and analyze it in the context of the main application: **endpoint below {file_content} Please generate detailed documentation in Markdown format as follows: 1. Overview: Describe the purpose of the endpoint and how it fits into the overall API structure shown in app.py. 2. Endpoint: Specify the URL path and HTTP method. 3. Request: - Headers: List any required headers, such as the x-api-key headers. - Body Parameters: List the required and optional parameters, including the parameter type and purpose. - Specifically study the validate_payload directive in the routes file to build the documentation - Example Request: Provide a sample request payload and a sample curl command. 4. Response: - Success Response: Reference the endpoint and general response from the app.py to show a full sample response from the api - Error Responses: Include examples of common error status codes, with example JSON responses for each. 5. Error Handling: - Describe common errors, like missing or invalid parameters, and indicate which status codes they produce - Include any specific error handling from the main application context 6. Usage Notes: Any additional notes on using the endpoint effectively. 7. Common Issues: List any common issues a user might encounter. 8. Best Practices: Any recommended best practices for this endpoint. Format the documentation with markdown headings, bullet points, and code blocks. ''' def call_claude_api(message: str, api_key: str) -> str: """Make a direct API call to Claude.""" headers = { "Content-Type": "application/json", "x-api-key": api_key, "anthropic-version": "2023-06-01" } data = { "model": "claude-3-sonnet-20240229", "max_tokens": 4096, "temperature": 0, "messages": [ {"role": "user", "content": message} ] } response = requests.post( "https://api.anthropic.com/v1/messages", headers=headers, json=data ) if response.status_code != 200: raise Exception(f"API call failed with status {response.status_code}: {response.text}") return response.json()["content"][0]["text"] def should_skip_doc_generation(output_file: Path, force: bool = False) -> bool: """ Check if documentation was updated in the last 24 hours. Args: output_file: Path to the output markdown file force: If True, always return False to force generation Returns: bool: True if file was updated in the last 24 hours and not forced, False otherwise """ # If force flag is provided, never skip if force: return False if not output_file.exists(): return False # Get file modification time mod_time = datetime.fromtimestamp(output_file.stat().st_mtime) # Check if file was modified in the last 24 hours time_threshold = datetime.now() - timedelta(hours=24) return mod_time > time_threshold def process_single_file(source_file: Path, output_path: Path, api_key: str, force: bool = False): """ Process a single Python file. Args: source_file: Path to the source Python file output_path: Path to output the markdown file api_key: Anthropic API key force: If True, generate docs even if they were updated recently """ try: # Create output file path if output_path.is_dir(): output_file = output_path / source_file.with_suffix('.md').name else: output_file = output_path # Check if docs were recently updated if should_skip_doc_generation(output_file, force): print(f"Skipping {source_file} - documentation updated within the last 24 hours") return # Read the source file with open(source_file, 'r', encoding='utf-8') as f: file_content = f.read() # Load app.py context app_context = load_app_context() if app_context is None: app_context = "No app.py context available." # Create the full prompt message = CLAUDE_PROMPT.format( app_context=app_context, file_content=file_content ) # Get documentation from Claude markdown_content = call_claude_api(message, api_key) # Create necessary directories output_file.parent.mkdir(parents=True, exist_ok=True) # Write the markdown content (will overwrite if exists) with open(output_file, 'w', encoding='utf-8') as f: f.write(markdown_content) print(f"Generated documentation for: {source_file}") print(f"Output saved to: {output_file}") except Exception as e: print(f"Error processing {source_file}: {str(e)}", file=sys.stderr) def process_directory(source_dir: Path, output_dir: Path, api_key: str, force: bool = False): """Process all Python files in the source directory recursively.""" # Track statistics total_files = 0 processed_files = 0 skipped_files = 0 error_files = 0 start_time = time.time() # Walk through all files in source directory for root, _, files in os.walk(source_dir): for file in files: if file.endswith('.py'): # Get the source file path source_file = Path(root) / file total_files += 1 try: # Calculate relative path to maintain directory structure rel_path = source_file.relative_to(source_dir) output_file = output_dir / rel_path.with_suffix('.md') # Create necessary directories output_file.parent.mkdir(parents=True, exist_ok=True) # Check if we should skip this file if should_skip_doc_generation(output_file, force): print(f"Skipping {source_file} - documentation updated within the last 24 hours") skipped_files += 1 continue # Process the file process_single_file(source_file, output_file, api_key, force) processed_files += 1 except Exception as e: print(f"Error processing {source_file}: {str(e)}", file=sys.stderr) error_files += 1 # Print summary elapsed_time = time.time() - start_time print("\nDocumentation Generation Summary:") print(f"Total Python files found: {total_files}") print(f"Files processed: {processed_files}") print(f"Files skipped (updated in last 24h): {skipped_files}") print(f"Files with errors: {error_files}") print(f"Total time: {elapsed_time:.2f} seconds") def main(): # Check if --force flag is provided force_generation = False source_path_arg = None for arg in sys.argv[1:]: if arg == "--force": force_generation = True else: source_path_arg = arg if not source_path_arg: print("Usage: python script.py [--force]") print("Note: source_path can be either a single .py file or a directory") print("Options:") print(" --force: Generate documentation even if it was updated within 24 hours") print("\nPlease ensure .env_shell.json exists in the same directory with:") print(" ANTHROPIC_API_KEY: Your Anthropic API key") print(" API_DOC_OUTPUT_DIR: Directory where documentation will be saved") sys.exit(1) # Load configuration from JSON file api_key, output_dir = load_config() # Validate configuration if not api_key: print("Error: ANTHROPIC_API_KEY not found in configuration file") sys.exit(1) if not output_dir: print("Error: API_DOC_OUTPUT_DIR not found in configuration file") sys.exit(1) output_path = Path(output_dir) # Get and validate source path source_path = Path(source_path_arg) if not source_path.exists(): print(f"Error: Source path does not exist: {source_path}") sys.exit(1) if source_path.is_file() and not source_path.suffix == '.py': print("Error: Source file must be a Python file (.py)") sys.exit(1) # Create output directory if it doesn't exist output_path.mkdir(parents=True, exist_ok=True) print(f"Starting documentation generation...") print(f"Source: {source_path}") print(f"Output: {output_path}") if force_generation: print(f"Force flag enabled: Will generate all documentation regardless of last update time.\n") else: print(f"Note: Files updated within the last 24 hours will be skipped (use --force to override).\n") # Process based on source type if source_path.is_file(): # For a single file output_file = output_path / source_path.with_suffix('.md').name if output_path.is_dir() else output_path # Check if should skip if should_skip_doc_generation(output_file, force_generation): print(f"Skipping {source_path} - documentation updated within the last 24 hours") else: process_single_file(source_path, output_path, api_key, force_generation) else: process_directory(source_path, output_path, api_key, force_generation) if __name__ == "__main__": main()