nca-toolkit / generate_docs.py
jananathbanuka
fix issues
4b12e15
# 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 <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)
# 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()