commiter-cli 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commiter/__init__.py +3 -0
- commiter/adapters/__init__.py +0 -0
- commiter/adapters/base.py +96 -0
- commiter/adapters/django_rest.py +247 -0
- commiter/adapters/express.py +204 -0
- commiter/adapters/fastapi.py +170 -0
- commiter/adapters/flask.py +169 -0
- commiter/adapters/nextjs.py +180 -0
- commiter/adapters/prisma.py +76 -0
- commiter/adapters/raw_sql.py +191 -0
- commiter/adapters/react.py +129 -0
- commiter/adapters/sqlalchemy.py +99 -0
- commiter/adapters/supabase.py +68 -0
- commiter/auth.py +130 -0
- commiter/cli.py +667 -0
- commiter/correlator.py +208 -0
- commiter/extractors/__init__.py +0 -0
- commiter/extractors/api_calls.py +91 -0
- commiter/extractors/api_endpoints.py +354 -0
- commiter/extractors/backend_files.py +33 -0
- commiter/extractors/base.py +40 -0
- commiter/extractors/db_operations.py +69 -0
- commiter/extractors/dependencies.py +219 -0
- commiter/generic_resolver.py +204 -0
- commiter/handler_index.py +97 -0
- commiter/lib.py +63 -0
- commiter/middleware_index.py +350 -0
- commiter/models.py +117 -0
- commiter/parser.py +1283 -0
- commiter/prefix_index.py +211 -0
- commiter/report/__init__.py +0 -0
- commiter/report/ai.py +120 -0
- commiter/report/api_guide.py +217 -0
- commiter/report/architecture.py +930 -0
- commiter/report/console.py +254 -0
- commiter/report/json_output.py +122 -0
- commiter/report/markdown.py +163 -0
- commiter/scanner.py +383 -0
- commiter/type_index.py +304 -0
- commiter/uploader.py +46 -0
- commiter/utils/__init__.py +0 -0
- commiter/utils/env_reader.py +78 -0
- commiter/utils/file_classifier.py +187 -0
- commiter/utils/path_helpers.py +73 -0
- commiter/utils/tsconfig_resolver.py +281 -0
- commiter/wrapper_index.py +288 -0
- commiter_cli-0.3.0.dist-info/METADATA +14 -0
- commiter_cli-0.3.0.dist-info/RECORD +96 -0
- commiter_cli-0.3.0.dist-info/WHEEL +5 -0
- commiter_cli-0.3.0.dist-info/entry_points.txt +2 -0
- commiter_cli-0.3.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/fixtures/arch_backend/app.py +22 -0
- tests/fixtures/arch_backend/middleware/__init__.py +0 -0
- tests/fixtures/arch_backend/middleware/rate_limit.py +4 -0
- tests/fixtures/arch_backend/routes/__init__.py +0 -0
- tests/fixtures/arch_backend/routes/analytics.py +20 -0
- tests/fixtures/arch_backend/routes/auth.py +29 -0
- tests/fixtures/arch_backend/routes/projects.py +60 -0
- tests/fixtures/arch_backend/routes/users.py +55 -0
- tests/fixtures/arch_monorepo/apps/api/app.py +30 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/auth.py +17 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/rate_limit.py +10 -0
- tests/fixtures/arch_monorepo/apps/api/routes/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/routes/auth.py +46 -0
- tests/fixtures/arch_monorepo/apps/api/routes/invites.py +30 -0
- tests/fixtures/arch_monorepo/apps/api/routes/notifications.py +25 -0
- tests/fixtures/arch_monorepo/apps/api/routes/projects.py +80 -0
- tests/fixtures/arch_monorepo/apps/api/routes/tasks.py +91 -0
- tests/fixtures/arch_monorepo/apps/api/routes/users.py +48 -0
- tests/fixtures/arch_monorepo/apps/api/services/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/services/email.py +11 -0
- tests/fixtures/backend_b/app.py +17 -0
- tests/fixtures/fastapi_app/app.py +48 -0
- tests/fixtures/fastapi_crossfile/routes.py +18 -0
- tests/fixtures/fastapi_crossfile/schemas.py +21 -0
- tests/fixtures/flask_app/app.py +33 -0
- tests/fixtures/flask_blueprint/app.py +7 -0
- tests/fixtures/flask_blueprint/routes/items.py +13 -0
- tests/fixtures/flask_blueprint/routes/users.py +20 -0
- tests/fixtures/middleware_test_flask/routes/public.py +8 -0
- tests/fixtures/middleware_test_flask/routes/users.py +26 -0
- tests/fixtures/python_deep_imports/app/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/health.py +11 -0
- tests/fixtures/python_deep_imports/app/api/v1/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/v1/items.py +18 -0
- tests/fixtures/python_deep_imports/app/api/v1/users.py +27 -0
- tests/fixtures/python_deep_imports/app/schemas/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/schemas/item.py +13 -0
- tests/fixtures/python_deep_imports/app/schemas/user.py +15 -0
- tests/fixtures/python_deep_imports/app/shared/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/shared/models.py +7 -0
- tests/fixtures/raw_sql_test/app.py +54 -0
- tests/test_architecture.py +757 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Rich terminal output for repository documentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich.columns import Columns
|
|
10
|
+
from rich.tree import Tree as RichTree
|
|
11
|
+
|
|
12
|
+
from commiter.models import RepoDocumentation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_report(docs: list[RepoDocumentation], console: Console | None = None,
|
|
16
|
+
sections: dict[str, str | None] | None = None) -> None:
|
|
17
|
+
"""Print a formatted summary of all scanned repositories to the terminal."""
|
|
18
|
+
if console is None:
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
for doc in docs:
|
|
22
|
+
_print_repo(doc, console, sections)
|
|
23
|
+
console.print()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _show_section(sections: dict[str, str | None] | None, key: str) -> bool:
|
|
27
|
+
"""Check if a section should be shown based on the sections config."""
|
|
28
|
+
if sections is None:
|
|
29
|
+
return True
|
|
30
|
+
return sections.get(key) is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _print_repo(doc: RepoDocumentation, console: Console,
|
|
34
|
+
sections: dict[str, str | None] | None = None) -> None:
|
|
35
|
+
"""Print a single repository's documentation."""
|
|
36
|
+
# Header
|
|
37
|
+
header = Text(f" {doc.repo_name}", style="bold cyan")
|
|
38
|
+
console.print(Panel(header, title="Repository", border_style="cyan"))
|
|
39
|
+
|
|
40
|
+
# Overview
|
|
41
|
+
overview = Table(show_header=False, box=None, padding=(0, 2))
|
|
42
|
+
overview.add_column("Key", style="bold")
|
|
43
|
+
overview.add_column("Value")
|
|
44
|
+
overview.add_row("Path", doc.repo_path)
|
|
45
|
+
overview.add_row("Languages", ", ".join(doc.languages) if doc.languages else "none detected")
|
|
46
|
+
overview.add_row("Frameworks", ", ".join(doc.frameworks) if doc.frameworks else "none detected")
|
|
47
|
+
overview.add_row("Files scanned", str(len(doc.file_classifications)))
|
|
48
|
+
|
|
49
|
+
# Summary counts
|
|
50
|
+
counts = []
|
|
51
|
+
if doc.endpoints:
|
|
52
|
+
counts.append(f"{len(doc.endpoints)} endpoints")
|
|
53
|
+
if doc.api_calls:
|
|
54
|
+
counts.append(f"{len(doc.api_calls)} API calls")
|
|
55
|
+
if doc.db_operations:
|
|
56
|
+
counts.append(f"{len(doc.db_operations)} DB operations")
|
|
57
|
+
if doc.dependencies:
|
|
58
|
+
counts.append(f"{len(doc.dependencies)} dependencies")
|
|
59
|
+
if counts:
|
|
60
|
+
overview.add_row("Summary", ", ".join(counts))
|
|
61
|
+
|
|
62
|
+
console.print(overview)
|
|
63
|
+
console.print()
|
|
64
|
+
|
|
65
|
+
# Dependencies
|
|
66
|
+
if doc.dependencies and _show_section(sections, "deps"):
|
|
67
|
+
_print_dependencies(doc, console)
|
|
68
|
+
|
|
69
|
+
# API Endpoints
|
|
70
|
+
if doc.endpoints and _show_section(sections, "endpoints"):
|
|
71
|
+
_print_endpoints(doc, console)
|
|
72
|
+
|
|
73
|
+
# API Calls (frontend)
|
|
74
|
+
if doc.api_calls and _show_section(sections, "calls"):
|
|
75
|
+
_print_api_calls(doc, console)
|
|
76
|
+
|
|
77
|
+
# DB Operations
|
|
78
|
+
if doc.db_operations and _show_section(sections, "db"):
|
|
79
|
+
_print_db_operations(doc, console)
|
|
80
|
+
|
|
81
|
+
# Shared DB relationships
|
|
82
|
+
shared_db = [r for r in doc.service_relationships if r.connection_type == "shared_db"]
|
|
83
|
+
if shared_db and _show_section(sections, "db"):
|
|
84
|
+
_print_shared_db(shared_db, console)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _print_dependencies(doc: RepoDocumentation, console: Console) -> None:
|
|
88
|
+
"""Print dependency table."""
|
|
89
|
+
table = Table(title="Dependencies", title_style="bold magenta")
|
|
90
|
+
table.add_column("Package", style="green")
|
|
91
|
+
table.add_column("Version")
|
|
92
|
+
table.add_column("Source")
|
|
93
|
+
table.add_column("Dev?", justify="center")
|
|
94
|
+
|
|
95
|
+
runtime = [d for d in doc.dependencies if not d.dev_only]
|
|
96
|
+
dev = [d for d in doc.dependencies if d.dev_only]
|
|
97
|
+
|
|
98
|
+
for dep in sorted(runtime, key=lambda d: d.name.lower()):
|
|
99
|
+
source_short = dep.source_file.split("/")[-1] if "/" in dep.source_file else dep.source_file
|
|
100
|
+
table.add_row(dep.name, dep.version_constraint, source_short, "")
|
|
101
|
+
|
|
102
|
+
for dep in sorted(dev, key=lambda d: d.name.lower()):
|
|
103
|
+
source_short = dep.source_file.split("/")[-1] if "/" in dep.source_file else dep.source_file
|
|
104
|
+
table.add_row(dep.name, dep.version_constraint, source_short, "yes")
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _print_endpoints(doc: RepoDocumentation, console: Console) -> None:
|
|
111
|
+
"""Print API endpoints — compact table + detailed cards."""
|
|
112
|
+
# Summary table with just the essentials
|
|
113
|
+
table = Table(title="API Endpoints", title_style="bold magenta")
|
|
114
|
+
table.add_column("Method", style="bold yellow", no_wrap=True)
|
|
115
|
+
table.add_column("Route", style="green")
|
|
116
|
+
table.add_column("Handler")
|
|
117
|
+
table.add_column("File", style="dim")
|
|
118
|
+
|
|
119
|
+
for ep in doc.endpoints:
|
|
120
|
+
rel_path = _rel_path(ep.file_path, doc.repo_name)
|
|
121
|
+
table.add_row(ep.http_method.upper(), ep.route_pattern, ep.handler_name, f"{rel_path}:{ep.line}")
|
|
122
|
+
|
|
123
|
+
console.print(table)
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
# Detailed cards for endpoints that have interesting details
|
|
127
|
+
has_details = any(
|
|
128
|
+
ep.parameters or ep.request_body_fields or ep.middleware or
|
|
129
|
+
ep.auth_decorators or ep.db_tables or ep.request_body_type
|
|
130
|
+
for ep in doc.endpoints
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if has_details:
|
|
134
|
+
console.print(Text(" Endpoint Details", style="bold magenta"))
|
|
135
|
+
console.print()
|
|
136
|
+
|
|
137
|
+
for ep in doc.endpoints:
|
|
138
|
+
details = _build_endpoint_details(ep)
|
|
139
|
+
if not details:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
method_style = {
|
|
143
|
+
"GET": "green", "POST": "yellow", "PUT": "blue",
|
|
144
|
+
"DELETE": "red", "PATCH": "cyan",
|
|
145
|
+
}.get(ep.http_method.upper(), "white")
|
|
146
|
+
|
|
147
|
+
title = f"[bold {method_style}]{ep.http_method.upper()}[/bold {method_style}] [green]{ep.route_pattern}[/green]"
|
|
148
|
+
panel = Panel(
|
|
149
|
+
"\n".join(details),
|
|
150
|
+
title=title,
|
|
151
|
+
title_align="left",
|
|
152
|
+
border_style="dim",
|
|
153
|
+
padding=(0, 2),
|
|
154
|
+
)
|
|
155
|
+
console.print(panel)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_endpoint_details(ep) -> list[str]:
|
|
159
|
+
"""Build detail lines for an endpoint panel."""
|
|
160
|
+
details = []
|
|
161
|
+
|
|
162
|
+
rel_path = ep.file_path.split("/")[-1] if "/" in ep.file_path else ep.file_path
|
|
163
|
+
details.append(f"[bold]Handler:[/bold] {ep.handler_name} [dim]({rel_path}:{ep.line})[/dim]")
|
|
164
|
+
|
|
165
|
+
if ep.parameters:
|
|
166
|
+
params = ", ".join(f"[cyan]{p.name}[/cyan] ({p.source.value})" for p in ep.parameters)
|
|
167
|
+
details.append(f"[bold]Params:[/bold] {params}")
|
|
168
|
+
|
|
169
|
+
if ep.request_body_type:
|
|
170
|
+
details.append(f"[bold]Body Type:[/bold] [cyan]{ep.request_body_type}[/cyan]")
|
|
171
|
+
|
|
172
|
+
if ep.request_body_fields:
|
|
173
|
+
fields = ", ".join(f"[cyan]{f}[/cyan]" for f in ep.request_body_fields)
|
|
174
|
+
details.append(f"[bold]Body:[/bold] {fields}")
|
|
175
|
+
|
|
176
|
+
if ep.response_fields:
|
|
177
|
+
fields = ", ".join(f"[cyan]{f}[/cyan]" for f in ep.response_fields[:6])
|
|
178
|
+
suffix = f" (+{len(ep.response_fields) - 6} more)" if len(ep.response_fields) > 6 else ""
|
|
179
|
+
details.append(f"[bold]Response:[/bold] {fields}{suffix}")
|
|
180
|
+
|
|
181
|
+
if ep.auth_decorators:
|
|
182
|
+
auth = ", ".join(f"[yellow]{a}[/yellow]" for a in ep.auth_decorators)
|
|
183
|
+
details.append(f"[bold]Auth:[/bold] {auth}")
|
|
184
|
+
|
|
185
|
+
if ep.middleware:
|
|
186
|
+
mw = ", ".join(f"[magenta]{m}[/magenta]" for m in ep.middleware)
|
|
187
|
+
details.append(f"[bold]Middleware:[/bold] {mw}")
|
|
188
|
+
|
|
189
|
+
if ep.db_tables:
|
|
190
|
+
tables = ", ".join(f"[blue]{t}[/blue]" for t in ep.db_tables)
|
|
191
|
+
details.append(f"[bold]DB Tables:[/bold] {tables}")
|
|
192
|
+
|
|
193
|
+
# Only return details if there's more than just the handler line
|
|
194
|
+
if len(details) <= 1:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
return details
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _print_api_calls(doc: RepoDocumentation, console: Console) -> None:
|
|
201
|
+
"""Print frontend API calls table."""
|
|
202
|
+
table = Table(title="Frontend API Calls", title_style="bold magenta")
|
|
203
|
+
table.add_column("Method", style="bold yellow", no_wrap=True)
|
|
204
|
+
table.add_column("URL", style="green")
|
|
205
|
+
table.add_column("Component/Page")
|
|
206
|
+
table.add_column("Traced From", style="dim")
|
|
207
|
+
|
|
208
|
+
for call in doc.api_calls:
|
|
209
|
+
traced = call.traced_from or "-"
|
|
210
|
+
table.add_row(call.http_method.upper(), call.url_pattern, call.component_or_page, traced)
|
|
211
|
+
|
|
212
|
+
console.print(table)
|
|
213
|
+
console.print()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _print_db_operations(doc: RepoDocumentation, console: Console) -> None:
|
|
217
|
+
"""Print database operations table."""
|
|
218
|
+
table = Table(title="Database Operations", title_style="bold magenta")
|
|
219
|
+
table.add_column("Operation", style="bold yellow", no_wrap=True)
|
|
220
|
+
table.add_column("Table", style="green")
|
|
221
|
+
table.add_column("ORM")
|
|
222
|
+
table.add_column("File", style="dim")
|
|
223
|
+
table.add_column("Filters", style="dim")
|
|
224
|
+
|
|
225
|
+
for op in doc.db_operations:
|
|
226
|
+
rel_path = _rel_path(op.file_path, doc.repo_name)
|
|
227
|
+
filters = ", ".join(op.filters) if op.filters else "-"
|
|
228
|
+
table.add_row(op.operation_type.upper(), op.table_name, op.orm_library, f"{rel_path}:{op.line}", filters)
|
|
229
|
+
|
|
230
|
+
console.print(table)
|
|
231
|
+
console.print()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _print_shared_db(relationships: list, console: Console) -> None:
|
|
235
|
+
"""Print shared database tables between repos."""
|
|
236
|
+
table = Table(title="Shared Database Tables", title_style="bold magenta")
|
|
237
|
+
table.add_column("Table", style="green")
|
|
238
|
+
table.add_column("Used In", style="cyan")
|
|
239
|
+
table.add_column("Also Used In", style="cyan")
|
|
240
|
+
table.add_column("Confidence", justify="center")
|
|
241
|
+
|
|
242
|
+
for rel in relationships:
|
|
243
|
+
table_name = rel.target_endpoint.replace("table:", "")
|
|
244
|
+
table.add_row(table_name, f"{rel.source_repo}/", f"{rel.target_repo}/", f"{rel.confidence:.0%}")
|
|
245
|
+
|
|
246
|
+
console.print(table)
|
|
247
|
+
console.print()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _rel_path(file_path: str, repo_name: str) -> str:
|
|
251
|
+
"""Make a file path relative for display."""
|
|
252
|
+
if repo_name + "/" in file_path:
|
|
253
|
+
return file_path.split(repo_name + "/", 1)[-1]
|
|
254
|
+
return file_path
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Generate structured JSON output from scanned repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
|
|
8
|
+
from commiter.models import RepoDocumentation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_json(docs: list[RepoDocumentation], indent: int = 2,
|
|
12
|
+
sections: dict[str, str | None] | None = None) -> str:
|
|
13
|
+
"""Generate a JSON string from the documentation results."""
|
|
14
|
+
output = []
|
|
15
|
+
for doc in docs:
|
|
16
|
+
repo_data = _serialize_repo(doc, sections)
|
|
17
|
+
output.append(repo_data)
|
|
18
|
+
|
|
19
|
+
return json.dumps(output, indent=indent, default=str)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _show(sections: dict | None, key: str) -> bool:
|
|
23
|
+
if sections is None:
|
|
24
|
+
return True
|
|
25
|
+
return sections.get(key) is not None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _serialize_repo(doc: RepoDocumentation, sections: dict | None = None) -> dict:
|
|
29
|
+
"""Serialize a RepoDocumentation to a JSON-friendly dict."""
|
|
30
|
+
result = {
|
|
31
|
+
"repo_name": doc.repo_name,
|
|
32
|
+
"repo_path": doc.repo_path,
|
|
33
|
+
"languages": doc.languages,
|
|
34
|
+
"frameworks": doc.frameworks,
|
|
35
|
+
"summary": {
|
|
36
|
+
"total_files": len(doc.file_classifications),
|
|
37
|
+
"total_endpoints": len(doc.endpoints),
|
|
38
|
+
"total_api_calls": len(doc.api_calls),
|
|
39
|
+
"total_dependencies": len(doc.dependencies),
|
|
40
|
+
"total_db_operations": len(doc.db_operations),
|
|
41
|
+
"total_relationships": len(doc.service_relationships),
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if _show(sections, "endpoints"):
|
|
46
|
+
result["endpoints"] = [
|
|
47
|
+
{
|
|
48
|
+
"http_method": ep.http_method,
|
|
49
|
+
"route_pattern": ep.route_pattern,
|
|
50
|
+
"handler_name": ep.handler_name,
|
|
51
|
+
"framework": ep.framework,
|
|
52
|
+
"file_path": ep.file_path,
|
|
53
|
+
"line": ep.line,
|
|
54
|
+
"parameters": [
|
|
55
|
+
{"name": p.name, "source": p.source.value, "type_hint": p.type_hint, "required": p.required}
|
|
56
|
+
for p in ep.parameters
|
|
57
|
+
],
|
|
58
|
+
"request_body_fields": ep.request_body_fields,
|
|
59
|
+
"response_fields": ep.response_fields,
|
|
60
|
+
"auth_decorators": ep.auth_decorators,
|
|
61
|
+
"db_tables": ep.db_tables,
|
|
62
|
+
"request_body_type": ep.request_body_type,
|
|
63
|
+
"response_type": ep.response_type,
|
|
64
|
+
"middleware": ep.middleware,
|
|
65
|
+
}
|
|
66
|
+
for ep in doc.endpoints
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if _show(sections, "calls"):
|
|
70
|
+
result["api_calls"] = [
|
|
71
|
+
{
|
|
72
|
+
"http_method": call.http_method,
|
|
73
|
+
"url_pattern": call.url_pattern,
|
|
74
|
+
"component_or_page": call.component_or_page,
|
|
75
|
+
"client_library": call.client_library,
|
|
76
|
+
"file_path": call.file_path,
|
|
77
|
+
"line": call.line,
|
|
78
|
+
"traced_from": call.traced_from,
|
|
79
|
+
"response_type": call.response_type,
|
|
80
|
+
"body_type": call.body_type,
|
|
81
|
+
}
|
|
82
|
+
for call in doc.api_calls
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if _show(sections, "deps"):
|
|
86
|
+
result["dependencies"] = {
|
|
87
|
+
"runtime": [
|
|
88
|
+
{"name": d.name, "version": d.version_constraint, "source": d.source_file}
|
|
89
|
+
for d in doc.dependencies if not d.dev_only
|
|
90
|
+
],
|
|
91
|
+
"dev": [
|
|
92
|
+
{"name": d.name, "version": d.version_constraint, "source": d.source_file}
|
|
93
|
+
for d in doc.dependencies if d.dev_only
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if _show(sections, "db"):
|
|
98
|
+
result["db_operations"] = [
|
|
99
|
+
{
|
|
100
|
+
"operation_type": op.operation_type,
|
|
101
|
+
"table_name": op.table_name,
|
|
102
|
+
"orm_library": op.orm_library,
|
|
103
|
+
"file_path": op.file_path,
|
|
104
|
+
"line": op.line,
|
|
105
|
+
"filters": op.filters,
|
|
106
|
+
}
|
|
107
|
+
for op in doc.db_operations
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
result["service_relationships"] = [
|
|
111
|
+
{
|
|
112
|
+
"source_repo": rel.source_repo,
|
|
113
|
+
"target_repo": rel.target_repo,
|
|
114
|
+
"connection_type": rel.connection_type,
|
|
115
|
+
"source_file": rel.source_file,
|
|
116
|
+
"target_endpoint": rel.target_endpoint,
|
|
117
|
+
"confidence": rel.confidence,
|
|
118
|
+
}
|
|
119
|
+
for rel in doc.service_relationships
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
return result
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Generate Markdown documentation from scanned repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from commiter.models import RepoDocumentation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_markdown(docs: list[RepoDocumentation],
|
|
11
|
+
sections: dict[str, str | None] | None = None) -> str:
|
|
12
|
+
"""Generate a single Markdown document covering all scanned repos."""
|
|
13
|
+
lines: list[str] = []
|
|
14
|
+
|
|
15
|
+
lines.append("# Repository Documentation\n")
|
|
16
|
+
lines.append(f"*Generated by commiter — {len(docs)} repository(ies) scanned.*\n")
|
|
17
|
+
lines.append("---\n")
|
|
18
|
+
|
|
19
|
+
for doc in docs:
|
|
20
|
+
lines.extend(_repo_section(doc, sections))
|
|
21
|
+
lines.append("---\n")
|
|
22
|
+
|
|
23
|
+
return "\n".join(lines)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _show(sections: dict | None, key: str) -> bool:
|
|
27
|
+
if sections is None:
|
|
28
|
+
return True
|
|
29
|
+
return sections.get(key) is not None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _repo_section(doc: RepoDocumentation,
|
|
33
|
+
sections: dict[str, str | None] | None = None) -> list[str]:
|
|
34
|
+
lines: list[str] = []
|
|
35
|
+
|
|
36
|
+
lines.append(f"## {doc.repo_name}\n")
|
|
37
|
+
|
|
38
|
+
# Overview table
|
|
39
|
+
lines.append("| Property | Value |")
|
|
40
|
+
lines.append("|----------|-------|")
|
|
41
|
+
lines.append(f"| Path | `{doc.repo_path}` |")
|
|
42
|
+
lines.append(f"| Languages | {', '.join(doc.languages) if doc.languages else 'none detected'} |")
|
|
43
|
+
lines.append(f"| Frameworks | {', '.join(doc.frameworks) if doc.frameworks else 'none detected'} |")
|
|
44
|
+
lines.append(f"| Files scanned | {len(doc.file_classifications)} |")
|
|
45
|
+
lines.append("")
|
|
46
|
+
|
|
47
|
+
# API Endpoints
|
|
48
|
+
if doc.endpoints and _show(sections, "endpoints"):
|
|
49
|
+
lines.extend(_endpoints_section(doc))
|
|
50
|
+
|
|
51
|
+
# Frontend API Calls
|
|
52
|
+
if doc.api_calls and _show(sections, "calls"):
|
|
53
|
+
lines.extend(_api_calls_section(doc))
|
|
54
|
+
|
|
55
|
+
# Dependencies
|
|
56
|
+
if doc.dependencies and _show(sections, "deps"):
|
|
57
|
+
lines.extend(_dependencies_section(doc))
|
|
58
|
+
|
|
59
|
+
# DB Operations
|
|
60
|
+
if doc.db_operations and _show(sections, "db"):
|
|
61
|
+
lines.extend(_db_operations_section(doc))
|
|
62
|
+
|
|
63
|
+
return lines
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _endpoints_section(doc: RepoDocumentation) -> list[str]:
|
|
67
|
+
lines: list[str] = []
|
|
68
|
+
lines.append("### API Endpoints\n")
|
|
69
|
+
lines.append("| Method | Route | Handler | File | Auth |")
|
|
70
|
+
lines.append("|--------|-------|---------|------|------|")
|
|
71
|
+
|
|
72
|
+
for ep in doc.endpoints:
|
|
73
|
+
rel = _rel_path(ep.file_path, doc.repo_name)
|
|
74
|
+
auth = ", ".join(ep.auth_decorators) if ep.auth_decorators else "-"
|
|
75
|
+
lines.append(f"| `{ep.http_method}` | `{ep.route_pattern}` | `{ep.handler_name}` | `{rel}:{ep.line}` | {auth} |")
|
|
76
|
+
|
|
77
|
+
lines.append("")
|
|
78
|
+
|
|
79
|
+
# Endpoint details
|
|
80
|
+
for ep in doc.endpoints:
|
|
81
|
+
lines.append(f"#### `{ep.http_method} {ep.route_pattern}`\n")
|
|
82
|
+
lines.append(f"- **Handler**: `{ep.handler_name}` ({ep.framework})")
|
|
83
|
+
if ep.parameters:
|
|
84
|
+
params_str = ", ".join(f"`{p.name}` ({p.source.value})" for p in ep.parameters)
|
|
85
|
+
lines.append(f"- **Parameters**: {params_str}")
|
|
86
|
+
if ep.request_body_fields:
|
|
87
|
+
lines.append(f"- **Request body**: {', '.join(f'`{f}`' for f in ep.request_body_fields)}")
|
|
88
|
+
if ep.response_fields:
|
|
89
|
+
lines.append(f"- **Response fields**: {', '.join(f'`{f}`' for f in ep.response_fields)}")
|
|
90
|
+
if ep.auth_decorators:
|
|
91
|
+
lines.append(f"- **Auth**: {', '.join(f'`{a}`' for a in ep.auth_decorators)}")
|
|
92
|
+
if ep.request_body_type:
|
|
93
|
+
lines.append(f"- **Request body type**: `{ep.request_body_type}`")
|
|
94
|
+
if ep.response_type:
|
|
95
|
+
lines.append(f"- **Response type**: `{ep.response_type}`")
|
|
96
|
+
if ep.middleware:
|
|
97
|
+
lines.append(f"- **Middleware**: {', '.join(f'`{m}`' for m in ep.middleware)}")
|
|
98
|
+
if ep.db_tables:
|
|
99
|
+
lines.append(f"- **DB tables**: {', '.join(f'`{t}`' for t in ep.db_tables)}")
|
|
100
|
+
lines.append("")
|
|
101
|
+
|
|
102
|
+
return lines
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _api_calls_section(doc: RepoDocumentation) -> list[str]:
|
|
106
|
+
lines: list[str] = []
|
|
107
|
+
lines.append("### Frontend API Calls\n")
|
|
108
|
+
lines.append("| Method | URL | Component/Page | Library | Traced From |")
|
|
109
|
+
lines.append("|--------|-----|----------------|---------|-------------|")
|
|
110
|
+
|
|
111
|
+
for call in doc.api_calls:
|
|
112
|
+
traced = call.traced_from or "-"
|
|
113
|
+
lines.append(f"| `{call.http_method}` | `{call.url_pattern}` | `{call.component_or_page}` | {call.client_library} | {traced} |")
|
|
114
|
+
|
|
115
|
+
lines.append("")
|
|
116
|
+
return lines
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _dependencies_section(doc: RepoDocumentation) -> list[str]:
|
|
120
|
+
lines: list[str] = []
|
|
121
|
+
runtime = [d for d in doc.dependencies if not d.dev_only]
|
|
122
|
+
dev = [d for d in doc.dependencies if d.dev_only]
|
|
123
|
+
|
|
124
|
+
if runtime:
|
|
125
|
+
lines.append("### Dependencies (Runtime)\n")
|
|
126
|
+
lines.append("| Package | Version | Source |")
|
|
127
|
+
lines.append("|---------|---------|--------|")
|
|
128
|
+
for dep in sorted(runtime, key=lambda d: d.name.lower()):
|
|
129
|
+
source = Path(dep.source_file).name
|
|
130
|
+
lines.append(f"| `{dep.name}` | `{dep.version_constraint}` | {source} |")
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
if dev:
|
|
134
|
+
lines.append("### Dependencies (Dev)\n")
|
|
135
|
+
lines.append("| Package | Version | Source |")
|
|
136
|
+
lines.append("|---------|---------|--------|")
|
|
137
|
+
for dep in sorted(dev, key=lambda d: d.name.lower()):
|
|
138
|
+
source = Path(dep.source_file).name
|
|
139
|
+
lines.append(f"| `{dep.name}` | `{dep.version_constraint}` | {source} |")
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _db_operations_section(doc: RepoDocumentation) -> list[str]:
|
|
146
|
+
lines: list[str] = []
|
|
147
|
+
lines.append("### Database Operations\n")
|
|
148
|
+
lines.append("| Operation | Table | ORM | File |")
|
|
149
|
+
lines.append("|-----------|-------|-----|------|")
|
|
150
|
+
|
|
151
|
+
for op in doc.db_operations:
|
|
152
|
+
rel = _rel_path(op.file_path, doc.repo_name)
|
|
153
|
+
lines.append(f"| `{op.operation_type}` | `{op.table_name}` | {op.orm_library} | `{rel}:{op.line}` |")
|
|
154
|
+
|
|
155
|
+
lines.append("")
|
|
156
|
+
return lines
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _rel_path(file_path: str, repo_name: str) -> str:
|
|
160
|
+
"""Make a file path relative to the repo for display."""
|
|
161
|
+
if repo_name + "/" in file_path:
|
|
162
|
+
return file_path.split(repo_name + "/", 1)[-1]
|
|
163
|
+
return file_path
|