Spaces:
Runtime error
Runtime error
File size: 12,175 Bytes
4b12e15 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# 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() |