Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51d2e2d4ce | |||
| a5050e4b49 | |||
| 66090f8f86 | |||
| 3c1ecf8e37 | |||
| c58037004f | |||
| 4d5d7d30a9 | |||
| b700427255 | |||
| 08ffc95e37 | |||
| ec0fa0e6a1 | |||
| 73739ceb72 | |||
| 9b8ef83be9 | |||
| 50a28a409b | |||
| 86134f8434 | |||
| 1852cf5a67 | |||
| a255123334 | |||
| c97fc2869d | |||
| 452294126f | |||
| dbda195bf9 | |||
| a4509da172 | |||
| de36111b6d | |||
| 7a2a30dc5d | |||
| 7f51beaaaa | |||
| 388f32196e | |||
| 95976afedc | |||
| b6369e0ec6 | |||
| 4dd9fc9f5b | |||
| a130a2303a | |||
| 98cd3f2fab | |||
| 81c0b62d39 | |||
| ce020c0359 | |||
| 1f4c616513 | |||
| d42aa5458a |
@@ -19,6 +19,6 @@ jobs:
|
|||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest tests/ -v --tb=short
|
run: pytest tests/integration/ tests/unit/ -v --tb=short
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: ruff check .
|
run: ruff check src/docgen/
|
||||||
|
|||||||
152
.gitignore
vendored
152
.gitignore
vendored
@@ -1,139 +1,21 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
MIT License
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
Copyright (c) 2024 DocGen Team
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
.Python
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
build/
|
in the Software without restriction, including without limitation the rights
|
||||||
develop-eggs/
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
dist/
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
downloads/
|
furnished to do so, subject to the following conditions:
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
The above copyright notice and this permission notice shall be included in all
|
||||||
*.manifest
|
copies or substantial portions of the Software.
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
pip-log.txt
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
pip-delete-this-directory.txt
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
# Unit test / coverage reports
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
htmlcov/
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
.tox/
|
SOFTWARE.
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
Pipfile.lock
|
|
||||||
|
|
||||||
# PEP 582
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Project specific
|
|
||||||
docs/
|
|
||||||
*.html
|
|
||||||
openapi.json
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Entry point for the DocGen CLI."""
|
{"""Entry point for the DocGen CLI."""
|
||||||
|
|
||||||
import sys
|
|
||||||
from docgen.cli import app
|
from docgen.cli import app
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(app())
|
app()
|
||||||
|
|||||||
@@ -1,245 +1,137 @@
|
|||||||
"""CLI interface using Typer."""
|
#!/usr/bin/env python3
|
||||||
|
"""CLI for DocGen - API documentation generator."""
|
||||||
|
|
||||||
import sys
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
||||||
from rich.table import Table
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from docgen import __version__
|
from docgen import __version__
|
||||||
from docgen.models import DocConfig, OutputFormat, Endpoint
|
from docgen.detectors import get_detector
|
||||||
from docgen.detectors import PythonDetector, JavaScriptDetector, GoDetector, RustDetector
|
from docgen.generators import get_generator
|
||||||
from docgen.generators import HTMLGenerator, MarkdownGenerator, OpenAPIGenerator
|
from docgen.models import DocConfig, OutputFormat
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(name="docgen", help="API Documentation Generator")
|
||||||
name="docgen",
|
|
||||||
help="Auto-generate beautiful API documentation from your codebase",
|
|
||||||
add_completion=False,
|
|
||||||
)
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def get_detectors():
|
def validate_framework(value: str) -> Optional[str]:
|
||||||
"""Get all available detectors."""
|
"""Validate and normalize framework name."""
|
||||||
return [PythonDetector(), JavaScriptDetector(), GoDetector(), RustDetector()]
|
if value is None:
|
||||||
|
return None
|
||||||
|
frameworks = ["fastapi", "flask", "django", "express", "fastify", "gin", "chi", "actix"]
|
||||||
|
normalized = value.lower().replace("-", "").replace("_", "")
|
||||||
|
for framework in frameworks:
|
||||||
|
if framework in normalized or normalized in framework:
|
||||||
|
return framework
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def scan_for_endpoints(
|
def detect_framework_auto(file_path: Path) -> Optional[str]:
|
||||||
input_dir: Path,
|
"""Auto-detect framework from file content."""
|
||||||
framework: Optional[str] = None,
|
content = file_path.read_text()
|
||||||
recursive: bool = True,
|
framework_patterns = {
|
||||||
) -> list[Endpoint]:
|
"fastapi": [r"from fastapi", r"import fastapi", r"FastAPI\("],
|
||||||
"""Scan directory for API endpoints."""
|
"flask": [r"from flask", r"import flask", r"Flask\("],
|
||||||
endpoints = []
|
"django": [r"from django", r"import django", r"path\(r"],
|
||||||
detectors = get_detectors()
|
"express": [r"require\([\'"]express[\'"]\)", r"from [\'"]express[\'"]"],
|
||||||
|
"fastify": [r"from [\'"]@fastify", r"require\([\'"]fastify[\'"]\)"],
|
||||||
with Progress(
|
"gin": [r'"github.com/gin-gonic/gin"', r"gin\.Default\("],
|
||||||
SpinnerColumn(),
|
"chi": [r'"github.com/go-chi/chi"', r"chi\.NewRouter\("],
|
||||||
TextColumn("[progress.description]{task.description}"),
|
"actix": [r"use actix_web", r"#[get\(", r"ActixWeb\("],
|
||||||
BarColumn(),
|
}
|
||||||
TaskProgressColumn(),
|
for framework, patterns in framework_patterns.items():
|
||||||
console=console,
|
for pattern in patterns:
|
||||||
) as progress:
|
if re.search(pattern, content):
|
||||||
task = progress.add_task("Scanning for endpoints...", total=None)
|
return framework
|
||||||
|
return None
|
||||||
for detector in detectors:
|
|
||||||
if framework and detector.framework_name != framework:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
found = detector.scan_directory(input_dir, recursive)
|
|
||||||
endpoints.extend(found)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
progress.update(task, completed=True)
|
|
||||||
|
|
||||||
return endpoints
|
|
||||||
|
|
||||||
|
|
||||||
def display_summary(endpoints: list[Endpoint]) -> None:
|
@app.command()
|
||||||
"""Display a summary of detected endpoints."""
|
def main(
|
||||||
if not endpoints:
|
input_dir: Path = typer.Argument(
|
||||||
console.print(Panel("[yellow]No endpoints found.[/yellow]", title="Result"))
|
Path("."), help="Input directory to scan for source files"
|
||||||
return
|
),
|
||||||
|
output_dir: Path = typer.Option(Path("docs"), "-o", "--output", help="Output directory"),
|
||||||
table = Table(title="Detected Endpoints")
|
format: OutputFormat = typer.Option(
|
||||||
table.add_column("Method", style="bold")
|
OutputFormat.HTML, "-f", "--format", help="Output format"
|
||||||
table.add_column("Path", style="cyan")
|
),
|
||||||
table.add_column("Summary", style="dim")
|
framework: Optional[str] = typer.Option(
|
||||||
|
None, "-F", "--framework", help="Framework to use (auto-detected if not specified)"
|
||||||
for endpoint in endpoints[:20]:
|
),
|
||||||
method_color = {
|
title: str = typer.Option("API Documentation", "-t", "--title", help="Documentation title"),
|
||||||
"GET": "blue",
|
description: str = typer.Option("", "-d", "--description", help="Description"),
|
||||||
"POST": "green",
|
theme: str = typer.Option("default", "-T", "--theme", help="Theme (default, dark, minimal)"),
|
||||||
"PUT": "orange3",
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"),
|
||||||
"PATCH": "turquoise2",
|
include_private: bool = typer.Option(
|
||||||
"DELETE": "red",
|
False, "--include-private", help="Include private endpoints"
|
||||||
}.get(endpoint.method.value, "white")
|
),
|
||||||
|
) -> None:
|
||||||
table.add_row(
|
"""Generate API documentation from source code."""
|
||||||
f"[{method_color}]{endpoint.method.value}[/]",
|
console.print(Panel.fit("[bold blue]DocGen[/bold blue] - API Documentation Generator"))
|
||||||
endpoint.path,
|
console.print(f"\n[dim]Version: {__version__}[/dim]")
|
||||||
endpoint.summary or "-",
|
console.print(f"Input: {input_dir}")
|
||||||
)
|
console.print(f"Output: {output_dir}")
|
||||||
|
console.print(f"Format: {format.value}")
|
||||||
if len(endpoints) > 20:
|
|
||||||
table.add_row("...", "...", f"[dim]({len(endpoints) - 20} more)[/]")
|
|
||||||
|
|
||||||
console.print(Panel(table, title=f"Found {len(endpoints)} Endpoints"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("version")
|
|
||||||
def show_version():
|
|
||||||
"""Show the DocGen version."""
|
|
||||||
console.print(f"[bold]DocGen-CLI[/bold] v{__version__}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("detect")
|
|
||||||
def detect_command(
|
|
||||||
input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"),
|
|
||||||
framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Specific framework to use (python, javascript, go, rust)"),
|
|
||||||
recursive: bool = typer.Option(True, "--no-recursive", help="Disable recursive scanning"),
|
|
||||||
):
|
|
||||||
"""Detect API endpoints in source code."""
|
|
||||||
if not input_dir.exists():
|
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
endpoints = scan_for_endpoints(input_dir, framework, recursive)
|
|
||||||
display_summary(endpoints)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("generate")
|
|
||||||
def generate_command(
|
|
||||||
input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"),
|
|
||||||
output_dir: Path = typer.Option(Path("docs"), "--output", "-o", help="Output directory for documentation"),
|
|
||||||
format: OutputFormat = typer.Option(OutputFormat.HTML, "--format", "-F", help="Output format"),
|
|
||||||
theme: str = typer.Option("default", "--theme", "-t", help="Theme for HTML output"),
|
|
||||||
framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Specific framework to use"),
|
|
||||||
title: str = typer.Option("API Documentation", "--title", help="Documentation title"),
|
|
||||||
description: str = typer.Option("", "--description", "-d", help="Documentation description"),
|
|
||||||
version: str = typer.Option("1.0.0", "--version", "-v", help="API version"),
|
|
||||||
verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"),
|
|
||||||
):
|
|
||||||
"""Generate API documentation."""
|
|
||||||
if not input_dir.exists():
|
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
config = DocConfig(
|
config = DocConfig(
|
||||||
input_dir=input_dir,
|
input_dir=input_dir,
|
||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
format=format,
|
format=format,
|
||||||
theme=theme,
|
|
||||||
framework=framework,
|
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
version=version,
|
theme=theme,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
|
include_private=include_private,
|
||||||
)
|
)
|
||||||
|
|
||||||
endpoints = scan_for_endpoints(input_dir, framework)
|
if framework:
|
||||||
|
config.framework = validate_framework(framework)
|
||||||
|
|
||||||
if verbose and endpoints:
|
try:
|
||||||
display_summary(endpoints)
|
detector_class = get_detector(config.framework)
|
||||||
|
if not detector_class:
|
||||||
|
if config.framework:
|
||||||
|
console.print(f"[red]Error: Unknown framework: {config.framework}[/red]")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
console.print("[yellow]Warning: Could not auto-detect framework, using Python[/yellow]")
|
||||||
|
from docgen.detectors import PythonDetector
|
||||||
|
detector_class = PythonDetector
|
||||||
|
|
||||||
if not endpoints:
|
detector = detector_class(config)
|
||||||
console.print("[yellow]No endpoints found. Generating empty documentation.[/yellow]")
|
|
||||||
|
|
||||||
console.print(f"[cyan]Generating {format.value} documentation...[/cyan]")
|
generator_class = get_generator(config.format)
|
||||||
|
if not generator_class:
|
||||||
|
console.print(f"[red]Error: Unknown format: {format}[/red]")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if format == OutputFormat.HTML:
|
generator = generator_class(config)
|
||||||
generator = HTMLGenerator(config)
|
|
||||||
elif format == OutputFormat.MARKDOWN:
|
|
||||||
generator = MarkdownGenerator(config)
|
|
||||||
else:
|
|
||||||
generator = OpenAPIGenerator(config)
|
|
||||||
|
|
||||||
output_path = generator.generate(endpoints, output_dir)
|
endpoints = detector.scan_directory(input_dir)
|
||||||
console.print(f"[green]Documentation generated: {output_path}[/green]")
|
if not endpoints:
|
||||||
|
console.print("[yellow]No endpoints detected.[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"\n[green]Detected {len(endpoints)} endpoints[/green]")
|
||||||
|
|
||||||
@app.command("serve")
|
if verbose:
|
||||||
def serve_command(
|
for ep in endpoints:
|
||||||
input_dir: Path = typer.Argument(Path("docs"), help="Directory containing documentation to serve"),
|
console.print(f" - {ep.method.value:6} {ep.path}")
|
||||||
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
|
|
||||||
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
|
||||||
reload: bool = typer.Option(True, "--no-reload", help="Enable/disable auto-reload on changes"),
|
|
||||||
):
|
|
||||||
"""Start a local development server with live reload."""
|
|
||||||
import uvicorn
|
|
||||||
import asyncio
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
if not input_dir.exists():
|
output_path = generator.generate(endpoints, output_dir)
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
console.print(f"\n[green]Documentation generated: {output_path}[/green]")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Error: {e}[/red]")
|
||||||
|
if verbose:
|
||||||
|
import traceback
|
||||||
|
console.print(traceback.format_exc())
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
class DocsReloadHandler(FileSystemEventHandler):
|
|
||||||
def __init__(self):
|
|
||||||
self._reload_needed = False
|
|
||||||
|
|
||||||
def on_any_event(self, event):
|
|
||||||
self._reload_needed = True
|
|
||||||
|
|
||||||
console.print(Panel(f"[bold]DocGen Server[/bold]\n\nListening on http://{host}:{port}", title="Started"))
|
|
||||||
|
|
||||||
config = uvicorn.Config("docgen.cli:reload_app", host=host, port=port, reload=reload)
|
|
||||||
server = uvicorn.Server(config)
|
|
||||||
server.run()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("init")
|
|
||||||
def init_command(
|
|
||||||
output_dir: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
|
|
||||||
):
|
|
||||||
"""Initialize DocGen configuration file."""
|
|
||||||
config_content = '''# DocGen Configuration
|
|
||||||
# Delete this file if you prefer zero-config operation
|
|
||||||
|
|
||||||
[tool.docgen]
|
|
||||||
input-dir = "."
|
|
||||||
output-dir = "docs"
|
|
||||||
format = "html"
|
|
||||||
theme = "default"
|
|
||||||
title = "API Documentation"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
# Framework detection is automatic, but you can specify:
|
|
||||||
# framework = "python" # python, javascript, go, rust
|
|
||||||
'''
|
|
||||||
|
|
||||||
config_path = output_dir / "docgen.toml"
|
|
||||||
config_path.write_text(config_content)
|
|
||||||
console.print(f"[green]Configuration file created: {config_path}[/green]")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
app()
|
|
||||||
|
|
||||||
|
|
||||||
def reload_app():
|
|
||||||
"""App for uvicorn reload."""
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.routing import Route
|
|
||||||
from starlette.responses import FileResponse
|
|
||||||
|
|
||||||
async def serve_static(request):
|
|
||||||
path = request.path_params.get("path", "index.html")
|
|
||||||
return FileResponse("docs/" + path)
|
|
||||||
|
|
||||||
routes = [
|
|
||||||
Route("/{path:path}", serve_static),
|
|
||||||
]
|
|
||||||
|
|
||||||
return Starlette(routes=routes)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
app()
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"""Detectors package."""
|
{"""Detectors package."""
|
||||||
|
|
||||||
from docgen.detectors.base import BaseDetector
|
from docgen.detectors.base import BaseDetector
|
||||||
from docgen.detectors.python import PythonDetector
|
|
||||||
from docgen.detectors.javascript import JavaScriptDetector
|
|
||||||
from docgen.detectors.go import GoDetector
|
from docgen.detectors.go import GoDetector
|
||||||
|
from docgen.detectors.javascript import JavaScriptDetector
|
||||||
|
from docgen.detectors.python import PythonDetector
|
||||||
from docgen.detectors.rust import RustDetector
|
from docgen.detectors.rust import RustDetector
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseDetector",
|
"BaseDetector",
|
||||||
"PythonDetector",
|
|
||||||
"JavaScriptDetector",
|
|
||||||
"GoDetector",
|
"GoDetector",
|
||||||
|
"JavaScriptDetector",
|
||||||
|
"PythonDetector",
|
||||||
"RustDetector",
|
"RustDetector",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""Base detector abstract class."""
|
{"""Base detector abstract class."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from docgen.models import Endpoint
|
from docgen.models import Endpoint
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import List
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseDetector(ABC):
|
class BaseDetector(ABC):
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""Go endpoint detector for Gin and chi."""
|
"""Go endpoint detector for Gin and chi."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from docgen.models import Endpoint, HTTPMethod
|
|
||||||
from docgen.detectors.base import BaseDetector
|
from docgen.detectors.base import BaseDetector
|
||||||
|
from docgen.models import Endpoint, HTTPMethod
|
||||||
|
|
||||||
|
|
||||||
class GoDetector(BaseDetector):
|
class GoDetector(BaseDetector):
|
||||||
@@ -19,7 +21,7 @@ class GoDetector(BaseDetector):
|
|||||||
)
|
)
|
||||||
|
|
||||||
GIN_HANDLE_PATTERN = re.compile(
|
GIN_HANDLE_PATTERN = re.compile(
|
||||||
r'(?:routers?|router)\s*\.\s*Handle\s*\("(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)"\s*,\s*"([^"]+)"',
|
r'(?:routers?|router)\s*\.\s*Handle\s*\(\s*"(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)"\s*,\s*"([^"]+)"',
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""JavaScript endpoint detector for Express and Fastify."""
|
"""JavaScript endpoint detector for Express and Fastify."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from docgen.models import Endpoint, HTTPMethod
|
|
||||||
from docgen.detectors.base import BaseDetector
|
from docgen.detectors.base import BaseDetector
|
||||||
|
from docgen.models import Endpoint, HTTPMethod
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptDetector(BaseDetector):
|
class JavaScriptDetector(BaseDetector):
|
||||||
@@ -59,7 +61,12 @@ class JavaScriptDetector(BaseDetector):
|
|||||||
|
|
||||||
def _detect_framework(self, content: str) -> Optional[str]:
|
def _detect_framework(self, content: str) -> Optional[str]:
|
||||||
"""Auto-detect the JavaScript framework."""
|
"""Auto-detect the JavaScript framework."""
|
||||||
if "from '@fastify/" in content or "from 'fastify'" in content or "import fastify" in content:
|
fastify_patterns = [
|
||||||
|
"from '@fastify/",
|
||||||
|
"from 'fastify'",
|
||||||
|
"import fastify",
|
||||||
|
]
|
||||||
|
if any(p in content for p in fastify_patterns):
|
||||||
return "fastify"
|
return "fastify"
|
||||||
if 'require("fastify")' in content or "require('fastify')" in content:
|
if 'require("fastify")' in content or "require('fastify')" in content:
|
||||||
return "fastify"
|
return "fastify"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""Python endpoint detector for FastAPI, Flask, and Django."""
|
{"""Python endpoint detector for FastAPI, Flask, and Django."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from docgen.models import Endpoint, HTTPMethod, Parameter, ParameterIn
|
|
||||||
from docgen.detectors.base import BaseDetector
|
from docgen.detectors.base import BaseDetector
|
||||||
|
from docgen.models import Endpoint, HTTPMethod
|
||||||
|
|
||||||
|
|
||||||
class PythonDetector(BaseDetector):
|
class PythonDetector(BaseDetector):
|
||||||
@@ -49,6 +50,7 @@ class PythonDetector(BaseDetector):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initialize the Python detector."""
|
||||||
self.framework: Optional[str] = None
|
self.framework: Optional[str] = None
|
||||||
|
|
||||||
def detect_endpoints(self, file_path: Path) -> list[Endpoint]:
|
def detect_endpoints(self, file_path: Path) -> list[Endpoint]:
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""Rust endpoint detector for Actix-web."""
|
"""Rust endpoint detector for Actix-web."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from docgen.models import Endpoint, HTTPMethod
|
|
||||||
from docgen.detectors.base import BaseDetector
|
from docgen.detectors.base import BaseDetector
|
||||||
|
from docgen.models import Endpoint, HTTPMethod
|
||||||
|
|
||||||
|
|
||||||
class RustDetector(BaseDetector):
|
class RustDetector(BaseDetector):
|
||||||
@@ -14,78 +16,44 @@ class RustDetector(BaseDetector):
|
|||||||
framework_name = "rust"
|
framework_name = "rust"
|
||||||
|
|
||||||
ACTIX_PATTERN = re.compile(
|
ACTIX_PATTERN = re.compile(
|
||||||
r'(?:route|service)\.\(("|\')(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)("|\')\s*\.?\s*(to|handler)',
|
r'(?:route|service)\.\("\'")(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)"\'\)\s*\.?\s*(to|handler)',
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
ACTIX_WEB_PATTERN = re.compile(
|
|
||||||
r'(?:App::new\(\)|scope|service)\.route\s*\(\s*"([^"]+)"\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)',
|
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
ACTIX_MACRO_PATTERN = re.compile(
|
ACTIX_MACRO_PATTERN = re.compile(
|
||||||
r'#\[route\s*\(\s*"([^"]+)"\s*,\s*method\s*=\s*(?:HttpMethod::)?(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)',
|
r'#\[(get|post|put|patch|delete|options|head|patch)\(([^"\)]+)\]',
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"}
|
|
||||||
|
|
||||||
METHOD_MAP = {
|
METHOD_MAP = {
|
||||||
"GET": HTTPMethod.GET,
|
"get": HTTPMethod.GET,
|
||||||
"POST": HTTPMethod.POST,
|
"post": HTTPMethod.POST,
|
||||||
"PUT": HTTPMethod.PUT,
|
"put": HTTPMethod.PUT,
|
||||||
"PATCH": HTTPMethod.PATCH,
|
"patch": HTTPMethod.PATCH,
|
||||||
"DELETE": HTTPMethod.DELETE,
|
"delete": HTTPMethod.DELETE,
|
||||||
"OPTIONS": HTTPMethod.OPTIONS,
|
"options": HTTPMethod.OPTIONS,
|
||||||
"HEAD": HTTPMethod.HEAD,
|
"head": HTTPMethod.HEAD,
|
||||||
}
|
}
|
||||||
|
|
||||||
def detect_endpoints(self, file_path: Path) -> list[Endpoint]:
|
def detect_endpoints(self, file_path: Path) -> list[Endpoint]:
|
||||||
"""Detect endpoints in a Rust file."""
|
"""Detect endpoints in a Rust file."""
|
||||||
content = file_path.read_text()
|
content = file_path.read_text()
|
||||||
endpoints = []
|
endpoints = []
|
||||||
|
endpoints.extend(self._detect_actix_macros(content, file_path))
|
||||||
if self._detect_actix(content):
|
|
||||||
endpoints.extend(self._detect_actix_endpoints(content, file_path))
|
|
||||||
|
|
||||||
return endpoints
|
return endpoints
|
||||||
|
|
||||||
def _detect_actix(self, content: str) -> bool:
|
def _detect_actix_macros(self, content: str, file_path: Path) -> list[Endpoint]:
|
||||||
"""Check if file uses Actix-web."""
|
"""Detect Actix-web macro endpoints."""
|
||||||
indicators = [
|
|
||||||
'actix_web',
|
|
||||||
'actix-web',
|
|
||||||
'actix::',
|
|
||||||
'HttpResponse',
|
|
||||||
'web::Resource',
|
|
||||||
]
|
|
||||||
return any(indicator in content for indicator in indicators)
|
|
||||||
|
|
||||||
def _detect_actix_endpoints(self, content: str, file_path: Path) -> list[Endpoint]:
|
|
||||||
"""Detect Actix-web endpoints."""
|
|
||||||
endpoints = []
|
endpoints = []
|
||||||
|
|
||||||
for match in self.ACTIX_WEB_PATTERN.finditer(content):
|
|
||||||
path, handler = match.groups()
|
|
||||||
endpoint = Endpoint(
|
|
||||||
path=path,
|
|
||||||
method=HTTPMethod.GET,
|
|
||||||
summary=f"GET {path}",
|
|
||||||
description=f"Handler: {handler}",
|
|
||||||
file_path=str(file_path),
|
|
||||||
line_number=content[:match.start()].count("\n") + 1,
|
|
||||||
)
|
|
||||||
endpoints.append(endpoint)
|
|
||||||
|
|
||||||
for match in self.ACTIX_MACRO_PATTERN.finditer(content):
|
for match in self.ACTIX_MACRO_PATTERN.finditer(content):
|
||||||
path, method = match.groups()
|
method_name, path = match.groups()
|
||||||
|
method = self.METHOD_MAP.get(method_name.lower(), HTTPMethod.GET)
|
||||||
endpoint = Endpoint(
|
endpoint = Endpoint(
|
||||||
path=path,
|
path=path,
|
||||||
method=self.METHOD_MAP.get(method, HTTPMethod.GET),
|
method=method,
|
||||||
summary=f"{method} {path}",
|
summary=f"{method_name.upper()} {path}",
|
||||||
file_path=str(file_path),
|
file_path=str(file_path),
|
||||||
line_number=content[:match.start()].count("\n") + 1,
|
line_number=content[:match.start()].count("\n") + 1,
|
||||||
)
|
)
|
||||||
endpoints.append(endpoint)
|
endpoints.append(endpoint)
|
||||||
|
|
||||||
return endpoints
|
return endpoints
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""Base generator class."""
|
"""Base generator class."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from docgen.models import DocConfig, Endpoint
|
from docgen.models import DocConfig, Endpoint
|
||||||
|
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ class BaseGenerator(ABC):
|
|||||||
"""Abstract base class for documentation generators."""
|
"""Abstract base class for documentation generators."""
|
||||||
|
|
||||||
def __init__(self, config: Optional[DocConfig] = None):
|
def __init__(self, config: Optional[DocConfig] = None):
|
||||||
|
"""Initialize the generator with optional configuration."""
|
||||||
self.config = config or DocConfig()
|
self.config = config or DocConfig()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""HTML documentation generator."""
|
{"""HTML documentation generator."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
from docgen.models import DocConfig, Endpoint
|
|
||||||
from docgen.generators import BaseGenerator
|
from docgen.generators import BaseGenerator
|
||||||
|
from docgen.models import DocConfig, Endpoint
|
||||||
|
|
||||||
|
|
||||||
def slugify(text: str) -> str:
|
def slugify(text: str) -> str:
|
||||||
@@ -16,6 +18,7 @@ class HTMLGenerator(BaseGenerator):
|
|||||||
"""Generator for Stripe-like interactive HTML documentation."""
|
"""Generator for Stripe-like interactive HTML documentation."""
|
||||||
|
|
||||||
def __init__(self, config: DocConfig = None):
|
def __init__(self, config: DocConfig = None):
|
||||||
|
"""Initialize the HTML generator."""
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
template_dir = Path(__file__).parent.parent / "templates"
|
template_dir = Path(__file__).parent.parent / "templates"
|
||||||
self.env = Environment(
|
self.env = Environment(
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
"""Markdown documentation generator."""
|
{"""Markdown documentation generator."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from docgen.models import DocConfig, Endpoint
|
|
||||||
from docgen.generators import BaseGenerator
|
from docgen.generators import BaseGenerator
|
||||||
|
from docgen.models import DocConfig, Endpoint
|
||||||
|
|
||||||
|
|
||||||
class MarkdownGenerator(BaseGenerator):
|
class MarkdownGenerator(BaseGenerator):
|
||||||
"""Generator for Markdown documentation."""
|
"""Generator for Markdown documentation."""
|
||||||
|
|
||||||
def __init__(self, config: DocConfig = None):
|
def __init__(self, config: DocConfig = None):
|
||||||
|
"""Initialize the Markdown generator."""
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
template_dir = Path(__file__).parent.parent / "templates"
|
template_dir = Path(__file__).parent.parent / "templates"
|
||||||
self.env = Environment(loader=FileSystemLoader(template_dir))
|
self.env = Environment(loader=FileSystemLoader(template_dir))
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""OpenAPI documentation generator."""
|
"""OpenAPI documentation generator."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Optional
|
||||||
|
|
||||||
|
from docgen.generators.base import BaseGenerator
|
||||||
from docgen.models import DocConfig, Endpoint, HTTPMethod
|
from docgen.models import DocConfig, Endpoint, HTTPMethod
|
||||||
from docgen.generators import BaseGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAPIGenerator(BaseGenerator):
|
class OpenAPIGenerator(BaseGenerator):
|
||||||
@@ -12,84 +14,55 @@ class OpenAPIGenerator(BaseGenerator):
|
|||||||
|
|
||||||
def generate(self, endpoints: list[Endpoint], output_dir: Path) -> Path:
|
def generate(self, endpoints: list[Endpoint], output_dir: Path) -> Path:
|
||||||
"""Generate OpenAPI specification."""
|
"""Generate OpenAPI specification."""
|
||||||
output_dir = self._ensure_output_dir(output_dir)
|
self._ensure_output_dir(output_dir)
|
||||||
|
spec = self._create_openapi_spec(endpoints)
|
||||||
|
output_path = output_dir / "openapi.json"
|
||||||
|
output_path.write_text(json.dumps(spec, indent=2))
|
||||||
|
return output_path
|
||||||
|
|
||||||
spec = self._build_openapi_spec(endpoints)
|
def _create_openapi_spec(self, endpoints: list[Endpoint]) -> dict:
|
||||||
spec_path = output_dir / "openapi.json"
|
"""Create OpenAPI specification dict."""
|
||||||
spec_path.write_text(json.dumps(spec, indent=2))
|
|
||||||
|
|
||||||
return spec_path
|
|
||||||
|
|
||||||
def _build_openapi_spec(self, endpoints: list[Endpoint]) -> dict[str, Any]:
|
|
||||||
"""Build the OpenAPI specification dictionary."""
|
|
||||||
spec = {
|
spec = {
|
||||||
"openapi": "3.0.3",
|
"openapi": "3.0.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": self.config.title,
|
"title": self.config.title,
|
||||||
"description": self.config.description,
|
|
||||||
"version": self.config.version,
|
"version": self.config.version,
|
||||||
|
"description": self.config.description,
|
||||||
},
|
},
|
||||||
"paths": {},
|
"paths": {},
|
||||||
"servers": [{"url": "/"}],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint in endpoints:
|
for endpoint in endpoints:
|
||||||
path_item = self._endpoint_to_path_item(endpoint)
|
path = endpoint.get_full_path()
|
||||||
|
if path not in spec["paths"]:
|
||||||
if endpoint.path not in spec["paths"]:
|
spec["paths"][path] = {}
|
||||||
spec["paths"][endpoint.path] = path_item
|
method_lower = endpoint.method.value.lower()
|
||||||
else:
|
spec["paths"][path][method_lower] = {
|
||||||
existing = spec["paths"][endpoint.path]
|
"summary": endpoint.summary,
|
||||||
for method in ["get", "post", "put", "patch", "delete", "options", "head"]:
|
"description": endpoint.description,
|
||||||
if method in path_item:
|
"operationId": endpoint.operation_id,
|
||||||
existing[method] = path_item[method]
|
"deprecated": endpoint.deprecated,
|
||||||
|
"security": endpoint.security,
|
||||||
return spec
|
"parameters": [
|
||||||
|
{
|
||||||
def _endpoint_to_path_item(self, endpoint: Endpoint) -> dict[str, Any]:
|
"name": p.name,
|
||||||
"""Convert an Endpoint to OpenAPI path item."""
|
"in": p.location.value,
|
||||||
method = endpoint.method.value.lower()
|
"required": p.required,
|
||||||
|
"description": p.description,
|
||||||
operation = {
|
"schema": {"type": p.type, "default": p.default},
|
||||||
"summary": endpoint.summary or f"{endpoint.method.value} {endpoint.path}",
|
}
|
||||||
"description": endpoint.description,
|
for p in endpoint.parameters
|
||||||
"operationId": endpoint.operation_id or f"{method}_{endpoint.path.replace('/', '_').strip('_')}",
|
],
|
||||||
"deprecated": endpoint.deprecated,
|
"requestBody": endpoint.request_body,
|
||||||
"parameters": [self._param_to_openapi(p) for p in endpoint.parameters],
|
"responses": {
|
||||||
"responses": self._build_responses(endpoint.responses),
|
str(r.status_code): {
|
||||||
}
|
"description": r.description,
|
||||||
|
"content": {
|
||||||
if endpoint.security:
|
r.content_type: {
|
||||||
operation["security"] = [{"bearerAuth": []}]
|
"schema": {"type": "object", "properties": r.example}
|
||||||
|
}
|
||||||
return {method: operation}
|
},
|
||||||
|
}
|
||||||
def _param_to_openapi(self, param) -> dict[str, Any]:
|
for r in endpoint.responses
|
||||||
"""Convert Parameter to OpenAPI parameter."""
|
},
|
||||||
return {
|
|
||||||
"name": param.name,
|
|
||||||
"in": param.location.value,
|
|
||||||
"description": param.description,
|
|
||||||
"required": param.required,
|
|
||||||
"schema": {"type": param.type, "default": param.default},
|
|
||||||
"example": param.example,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _build_responses(self, responses) -> dict[str, Any]:
|
|
||||||
"""Build OpenAPI responses object."""
|
|
||||||
if not responses:
|
|
||||||
return {
|
|
||||||
"200": {"description": "Successful response"},
|
|
||||||
"default": {"description": "Error response"},
|
|
||||||
}
|
}
|
||||||
|
return spec
|
||||||
result = {}
|
|
||||||
for resp in responses:
|
|
||||||
resp_obj = {"description": resp.description}
|
|
||||||
if resp.example:
|
|
||||||
resp_obj["content"] = {
|
|
||||||
resp.content_type: {"schema": resp.example}
|
|
||||||
}
|
|
||||||
result[str(resp.status_code)] = resp_obj
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
"""Models package."""
|
{"""Models package."""
|
||||||
|
|
||||||
from docgen.models.endpoint import Endpoint, Parameter, Response, HTTPMethod, ParameterIn
|
|
||||||
from docgen.models.config import DocConfig, OutputFormat, Theme
|
from docgen.models.config import DocConfig, OutputFormat, Theme
|
||||||
|
from docgen.models.endpoint import Endpoint, HTTPMethod, Parameter, ParameterIn, Response
|
||||||
|
|
||||||
__all__ = ["Endpoint", "Parameter", "Response", "HTTPMethod", "ParameterIn", "DocConfig", "OutputFormat", "Theme"]
|
__all__ = [
|
||||||
|
"Endpoint",
|
||||||
|
"Parameter",
|
||||||
|
"Response",
|
||||||
|
"HTTPMethod",
|
||||||
|
"ParameterIn",
|
||||||
|
"DocConfig",
|
||||||
|
"OutputFormat",
|
||||||
|
"Theme",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Static assets for DocGen.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Templates for DocGen.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Integration tests for CLI commands."""
|
{"""Integration tests for CLI commands."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from docgen.cli import app
|
from docgen.cli import app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
"""Integration tests for documentation generation."""
|
{"""Integration tests for documentation generation."""
|
||||||
|
|
||||||
import pytest
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from docgen.generators import HTMLGenerator, MarkdownGenerator, OpenAPIGenerator
|
||||||
|
from docgen.models import DocConfig, Endpoint, HTTPMethod, Parameter, ParameterIn
|
||||||
|
|
||||||
|
|
||||||
class TestHTMLGeneration:
|
class TestHTMLGeneration:
|
||||||
"""Tests for HTML documentation generation."""
|
"""Tests for HTML documentation generation."""
|
||||||
|
|
||||||
def test_html_generator_basic(self):
|
def test_html_generator_basic(self):
|
||||||
"""Test basic HTML generation."""
|
"""Test basic HTML generation."""
|
||||||
from docgen.models import Endpoint, HTTPMethod, DocConfig
|
|
||||||
from docgen.generators import HTMLGenerator
|
|
||||||
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
Endpoint(
|
Endpoint(
|
||||||
path="/api/users",
|
path="/api/users",
|
||||||
@@ -46,9 +45,6 @@ class TestHTMLGeneration:
|
|||||||
|
|
||||||
def test_html_generator_grouped_endpoints(self):
|
def test_html_generator_grouped_endpoints(self):
|
||||||
"""Test endpoint grouping in HTML."""
|
"""Test endpoint grouping in HTML."""
|
||||||
from docgen.models import Endpoint, HTTPMethod, DocConfig
|
|
||||||
from docgen.generators import HTMLGenerator
|
|
||||||
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
Endpoint(
|
Endpoint(
|
||||||
path="/users",
|
path="/users",
|
||||||
@@ -75,9 +71,6 @@ class TestMarkdownGeneration:
|
|||||||
|
|
||||||
def test_markdown_generator_basic(self):
|
def test_markdown_generator_basic(self):
|
||||||
"""Test basic Markdown generation."""
|
"""Test basic Markdown generation."""
|
||||||
from docgen.models import Endpoint, HTTPMethod, DocConfig
|
|
||||||
from docgen.generators import MarkdownGenerator
|
|
||||||
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
Endpoint(
|
Endpoint(
|
||||||
path="/api/users",
|
path="/api/users",
|
||||||
@@ -109,10 +102,6 @@ class TestOpenAPIGeneration:
|
|||||||
|
|
||||||
def test_openapi_generator_basic(self):
|
def test_openapi_generator_basic(self):
|
||||||
"""Test basic OpenAPI generation."""
|
"""Test basic OpenAPI generation."""
|
||||||
from docgen.models import Endpoint, HTTPMethod, DocConfig, Parameter, ParameterIn
|
|
||||||
from docgen.generators import OpenAPIGenerator
|
|
||||||
import json
|
|
||||||
|
|
||||||
param = Parameter(
|
param = Parameter(
|
||||||
name="id",
|
name="id",
|
||||||
type="integer",
|
type="integer",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""Tests for endpoint detectors."""
|
"""Tests for endpoint detectors."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from docgen.models import HTTPMethod
|
from docgen.models import HTTPMethod
|
||||||
|
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ class TestBaseDetector:
|
|||||||
|
|
||||||
def test_can_detect(self):
|
def test_can_detect(self):
|
||||||
"""Test file extension detection."""
|
"""Test file extension detection."""
|
||||||
from docgen.detectors import PythonDetector, JavaScriptDetector
|
from docgen.detectors import JavaScriptDetector, PythonDetector
|
||||||
|
|
||||||
py_detector = PythonDetector()
|
py_detector = PythonDetector()
|
||||||
js_detector = JavaScriptDetector()
|
js_detector = JavaScriptDetector()
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""Tests for docgen models."""
|
"""Tests for docgen models."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from docgen.models import Endpoint, Parameter, Response, HTTPMethod, DocConfig, OutputFormat, ParameterIn
|
|
||||||
|
from docgen.models import (
|
||||||
|
DocConfig,
|
||||||
|
Endpoint,
|
||||||
|
HTTPMethod,
|
||||||
|
OutputFormat,
|
||||||
|
Parameter,
|
||||||
|
ParameterIn,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEndpoint:
|
class TestEndpoint:
|
||||||
|
|||||||
Reference in New Issue
Block a user