| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
|
|
| 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: |
| |
| 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 |
|
|
| |
| 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: |
| return False |
| |
| if not output_file.exists(): |
| return False |
| |
| |
| mod_time = datetime.fromtimestamp(output_file.stat().st_mtime) |
| |
| |
| 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: |
| |
| if output_path.is_dir(): |
| output_file = output_path / source_file.with_suffix('.md').name |
| else: |
| output_file = output_path |
| |
| |
| if should_skip_doc_generation(output_file, force): |
| print(f"Skipping {source_file} - documentation updated within the last 24 hours") |
| return |
| |
| |
| with open(source_file, 'r', encoding='utf-8') as f: |
| file_content = f.read() |
|
|
| |
| app_context = load_app_context() |
| if app_context is None: |
| app_context = "No app.py context available." |
|
|
| |
| message = CLAUDE_PROMPT.format( |
| app_context=app_context, |
| file_content=file_content |
| ) |
|
|
| |
| markdown_content = call_claude_api(message, api_key) |
|
|
| |
| output_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
| |
| 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.""" |
| |
| total_files = 0 |
| processed_files = 0 |
| skipped_files = 0 |
| error_files = 0 |
| |
| start_time = time.time() |
| |
| |
| for root, _, files in os.walk(source_dir): |
| for file in files: |
| if file.endswith('.py'): |
| |
| source_file = Path(root) / file |
| total_files += 1 |
| |
| try: |
| |
| rel_path = source_file.relative_to(source_dir) |
| output_file = output_dir / rel_path.with_suffix('.md') |
|
|
| |
| output_file.parent.mkdir(parents=True, exist_ok=True) |
| |
| |
| 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_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 |
| |
| |
| 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(): |
| |
| 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 <source_path> [--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) |
|
|
| |
| api_key, output_dir = load_config() |
|
|
| |
| 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) |
| |
| |
| 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) |
|
|
| |
| 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") |
| |
| |
| if source_path.is_file(): |
| |
| output_file = output_path / source_path.with_suffix('.md').name if output_path.is_dir() else output_path |
| |
| |
| 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() |