aja-codeintel 0.1.9__tar.gz → 0.2.0__tar.gz
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.
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/PKG-INFO +1 -1
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/PKG-INFO +1 -1
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/SOURCES.txt +5 -1
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/__init__.py +2 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/endpoints_cmd.py +1 -1
- aja_codeintel-0.2.0/codeintel_cli/commands/project/entire_cmd.py +417 -0
- aja_codeintel-0.2.0/codeintel_cli/commands/project/trace_cmd.py +738 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/method_index.py +2 -2
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/models.py +2 -15
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/python/models.py +1 -14
- aja_codeintel-0.2.0/codeintel_cli/lang/shared_models.py +19 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/pyproject.toml +1 -1
- aja_codeintel-0.2.0/tests/test_entire_cmd.py +208 -0
- aja_codeintel-0.2.0/tests/test_trace_cmd.py +270 -0
- aja_codeintel-0.1.9/codeintel_cli/commands/project/trace_cmd.py +0 -473
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/LICENSE +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/README.md +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/dependency_links.txt +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/entry_points.txt +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/requires.txt +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/aja_codeintel.egg-info/top_level.txt +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/__main__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/cli.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/graph/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/graph/deps_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/graph/related_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/graph/relsymbols_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/graph/reverse_related_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/nav/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/nav/copy_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/nav/open_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/nav/where_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/context_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/debug_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/errors_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/fastapi_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/folder_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/frontend_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/imports_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/models_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/modeltree_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/new.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/overview_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/resolve_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/scan_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/servicemap_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/tree_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/types_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/commands/project/version_cmd.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/java_context.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/java_rel.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/java_service.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/python_context.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/python_rel.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/context/python_service.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/fuzzy.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/opener.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/project.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/resolve_folder.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/resolve_model_target.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/resolve_target.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/timing.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/core/where.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/db/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/db/cache.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/db/operations.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/db/schema.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/fastapi_scanner.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/java_spring.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/models.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/openapi_spec.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/python_web.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/endpoints/scan.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/errors.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/frontend/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/frontend/server.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/graph/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/graph/builder.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/graph/query.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/graph/traverse.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/ast_engine.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/auth.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/base_path.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/call_graph.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/engine.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/enums.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/pagination.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/resolve.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/types.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/java/validation.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/python/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/python/engine.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/lang/router.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/parser/imports.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/parser/resolve.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/parser/symbols.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/scanner/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/scanner/scanner.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/terminal/__init__.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/terminal/error_parser.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/codeintel_cli/terminal/printer.py +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/setup.cfg +0 -0
- {aja_codeintel-0.1.9 → aja_codeintel-0.2.0}/tests/test_fastapi_scanner.py +0 -0
|
@@ -25,6 +25,7 @@ codeintel_cli/commands/project/__init__.py
|
|
|
25
25
|
codeintel_cli/commands/project/context_cmd.py
|
|
26
26
|
codeintel_cli/commands/project/debug_cmd.py
|
|
27
27
|
codeintel_cli/commands/project/endpoints_cmd.py
|
|
28
|
+
codeintel_cli/commands/project/entire_cmd.py
|
|
28
29
|
codeintel_cli/commands/project/errors_cmd.py
|
|
29
30
|
codeintel_cli/commands/project/fastapi_cmd.py
|
|
30
31
|
codeintel_cli/commands/project/folder_cmd.py
|
|
@@ -74,6 +75,7 @@ codeintel_cli/graph/query.py
|
|
|
74
75
|
codeintel_cli/graph/traverse.py
|
|
75
76
|
codeintel_cli/lang/__init__.py
|
|
76
77
|
codeintel_cli/lang/router.py
|
|
78
|
+
codeintel_cli/lang/shared_models.py
|
|
77
79
|
codeintel_cli/lang/java/__init__.py
|
|
78
80
|
codeintel_cli/lang/java/ast_engine.py
|
|
79
81
|
codeintel_cli/lang/java/auth.py
|
|
@@ -98,4 +100,6 @@ codeintel_cli/scanner/scanner.py
|
|
|
98
100
|
codeintel_cli/terminal/__init__.py
|
|
99
101
|
codeintel_cli/terminal/error_parser.py
|
|
100
102
|
codeintel_cli/terminal/printer.py
|
|
101
|
-
tests/
|
|
103
|
+
tests/test_entire_cmd.py
|
|
104
|
+
tests/test_fastapi_scanner.py
|
|
105
|
+
tests/test_trace_cmd.py
|
|
@@ -21,6 +21,7 @@ def register_project_commands(app: typer.Typer) -> None:
|
|
|
21
21
|
from .debug_cmd import register_debug
|
|
22
22
|
from .fastapi_cmd import register_fastapi_scan
|
|
23
23
|
from .trace_cmd import register_trace
|
|
24
|
+
from .entire_cmd import register_entire
|
|
24
25
|
|
|
25
26
|
register_overview(app)
|
|
26
27
|
register_models(app)
|
|
@@ -40,3 +41,4 @@ def register_project_commands(app: typer.Typer) -> None:
|
|
|
40
41
|
register_debug(app)
|
|
41
42
|
register_fastapi_scan(app)
|
|
42
43
|
register_trace(app)
|
|
44
|
+
register_entire(app)
|
|
@@ -14,7 +14,7 @@ from ...core.project import find_project_root
|
|
|
14
14
|
from ...endpoints.java_spring import extract_spring_endpoints
|
|
15
15
|
from ...endpoints.python_web import extract_python_endpoints
|
|
16
16
|
from ...endpoints.scan import EndpointScanOptions, iter_supported_source_files
|
|
17
|
-
from ...endpoints.openapi_spec import extract_openapi_endpoints
|
|
17
|
+
from ...endpoints.openapi_spec import extract_openapi_endpoints
|
|
18
18
|
from ...errors import InvalidPathError
|
|
19
19
|
|
|
20
20
|
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aja entire [path]
|
|
3
|
+
|
|
4
|
+
Full project intelligence report:
|
|
5
|
+
- Project tree (with .env, all files, inner functions, classes, models, DTOs)
|
|
6
|
+
- Every endpoint with FULL TRACE (import-aware, follows actual code flow)
|
|
7
|
+
- Request/Response models for each endpoint
|
|
8
|
+
- No guessing — traces through actual import chains
|
|
9
|
+
|
|
10
|
+
Even 130+ endpoints — every single one gets a proper trace.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import time
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
|
|
25
|
+
from ...core.project import find_project_root
|
|
26
|
+
from ...endpoints.fastapi_scanner import scan_fastapi_project, _collect_python_files, PydanticDTO
|
|
27
|
+
from .trace_cmd import (
|
|
28
|
+
_build_func_index,
|
|
29
|
+
_trace,
|
|
30
|
+
_render,
|
|
31
|
+
_file_role,
|
|
32
|
+
_short_path,
|
|
33
|
+
_get_ast,
|
|
34
|
+
_find_func,
|
|
35
|
+
_ast_cache,
|
|
36
|
+
FuncDef,
|
|
37
|
+
TraceNode,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------- #
|
|
41
|
+
# Ignore dirs
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
|
|
44
|
+
_SKIP_DIRS = {
|
|
45
|
+
".git", ".idea", ".vscode", "__pycache__", ".pytest_cache",
|
|
46
|
+
".ruff_cache", ".tox", ".venv", "venv", "env",
|
|
47
|
+
"node_modules", "dist", "build", "out", "target", ".gradle",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------------- #
|
|
51
|
+
# Tree: show .env, all source files with classes/functions inside
|
|
52
|
+
# --------------------------------------------------------------------------- #
|
|
53
|
+
|
|
54
|
+
def _walk_source_files(root: Path) -> list[Path]:
|
|
55
|
+
"""Walk project and collect all .py files, .env, config files."""
|
|
56
|
+
files: list[Path] = []
|
|
57
|
+
stack = [root.resolve()]
|
|
58
|
+
while stack:
|
|
59
|
+
cur = stack.pop()
|
|
60
|
+
try:
|
|
61
|
+
for child in sorted(cur.iterdir(), key=lambda p: (p.is_file(), p.name.lower())):
|
|
62
|
+
if child.is_dir():
|
|
63
|
+
if child.name not in _SKIP_DIRS:
|
|
64
|
+
stack.append(child)
|
|
65
|
+
elif child.suffix in (".py", ".env", ".toml", ".yaml", ".yml", ".cfg", ".ini", ".json"):
|
|
66
|
+
files.append(child)
|
|
67
|
+
elif child.name in (".env", ".env.example", ".env.local", "Dockerfile", "docker-compose.yml"):
|
|
68
|
+
files.append(child)
|
|
69
|
+
except (PermissionError, FileNotFoundError):
|
|
70
|
+
continue
|
|
71
|
+
return files
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _extract_file_symbols(path: Path) -> dict:
|
|
75
|
+
"""
|
|
76
|
+
Extract classes (with methods/fields) and top-level functions from a .py file.
|
|
77
|
+
Returns: { "classes": [...], "functions": [...] }
|
|
78
|
+
"""
|
|
79
|
+
result = {"classes": [], "functions": []}
|
|
80
|
+
try:
|
|
81
|
+
src = path.read_text(encoding="utf-8", errors="ignore")
|
|
82
|
+
tree = ast.parse(src)
|
|
83
|
+
except Exception:
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
for node in tree.body:
|
|
87
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
88
|
+
params = []
|
|
89
|
+
for arg in node.args.args:
|
|
90
|
+
if arg.arg == "self":
|
|
91
|
+
continue
|
|
92
|
+
ann = ast.unparse(arg.annotation) if arg.annotation else "Any"
|
|
93
|
+
params.append(f"{arg.arg}: {ann}")
|
|
94
|
+
ret = ast.unparse(node.returns) if node.returns else "None"
|
|
95
|
+
result["functions"].append({
|
|
96
|
+
"name": node.name,
|
|
97
|
+
"params": params,
|
|
98
|
+
"return": ret,
|
|
99
|
+
"line": node.lineno,
|
|
100
|
+
})
|
|
101
|
+
elif isinstance(node, ast.ClassDef):
|
|
102
|
+
cls_info = {
|
|
103
|
+
"name": node.name,
|
|
104
|
+
"bases": [ast.unparse(b) for b in node.bases],
|
|
105
|
+
"methods": [],
|
|
106
|
+
"fields": [],
|
|
107
|
+
"line": node.lineno,
|
|
108
|
+
}
|
|
109
|
+
for item in node.body:
|
|
110
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
111
|
+
params = []
|
|
112
|
+
for arg in item.args.args:
|
|
113
|
+
if arg.arg == "self":
|
|
114
|
+
continue
|
|
115
|
+
ann = ast.unparse(arg.annotation) if arg.annotation else "Any"
|
|
116
|
+
params.append(f"{arg.arg}: {ann}")
|
|
117
|
+
ret = ast.unparse(item.returns) if item.returns else "None"
|
|
118
|
+
cls_info["methods"].append({
|
|
119
|
+
"name": item.name,
|
|
120
|
+
"params": params,
|
|
121
|
+
"return": ret,
|
|
122
|
+
"line": item.lineno,
|
|
123
|
+
})
|
|
124
|
+
elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
125
|
+
field_type = ast.unparse(item.annotation) if item.annotation else "Any"
|
|
126
|
+
cls_info["fields"].append({
|
|
127
|
+
"name": item.target.id,
|
|
128
|
+
"type": field_type,
|
|
129
|
+
})
|
|
130
|
+
result["classes"].append(cls_info)
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print_project_tree(project_root: Path) -> None:
|
|
136
|
+
"""Print project tree with .env files and all source files showing inner structure."""
|
|
137
|
+
typer.echo("=" * 70)
|
|
138
|
+
typer.echo(" PROJECT TREE (with classes, functions, models)")
|
|
139
|
+
typer.echo("=" * 70)
|
|
140
|
+
typer.echo(f" ROOT: {project_root}")
|
|
141
|
+
typer.echo("")
|
|
142
|
+
|
|
143
|
+
# Show .env files first
|
|
144
|
+
env_files = []
|
|
145
|
+
for pattern in (".env", ".env.example", ".env.local", ".env.development", ".env.production"):
|
|
146
|
+
f = project_root / pattern
|
|
147
|
+
if f.exists():
|
|
148
|
+
env_files.append(f)
|
|
149
|
+
|
|
150
|
+
if env_files:
|
|
151
|
+
typer.echo(" ╔══ ENVIRONMENT FILES")
|
|
152
|
+
for ef in env_files:
|
|
153
|
+
typer.echo(f" ║ {ef.name}")
|
|
154
|
+
try:
|
|
155
|
+
lines = ef.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
156
|
+
for line in lines:
|
|
157
|
+
line = line.strip()
|
|
158
|
+
if not line or line.startswith("#"):
|
|
159
|
+
continue
|
|
160
|
+
# Show key but mask sensitive values
|
|
161
|
+
if "=" in line:
|
|
162
|
+
key = line.split("=", 1)[0].strip()
|
|
163
|
+
typer.echo(f" ║ {key}=***")
|
|
164
|
+
else:
|
|
165
|
+
typer.echo(f" ║ {line}")
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
typer.echo(" ╚══")
|
|
169
|
+
typer.echo("")
|
|
170
|
+
|
|
171
|
+
# Walk and show source structure
|
|
172
|
+
def _walk_tree(cur: Path, prefix: str = "", depth: int = 0, max_depth: int = 10) -> None:
|
|
173
|
+
if depth > max_depth:
|
|
174
|
+
return
|
|
175
|
+
try:
|
|
176
|
+
entries = sorted(cur.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
|
|
177
|
+
except (PermissionError, FileNotFoundError):
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
visible = []
|
|
181
|
+
for e in entries:
|
|
182
|
+
if e.name in _SKIP_DIRS:
|
|
183
|
+
continue
|
|
184
|
+
if e.is_dir():
|
|
185
|
+
visible.append(e)
|
|
186
|
+
elif e.suffix in (".py",) or e.name in (".env",):
|
|
187
|
+
visible.append(e)
|
|
188
|
+
|
|
189
|
+
for i, entry in enumerate(visible):
|
|
190
|
+
is_last = i == len(visible) - 1
|
|
191
|
+
branch = "└── " if is_last else "├── "
|
|
192
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
193
|
+
|
|
194
|
+
if entry.is_dir():
|
|
195
|
+
typer.echo(f" {prefix}{branch}{entry.name}/")
|
|
196
|
+
_walk_tree(entry, child_prefix, depth + 1, max_depth)
|
|
197
|
+
else:
|
|
198
|
+
typer.echo(f" {prefix}{branch}{entry.name}")
|
|
199
|
+
# Show inner structure for .py files
|
|
200
|
+
if entry.suffix == ".py":
|
|
201
|
+
symbols = _extract_file_symbols(entry)
|
|
202
|
+
inner_items = []
|
|
203
|
+
for cls in symbols["classes"]:
|
|
204
|
+
bases = f"({', '.join(cls['bases'])})" if cls['bases'] else ""
|
|
205
|
+
inner_items.append(("class", f"{cls['name']}{bases}", cls))
|
|
206
|
+
for fn in symbols["functions"]:
|
|
207
|
+
inner_items.append(("func", f"{fn['name']}()", fn))
|
|
208
|
+
|
|
209
|
+
for j, (kind, label, info) in enumerate(inner_items):
|
|
210
|
+
is_inner_last = j == len(inner_items) - 1
|
|
211
|
+
inner_branch = "└─ " if is_inner_last else "├─ "
|
|
212
|
+
inner_prefix = child_prefix + (" " if is_inner_last else "│ ")
|
|
213
|
+
|
|
214
|
+
if kind == "class":
|
|
215
|
+
typer.echo(f" {child_prefix}{inner_branch}class {label}")
|
|
216
|
+
# Show fields and methods
|
|
217
|
+
members = []
|
|
218
|
+
for fld in info.get("fields", []):
|
|
219
|
+
members.append(f"{fld['name']}: {fld['type']}")
|
|
220
|
+
for meth in info.get("methods", []):
|
|
221
|
+
members.append(f"{meth['name']}()")
|
|
222
|
+
for k, mem in enumerate(members[:15]): # limit to 15
|
|
223
|
+
mem_last = k == len(members[:15]) - 1
|
|
224
|
+
mem_branch = "└─ " if mem_last else "├─ "
|
|
225
|
+
typer.echo(f" {inner_prefix}{mem_branch}{mem}")
|
|
226
|
+
else:
|
|
227
|
+
typer.echo(f" {child_prefix}{inner_branch}def {label}")
|
|
228
|
+
|
|
229
|
+
_walk_tree(project_root)
|
|
230
|
+
typer.echo("")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# --------------------------------------------------------------------------- #
|
|
234
|
+
# DTO / Model display helpers
|
|
235
|
+
# --------------------------------------------------------------------------- #
|
|
236
|
+
|
|
237
|
+
def _format_dto(dto: PydanticDTO, indent: str = " ") -> list[str]:
|
|
238
|
+
"""Format a Pydantic DTO with all fields."""
|
|
239
|
+
lines = []
|
|
240
|
+
bases = f"({', '.join(dto.bases)})" if dto.bases else ""
|
|
241
|
+
lines.append(f"{indent}{dto.name}{bases}")
|
|
242
|
+
for f in dto.fields:
|
|
243
|
+
req = "*" if f.required else " "
|
|
244
|
+
default = f" = {f.default}" if f.default else ""
|
|
245
|
+
lines.append(f"{indent} {req} {f.name}: {f.type}{default}")
|
|
246
|
+
return lines
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------------------------------- #
|
|
250
|
+
# ENTIRE command: traces every endpoint
|
|
251
|
+
# --------------------------------------------------------------------------- #
|
|
252
|
+
|
|
253
|
+
def register_entire(app: typer.Typer) -> None:
|
|
254
|
+
@app.command("entire", help="Full project report: tree + every endpoint traced with request/response.")
|
|
255
|
+
def entire(
|
|
256
|
+
path: str = typer.Argument(".", help="Project root folder"),
|
|
257
|
+
depth: int = typer.Option(6, "--depth", "-d", help="Max trace depth per endpoint"),
|
|
258
|
+
no_tree: bool = typer.Option(False, "--no-tree", help="Skip project tree section"),
|
|
259
|
+
) -> None:
|
|
260
|
+
_ast_cache.clear()
|
|
261
|
+
t0 = time.perf_counter()
|
|
262
|
+
|
|
263
|
+
folder = Path(path).resolve()
|
|
264
|
+
if not folder.exists() or not folder.is_dir():
|
|
265
|
+
typer.echo(f" Invalid folder: {folder}")
|
|
266
|
+
raise typer.Exit(1)
|
|
267
|
+
|
|
268
|
+
project_root = find_project_root(folder)
|
|
269
|
+
|
|
270
|
+
# ── 1. Project Tree ────────────────────────────────────────────
|
|
271
|
+
if not no_tree:
|
|
272
|
+
_print_project_tree(project_root)
|
|
273
|
+
|
|
274
|
+
# ── 2. Scan all endpoints and models ───────────────────────────
|
|
275
|
+
all_py = _collect_python_files(project_root, _SKIP_DIRS)
|
|
276
|
+
endpoints, pydantic_dtos, sqla_models, request_models, response_models, endpoint_deps = \
|
|
277
|
+
scan_fastapi_project(project_root, files=all_py)
|
|
278
|
+
|
|
279
|
+
# Build indexes
|
|
280
|
+
func_index = _build_func_index(all_py)
|
|
281
|
+
|
|
282
|
+
# DTO lookup by name
|
|
283
|
+
dto_by_name: dict[str, PydanticDTO] = {d.name: d for d in pydantic_dtos}
|
|
284
|
+
|
|
285
|
+
# De-duplicate endpoints
|
|
286
|
+
seen_keys: set[tuple[str, str]] = set()
|
|
287
|
+
unique_endpoints = []
|
|
288
|
+
for e in endpoints:
|
|
289
|
+
k = (e.method, e.path)
|
|
290
|
+
if k not in seen_keys:
|
|
291
|
+
seen_keys.add(k)
|
|
292
|
+
unique_endpoints.append(e)
|
|
293
|
+
unique_endpoints.sort(key=lambda e: (e.path, e.method))
|
|
294
|
+
|
|
295
|
+
# ── 3. MODELS / DTOs Section ──────────────────────────────────
|
|
296
|
+
typer.echo("=" * 70)
|
|
297
|
+
typer.echo(" MODELS & DTOs")
|
|
298
|
+
typer.echo("=" * 70)
|
|
299
|
+
typer.echo("")
|
|
300
|
+
|
|
301
|
+
if sqla_models:
|
|
302
|
+
typer.echo(" ── SQLAlchemy Models ──")
|
|
303
|
+
for m in sqla_models:
|
|
304
|
+
typer.echo(f" {m.name} (table: {m.table_name}) {m.file}")
|
|
305
|
+
for col in m.columns:
|
|
306
|
+
pk = " [PK]" if col.primary_key else ""
|
|
307
|
+
fk = f" [FK→{col.foreign_key}]" if col.foreign_key else ""
|
|
308
|
+
typer.echo(f" {col.name}: {col.col_type}{pk}{fk}")
|
|
309
|
+
for rel in m.relationships:
|
|
310
|
+
typer.echo(f" →{rel.name}: {rel.target}")
|
|
311
|
+
typer.echo("")
|
|
312
|
+
|
|
313
|
+
if pydantic_dtos:
|
|
314
|
+
# Classify
|
|
315
|
+
req_dtos = [d for d in pydantic_dtos if d.name in request_models and d.name not in response_models]
|
|
316
|
+
res_dtos = [d for d in pydantic_dtos if d.name in response_models and d.name not in request_models]
|
|
317
|
+
both_dtos = [d for d in pydantic_dtos if d.name in request_models and d.name in response_models]
|
|
318
|
+
other_dtos = [d for d in pydantic_dtos if d.name not in request_models and d.name not in response_models]
|
|
319
|
+
|
|
320
|
+
for label, group in [("REQUEST DTOs", req_dtos), ("RESPONSE DTOs", res_dtos),
|
|
321
|
+
("REQUEST+RESPONSE", both_dtos), ("SHARED/CONFIG", other_dtos)]:
|
|
322
|
+
if not group:
|
|
323
|
+
continue
|
|
324
|
+
typer.echo(f" ── {label} ({len(group)}) ──")
|
|
325
|
+
for dto in group:
|
|
326
|
+
for line in _format_dto(dto):
|
|
327
|
+
typer.echo(line)
|
|
328
|
+
typer.echo("")
|
|
329
|
+
|
|
330
|
+
# ── 4. ENDPOINT TRACES ────────────────────────────────────────
|
|
331
|
+
typer.echo("=" * 70)
|
|
332
|
+
typer.echo(f" ENDPOINT TRACES ({len(unique_endpoints)} endpoints)")
|
|
333
|
+
typer.echo("=" * 70)
|
|
334
|
+
typer.echo("")
|
|
335
|
+
|
|
336
|
+
for idx, ep in enumerate(unique_endpoints, 1):
|
|
337
|
+
handler_name = ep.handler
|
|
338
|
+
handler_file = Path(ep.file)
|
|
339
|
+
|
|
340
|
+
typer.echo(f" {'━' * 66}")
|
|
341
|
+
typer.echo(f" [{idx:03d}] {ep.method} {ep.path}")
|
|
342
|
+
typer.echo(f" {'━' * 66}")
|
|
343
|
+
typer.echo(f" Handler : {handler_name}() · {_short_path(handler_file, project_root)}")
|
|
344
|
+
|
|
345
|
+
# Show request/response models for this endpoint
|
|
346
|
+
req_model = getattr(ep, 'request_body', None) or None
|
|
347
|
+
resp_model = getattr(ep, 'response_model', None) or None
|
|
348
|
+
|
|
349
|
+
if req_model:
|
|
350
|
+
typer.echo(f" Request : {req_model}")
|
|
351
|
+
if req_model in dto_by_name:
|
|
352
|
+
dto = dto_by_name[req_model]
|
|
353
|
+
for f in dto.fields:
|
|
354
|
+
req_mark = "*" if f.required else " "
|
|
355
|
+
typer.echo(f" {req_mark} {f.name}: {f.type}")
|
|
356
|
+
|
|
357
|
+
if resp_model:
|
|
358
|
+
typer.echo(f" Response: {resp_model}")
|
|
359
|
+
# Try to resolve the actual type (strip list[], Optional[], etc.)
|
|
360
|
+
inner = re.sub(r"^(?:list|List)\[(.+)\]$", r"\1", resp_model)
|
|
361
|
+
inner = re.sub(r"^Optional\[(.+)\]$", r"\1", inner)
|
|
362
|
+
if inner in dto_by_name:
|
|
363
|
+
dto = dto_by_name[inner]
|
|
364
|
+
for f in dto.fields:
|
|
365
|
+
typer.echo(f" {f.name}: {f.type}")
|
|
366
|
+
|
|
367
|
+
# Show dependencies
|
|
368
|
+
ep_deps = None
|
|
369
|
+
for dep_item in endpoint_deps:
|
|
370
|
+
if dep_item["method"] == ep.method and dep_item["path"] == ep.path:
|
|
371
|
+
ep_deps = dep_item["depends"]
|
|
372
|
+
break
|
|
373
|
+
if ep_deps:
|
|
374
|
+
typer.echo(f" Depends : {', '.join(ep_deps)}")
|
|
375
|
+
|
|
376
|
+
# Trace the call chain
|
|
377
|
+
tree = _get_ast(handler_file)
|
|
378
|
+
if tree is None:
|
|
379
|
+
typer.echo(f" ⚠ Could not parse {handler_file.name}")
|
|
380
|
+
typer.echo("")
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
handler_node = _find_func(tree, handler_name)
|
|
384
|
+
if handler_node is None:
|
|
385
|
+
typer.echo(f" ⚠ Function '{handler_name}' not found in {handler_file.name}")
|
|
386
|
+
typer.echo("")
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
visited: set[tuple[str, str]] = set()
|
|
390
|
+
root_node = _trace(handler_node, handler_file, func_index, project_root,
|
|
391
|
+
visited, depth=0, max_depth=depth)
|
|
392
|
+
|
|
393
|
+
typer.echo(f" Call chain:")
|
|
394
|
+
typer.echo(f" {handler_name}()")
|
|
395
|
+
child_prefix = " "
|
|
396
|
+
for i, child in enumerate(root_node.calls):
|
|
397
|
+
for line in _render(child, project_root, child_prefix, i == len(root_node.calls) - 1):
|
|
398
|
+
typer.echo(line)
|
|
399
|
+
|
|
400
|
+
if root_node.unresolved:
|
|
401
|
+
typer.echo(f" Unresolved: {', '.join(root_node.unresolved)}")
|
|
402
|
+
|
|
403
|
+
typer.echo("")
|
|
404
|
+
|
|
405
|
+
# ── 5. Summary ────────────────────────────────────────────────
|
|
406
|
+
elapsed = time.perf_counter() - t0
|
|
407
|
+
typer.echo("=" * 70)
|
|
408
|
+
typer.echo(f" SUMMARY")
|
|
409
|
+
typer.echo("=" * 70)
|
|
410
|
+
typer.echo(f" Total endpoints : {len(unique_endpoints)}")
|
|
411
|
+
typer.echo(f" Total DTOs : {len(pydantic_dtos)}")
|
|
412
|
+
typer.echo(f" Request models : {len(request_models)}")
|
|
413
|
+
typer.echo(f" Response models : {len(response_models)}")
|
|
414
|
+
typer.echo(f" SQLAlchemy models : {len(sqla_models)}")
|
|
415
|
+
typer.echo(f" Python files : {len(all_py)}")
|
|
416
|
+
typer.echo(f" Finished in : {elapsed:.2f}s")
|
|
417
|
+
typer.echo("")
|