aja-codeintel 0.1.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.
- aja_codeintel-0.1.0.dist-info/METADATA +436 -0
- aja_codeintel-0.1.0.dist-info/RECORD +68 -0
- aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
- aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
- codeintel_cli/__init__.py +1 -0
- codeintel_cli/__main__.py +4 -0
- codeintel_cli/cli.py +41 -0
- codeintel_cli/commands/__init__.py +1 -0
- codeintel_cli/commands/graph/__init__.py +18 -0
- codeintel_cli/commands/graph/deps_cmd.py +35 -0
- codeintel_cli/commands/graph/related_cmd.py +121 -0
- codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
- codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
- codeintel_cli/commands/nav/__init__.py +12 -0
- codeintel_cli/commands/nav/copy_cmd.py +101 -0
- codeintel_cli/commands/nav/open_cmd.py +18 -0
- codeintel_cli/commands/nav/where_cmd.py +21 -0
- codeintel_cli/commands/project/__init__.py +26 -0
- codeintel_cli/commands/project/context_cmd.py +326 -0
- codeintel_cli/commands/project/folder_cmd.py +51 -0
- codeintel_cli/commands/project/imports_cmd.py +90 -0
- codeintel_cli/commands/project/models_cmd.py +98 -0
- codeintel_cli/commands/project/modeltree_cmd.py +476 -0
- codeintel_cli/commands/project/new.py +0 -0
- codeintel_cli/commands/project/resolve_cmd.py +29 -0
- codeintel_cli/commands/project/scan_cmd.py +51 -0
- codeintel_cli/commands/project/servicemap_cmd.py +180 -0
- codeintel_cli/commands/project/tree_cmd.py +203 -0
- codeintel_cli/commands/project/version_cmd.py +14 -0
- codeintel_cli/context/java_context.py +180 -0
- codeintel_cli/context/java_rel.py +299 -0
- codeintel_cli/context/java_service.py +291 -0
- codeintel_cli/context/python_context.py +91 -0
- codeintel_cli/context/python_rel.py +251 -0
- codeintel_cli/context/python_service.py +205 -0
- codeintel_cli/core/fuzzy.py +72 -0
- codeintel_cli/core/opener.py +37 -0
- codeintel_cli/core/project.py +34 -0
- codeintel_cli/core/resolve_folder.py +68 -0
- codeintel_cli/core/resolve_model_target.py +92 -0
- codeintel_cli/core/resolve_target.py +53 -0
- codeintel_cli/core/timing.py +13 -0
- codeintel_cli/core/where.py +77 -0
- codeintel_cli/db/__init__.py +7 -0
- codeintel_cli/db/cache.py +224 -0
- codeintel_cli/db/operations.py +333 -0
- codeintel_cli/db/schema.py +102 -0
- codeintel_cli/errors.py +78 -0
- codeintel_cli/graph/__init__.py +1 -0
- codeintel_cli/graph/builder.py +149 -0
- codeintel_cli/graph/query.py +30 -0
- codeintel_cli/graph/traverse.py +49 -0
- codeintel_cli/lang/__init__.py +0 -0
- codeintel_cli/lang/java/__init__.py +0 -0
- codeintel_cli/lang/java/engine.py +18 -0
- codeintel_cli/lang/java/models.py +105 -0
- codeintel_cli/lang/java/resolve.py +49 -0
- codeintel_cli/lang/python/__init__.py +0 -0
- codeintel_cli/lang/python/engine.py +8 -0
- codeintel_cli/lang/python/models.py +86 -0
- codeintel_cli/lang/router.py +24 -0
- codeintel_cli/parser/imports.py +26 -0
- codeintel_cli/parser/resolve.py +49 -0
- codeintel_cli/parser/symbols.py +92 -0
- codeintel_cli/scanner/__init__.py +0 -0
- codeintel_cli/scanner/scanner.py +41 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ...errors import InvalidPathError
|
|
10
|
+
from ...core.resolve_model_target import resolve_model_target_file as _resolve_model_target_file
|
|
11
|
+
from ...lang.java.resolve import resolve_java_import_to_file
|
|
12
|
+
from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
13
|
+
from ...context.java_rel import (
|
|
14
|
+
clean_java,
|
|
15
|
+
top_type_name,
|
|
16
|
+
java_fields_and_rels,
|
|
17
|
+
model_fields_from_extractor,
|
|
18
|
+
Rel,
|
|
19
|
+
)
|
|
20
|
+
from ...context.python_rel import python_collect_relationship_map
|
|
21
|
+
from ...db.cache import CacheManager
|
|
22
|
+
|
|
23
|
+
VIEWS = ["compact", "tree", "pretty", "flow", "db"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_cached_files(project_root: Path) -> list[Path]:
|
|
27
|
+
with CacheManager(project_root) as cache:
|
|
28
|
+
if cache.needs_rescan():
|
|
29
|
+
cache.scan_project(verbose=False)
|
|
30
|
+
return [p.resolve() for p in cache.get_cached_files()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _fmt_fields(fields: list[tuple[str, str, bool]], indent: str = " ") -> list[str]:
|
|
34
|
+
out: list[str] = []
|
|
35
|
+
for n, t, pk in fields:
|
|
36
|
+
out.append(f"{indent}- {n}: {t}" + (" (PK)" if pk else ""))
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _rel_label(kind: str) -> str:
|
|
41
|
+
return {"OneToOne": "1:1", "OneToMany": "1:N", "ManyToMany": "N:M", "ManyToOne": "N:1"}.get(kind, kind)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _rel_meta(r: Rel) -> str:
|
|
45
|
+
parts: list[str] = []
|
|
46
|
+
if r.mapped_by:
|
|
47
|
+
parts.append(f"mappedBy={r.mapped_by}")
|
|
48
|
+
if r.join_table:
|
|
49
|
+
parts.append(f"joinTable={r.join_table}")
|
|
50
|
+
if r.join_columns:
|
|
51
|
+
parts.append("joinCols=" + ",".join(r.join_columns))
|
|
52
|
+
return (" [" + "; ".join(parts) + "]") if parts else ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _box(body: list[str]) -> list[str]:
|
|
56
|
+
w = max([len(x) for x in body] + [0])
|
|
57
|
+
top = "┌" + "─" * (w + 2) + "┐"
|
|
58
|
+
sep = "├" + "─" * (w + 2) + "┤"
|
|
59
|
+
bot = "└" + "─" * (w + 2) + "┘"
|
|
60
|
+
lines = [top, sep]
|
|
61
|
+
for x in body:
|
|
62
|
+
lines.append("│ " + x.ljust(w) + " │")
|
|
63
|
+
lines.append(bot)
|
|
64
|
+
return lines
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _default_outfile(root: Path, name: str) -> Path:
|
|
68
|
+
base = root / f"codeintel-modeltree-{name}.txt"
|
|
69
|
+
if not base.exists():
|
|
70
|
+
return base
|
|
71
|
+
i = 2
|
|
72
|
+
while True:
|
|
73
|
+
p = root / f"codeintel-modeltree-{name}-{i}.txt"
|
|
74
|
+
if not p.exists():
|
|
75
|
+
return p
|
|
76
|
+
i += 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _open_in_editor(path: Path) -> None:
|
|
80
|
+
p = path.resolve()
|
|
81
|
+
try:
|
|
82
|
+
from ...core.opener import open_file
|
|
83
|
+
|
|
84
|
+
open_file(p)
|
|
85
|
+
return
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
try:
|
|
89
|
+
subprocess.Popen(["code", str(p)], close_fds=True)
|
|
90
|
+
return
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
try:
|
|
94
|
+
if sys.platform.startswith("win"):
|
|
95
|
+
subprocess.Popen(["cmd", "/c", "start", "", str(p)], close_fds=True)
|
|
96
|
+
elif sys.platform == "darwin":
|
|
97
|
+
subprocess.Popen(["open", str(p)], close_fds=True)
|
|
98
|
+
else:
|
|
99
|
+
subprocess.Popen(["xdg-open", str(p)], close_fds=True)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_domain_model_path(p: Path) -> bool:
|
|
105
|
+
parts = {x.lower() for x in p.parts}
|
|
106
|
+
return bool(parts & {"model", "models", "entity", "entities", "domain"})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _java_model_annotation_hint(path: Path) -> bool:
|
|
110
|
+
try:
|
|
111
|
+
t = path.read_text(encoding="utf-8", errors="ignore")
|
|
112
|
+
except Exception:
|
|
113
|
+
return False
|
|
114
|
+
return any(x in t for x in ("@Entity", "@Table", "@Embeddable", "@MappedSuperclass"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _has_class_definition(path: Path) -> bool:
|
|
118
|
+
try:
|
|
119
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
120
|
+
return "class " in text and len(text.strip()) > 50
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_java_model_file(p: Path) -> bool:
|
|
126
|
+
if p.suffix.lower() != ".java":
|
|
127
|
+
return False
|
|
128
|
+
if p.name == "__init__.py":
|
|
129
|
+
return False
|
|
130
|
+
if not (_is_domain_model_path(p) or _java_model_annotation_hint(p)):
|
|
131
|
+
return False
|
|
132
|
+
return _has_class_definition(p)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_python_model_file(p: Path) -> bool:
|
|
136
|
+
if p.suffix.lower() != ".py":
|
|
137
|
+
return False
|
|
138
|
+
if p.name in {"__init__.py", "models.py"}:
|
|
139
|
+
return False
|
|
140
|
+
if not _is_domain_model_path(p):
|
|
141
|
+
return False
|
|
142
|
+
return _has_class_definition(p)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _choose_view_interactive(default: str = "tree") -> str:
|
|
146
|
+
typer.echo("Select view:")
|
|
147
|
+
for i, v in enumerate(VIEWS, 1):
|
|
148
|
+
typer.echo(f"{i}. {v}")
|
|
149
|
+
pick = typer.prompt("Enter number", type=int, default=VIEWS.index(default) + 1)
|
|
150
|
+
if 1 <= pick <= len(VIEWS):
|
|
151
|
+
return VIEWS[pick - 1]
|
|
152
|
+
return default
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _layered_related(
|
|
156
|
+
graph: dict[Path, set[Path]],
|
|
157
|
+
start: Path,
|
|
158
|
+
depth: int,
|
|
159
|
+
include_reverse: bool,
|
|
160
|
+
hubs: set[Path],
|
|
161
|
+
) -> list[list[Path]]:
|
|
162
|
+
adj: dict[Path, set[Path]] = {k: set(v) for k, v in graph.items()}
|
|
163
|
+
if include_reverse:
|
|
164
|
+
rev: dict[Path, set[Path]] = {}
|
|
165
|
+
for src, deps in adj.items():
|
|
166
|
+
for dst in deps:
|
|
167
|
+
rev.setdefault(dst, set()).add(src)
|
|
168
|
+
for node, incoming in rev.items():
|
|
169
|
+
adj.setdefault(node, set()).update(incoming)
|
|
170
|
+
|
|
171
|
+
visited: set[Path] = {start}
|
|
172
|
+
q: deque[tuple[Path, int]] = deque([(start, 0)])
|
|
173
|
+
by_depth: dict[int, list[Path]] = {}
|
|
174
|
+
|
|
175
|
+
while q:
|
|
176
|
+
node, d = q.popleft()
|
|
177
|
+
if d == depth:
|
|
178
|
+
continue
|
|
179
|
+
for nxt in adj.get(node, set()):
|
|
180
|
+
if nxt in visited or nxt in hubs:
|
|
181
|
+
continue
|
|
182
|
+
visited.add(nxt)
|
|
183
|
+
nd = d + 1
|
|
184
|
+
by_depth.setdefault(nd, []).append(nxt)
|
|
185
|
+
q.append((nxt, nd))
|
|
186
|
+
|
|
187
|
+
return [sorted(by_depth.get(i, [])) for i in range(1, depth + 1)]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _file_for_java_class(name: str, scope: list[Path], root: Path) -> Path | None:
|
|
191
|
+
if not name:
|
|
192
|
+
return None
|
|
193
|
+
exact = [p for p in scope if p.suffix.lower() == ".java" and p.stem == name]
|
|
194
|
+
if exact:
|
|
195
|
+
return exact[0].resolve()
|
|
196
|
+
try:
|
|
197
|
+
p = resolve_java_import_to_file(name, root)
|
|
198
|
+
if p and p.exists():
|
|
199
|
+
return p.resolve()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _java_model_scope_from_files(files: list[Path]) -> list[Path]:
|
|
206
|
+
return [p.resolve() for p in files if _is_java_model_file(p)]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _collect_java_relationship_map(
|
|
210
|
+
target: Path,
|
|
211
|
+
project_root: Path,
|
|
212
|
+
depth: int,
|
|
213
|
+
forward_only: bool,
|
|
214
|
+
include_hubs: bool,
|
|
215
|
+
):
|
|
216
|
+
all_files = _get_cached_files(project_root)
|
|
217
|
+
|
|
218
|
+
graph, dep_counts = build_graph_with_counts(all_files, project_root, use_sqlite_cache=True)
|
|
219
|
+
|
|
220
|
+
hubs: set[Path] = set()
|
|
221
|
+
if not include_hubs:
|
|
222
|
+
hubs = get_hub_files_by_ratio(dep_counts, len(all_files), 0.5)
|
|
223
|
+
|
|
224
|
+
layers = _layered_related(
|
|
225
|
+
graph=graph,
|
|
226
|
+
start=target.resolve(),
|
|
227
|
+
depth=depth,
|
|
228
|
+
include_reverse=not forward_only,
|
|
229
|
+
hubs=hubs,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
model_files = set(_java_model_scope_from_files(all_files))
|
|
233
|
+
scope: list[Path] = []
|
|
234
|
+
for layer in layers:
|
|
235
|
+
for p in layer:
|
|
236
|
+
rp = p.resolve()
|
|
237
|
+
if rp in model_files:
|
|
238
|
+
scope.append(rp)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
text = clean_java(target.read_text(encoding="utf-8", errors="ignore"))
|
|
242
|
+
except Exception:
|
|
243
|
+
text = ""
|
|
244
|
+
|
|
245
|
+
model_name = top_type_name(text, target.stem)
|
|
246
|
+
main_fields, main_rels = java_fields_and_rels(target)
|
|
247
|
+
|
|
248
|
+
fields_by_entity: dict[str, list[tuple[str, str, bool]]] = {}
|
|
249
|
+
for r in main_rels:
|
|
250
|
+
if not r or not r.target:
|
|
251
|
+
continue
|
|
252
|
+
p = _file_for_java_class(r.target, scope + [target], project_root)
|
|
253
|
+
if p and p.exists():
|
|
254
|
+
flds = model_fields_from_extractor(p, project_root)
|
|
255
|
+
if not flds:
|
|
256
|
+
flds, _ = java_fields_and_rels(p)
|
|
257
|
+
if flds:
|
|
258
|
+
fields_by_entity[r.target] = flds
|
|
259
|
+
|
|
260
|
+
reverse: list[tuple[str, Rel]] = []
|
|
261
|
+
for p in model_files:
|
|
262
|
+
if p.resolve() == target.resolve():
|
|
263
|
+
continue
|
|
264
|
+
try:
|
|
265
|
+
t = clean_java(p.read_text(encoding="utf-8", errors="ignore"))
|
|
266
|
+
except Exception:
|
|
267
|
+
continue
|
|
268
|
+
src_name = top_type_name(t, p.stem)
|
|
269
|
+
_, rels = java_fields_and_rels(p)
|
|
270
|
+
for rr in rels:
|
|
271
|
+
if rr and rr.target == model_name:
|
|
272
|
+
reverse.append((src_name, rr))
|
|
273
|
+
if src_name not in fields_by_entity:
|
|
274
|
+
flds = model_fields_from_extractor(p, project_root)
|
|
275
|
+
if not flds:
|
|
276
|
+
flds, _ = java_fields_and_rels(p)
|
|
277
|
+
if flds:
|
|
278
|
+
fields_by_entity[src_name] = flds
|
|
279
|
+
|
|
280
|
+
reverse.sort(key=lambda x: (x[0].lower(), x[1].kind, x[1].field.lower()))
|
|
281
|
+
return model_name, main_fields, main_rels, fields_by_entity, reverse
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _view_compact(model: str, fields, rels, reverse) -> list[str]:
|
|
285
|
+
lines: list[str] = [model]
|
|
286
|
+
lines.extend(_fmt_fields(fields))
|
|
287
|
+
lines.append("")
|
|
288
|
+
lines.append("Direct relationships:")
|
|
289
|
+
if not rels:
|
|
290
|
+
lines.append(" (none)")
|
|
291
|
+
else:
|
|
292
|
+
for r in rels:
|
|
293
|
+
lines.append(f" - @{r.kind} -> {r.target} (field: {r.field}){_rel_meta(r)}")
|
|
294
|
+
lines.append("")
|
|
295
|
+
lines.append("Reverse relationships:")
|
|
296
|
+
if not reverse:
|
|
297
|
+
lines.append(" (none)")
|
|
298
|
+
else:
|
|
299
|
+
for src, r in reverse:
|
|
300
|
+
lines.append(f" - {src} -> {model} (@{r.kind} via {r.field}){_rel_meta(r)}")
|
|
301
|
+
return lines
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _view_tree(model: str, fields, rels, fields_by, reverse) -> list[str]:
|
|
305
|
+
lines: list[str] = [model, "├─ Fields"]
|
|
306
|
+
for i, (n, t, pk) in enumerate(fields):
|
|
307
|
+
end = "└─" if i == len(fields) - 1 else "├─"
|
|
308
|
+
lines.append(f"│ {end} {n}: {t}" + (" (PK)" if pk else ""))
|
|
309
|
+
lines.append("│")
|
|
310
|
+
|
|
311
|
+
groups: dict[str, list] = {}
|
|
312
|
+
for r in rels:
|
|
313
|
+
groups.setdefault(r.kind, []).append(r)
|
|
314
|
+
|
|
315
|
+
order = ["OneToOne", "OneToMany", "ManyToMany", "ManyToOne"]
|
|
316
|
+
kinds = [k for k in order if k in groups]
|
|
317
|
+
|
|
318
|
+
if not kinds:
|
|
319
|
+
lines.append("└─ Relationships")
|
|
320
|
+
lines.append(" └─ (none)")
|
|
321
|
+
else:
|
|
322
|
+
for ki, kind in enumerate(kinds):
|
|
323
|
+
last_kind = ki == len(kinds) - 1
|
|
324
|
+
kbranch = "└─" if last_kind else "├─"
|
|
325
|
+
kpad = " " if last_kind else "│ "
|
|
326
|
+
lines.append(f"{kbranch} @{kind}")
|
|
327
|
+
rs = groups[kind]
|
|
328
|
+
for ri, r in enumerate(rs):
|
|
329
|
+
rlast = ri == len(rs) - 1
|
|
330
|
+
rbranch = "└─" if rlast else "├─"
|
|
331
|
+
suffix = "[]" if kind in {"OneToMany", "ManyToMany"} else ""
|
|
332
|
+
lines.append(f"{kpad}{rbranch} {r.target}{suffix} (field: {r.field}){_rel_meta(r)}")
|
|
333
|
+
flds = fields_by.get(r.target, [])
|
|
334
|
+
for fi, (fn, ft, _pk) in enumerate(flds):
|
|
335
|
+
flast = fi == len(flds) - 1
|
|
336
|
+
fbranch = "└─" if flast else "├─"
|
|
337
|
+
lines.append(f"{kpad}{' ' if rlast else '│ '} {fbranch} {fn}: {ft}")
|
|
338
|
+
|
|
339
|
+
lines.append("")
|
|
340
|
+
lines.append("Reverse References:")
|
|
341
|
+
if not reverse:
|
|
342
|
+
lines.append(" (none)")
|
|
343
|
+
else:
|
|
344
|
+
for src, r in reverse:
|
|
345
|
+
lines.append(f" ↑ {src} (@{r.kind} via {r.field}){_rel_meta(r)}")
|
|
346
|
+
return lines
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _view_pretty(model: str, fields, rels, fields_by, reverse) -> list[str]:
|
|
350
|
+
lines: list[str] = [model, "━" * 80]
|
|
351
|
+
for n, t, pk in fields:
|
|
352
|
+
icon = "🔑" if pk else "•"
|
|
353
|
+
lines.append(f" {icon} {n}: {t}")
|
|
354
|
+
|
|
355
|
+
lines.append("")
|
|
356
|
+
lines.append("Relationships")
|
|
357
|
+
lines.append("━" * 80)
|
|
358
|
+
|
|
359
|
+
if not rels:
|
|
360
|
+
lines.append(" (none)")
|
|
361
|
+
else:
|
|
362
|
+
for r in rels:
|
|
363
|
+
tgt_fields = fields_by.get(r.target, [])
|
|
364
|
+
body: list[str] = [f"{_rel_label(r.kind)} {model} -> {r.target} (field: {r.field}){_rel_meta(r)}", ""]
|
|
365
|
+
if tgt_fields:
|
|
366
|
+
for fn, ft, pk in tgt_fields:
|
|
367
|
+
icon = "🔑" if pk else "•"
|
|
368
|
+
body.append(f"{icon} {fn}: {ft}")
|
|
369
|
+
else:
|
|
370
|
+
body.append("(target fields not found)")
|
|
371
|
+
lines.extend(_box(body))
|
|
372
|
+
lines.append("")
|
|
373
|
+
|
|
374
|
+
lines.append("Reverse References")
|
|
375
|
+
lines.append("━" * 80)
|
|
376
|
+
|
|
377
|
+
if not reverse:
|
|
378
|
+
lines.append(" (none)")
|
|
379
|
+
else:
|
|
380
|
+
for src, rr in reverse:
|
|
381
|
+
lines.append(f" ↑ {src} (@{rr.kind} via {rr.field}){_rel_meta(rr)}")
|
|
382
|
+
|
|
383
|
+
return lines
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _view_flow(model: str, fields, rels, reverse) -> list[str]:
|
|
387
|
+
lines: list[str] = [model, ""]
|
|
388
|
+
for n, t, pk in fields:
|
|
389
|
+
lines.append(f" - {n}: {t}" + (" (PK)" if pk else ""))
|
|
390
|
+
lines.append("")
|
|
391
|
+
lines.append("Outgoing:")
|
|
392
|
+
if not rels:
|
|
393
|
+
lines.append(" (none)")
|
|
394
|
+
else:
|
|
395
|
+
for r in rels:
|
|
396
|
+
lines.append(f" {model} --@{r.kind}--> {r.target} (field: {r.field}){_rel_meta(r)}")
|
|
397
|
+
lines.append("")
|
|
398
|
+
lines.append("Incoming:")
|
|
399
|
+
if not reverse:
|
|
400
|
+
lines.append(" (none)")
|
|
401
|
+
else:
|
|
402
|
+
for src, r in reverse:
|
|
403
|
+
lines.append(f" {src} --@{r.kind}--> {model} (field: {r.field}){_rel_meta(r)}")
|
|
404
|
+
return lines
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _view_db(model: str, fields, rels, reverse) -> list[str]:
|
|
408
|
+
lines: list[str] = [f"TABLE: {model.lower()}"]
|
|
409
|
+
for n, t, pk in fields:
|
|
410
|
+
lines.append(f" - {n}: {t}" + (" (PK)" if pk else ""))
|
|
411
|
+
lines.append("")
|
|
412
|
+
lines.append("Relationships:")
|
|
413
|
+
if not rels and not reverse:
|
|
414
|
+
lines.append(" (none)")
|
|
415
|
+
return lines
|
|
416
|
+
for r in rels:
|
|
417
|
+
lines.append(f" - @{r.kind} -> {r.target} (field: {r.field}){_rel_meta(r)}")
|
|
418
|
+
for src, r in reverse:
|
|
419
|
+
lines.append(f" - {src} -> {model} (@{r.kind} via {r.field}){_rel_meta(r)}")
|
|
420
|
+
return lines
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def register_modeltree(app: typer.Typer) -> None:
|
|
424
|
+
@app.command(help="Model relationship tree")
|
|
425
|
+
def modeltree(
|
|
426
|
+
model: str = typer.Argument(...),
|
|
427
|
+
root: str = typer.Option("."),
|
|
428
|
+
depth: int = typer.Option(2, "--depth", "-d"),
|
|
429
|
+
forward_only: bool = typer.Option(False, "--forward-only"),
|
|
430
|
+
include_hubs: bool = typer.Option(False, "--include-hubs"),
|
|
431
|
+
view: str | None = typer.Option(None, "--view", "-v"),
|
|
432
|
+
out: Path | None = typer.Option(None, "--out"),
|
|
433
|
+
) -> None:
|
|
434
|
+
target, project_root = _resolve_model_target_file(model, root=root)
|
|
435
|
+
|
|
436
|
+
if view is None:
|
|
437
|
+
view = _choose_view_interactive(default="tree")
|
|
438
|
+
if view not in VIEWS:
|
|
439
|
+
raise typer.BadParameter(f"view must be one of: {', '.join(VIEWS)}")
|
|
440
|
+
|
|
441
|
+
create_file = typer.confirm("Create .txt file?", default=True)
|
|
442
|
+
|
|
443
|
+
if target.suffix.lower() == ".java":
|
|
444
|
+
if not _is_java_model_file(target):
|
|
445
|
+
raise InvalidPathError(message="Not a model/entity file", path=target)
|
|
446
|
+
model_name, fields, rels, fields_by, reverse = _collect_java_relationship_map(
|
|
447
|
+
target, project_root, depth, forward_only, include_hubs
|
|
448
|
+
)
|
|
449
|
+
elif target.suffix.lower() == ".py":
|
|
450
|
+
if not _is_python_model_file(target):
|
|
451
|
+
raise InvalidPathError(message="Not a model/entity file", path=target)
|
|
452
|
+
model_name, fields, rels, fields_by, reverse = python_collect_relationship_map(target, project_root)
|
|
453
|
+
else:
|
|
454
|
+
raise InvalidPathError(message="Unsupported model file type", path=target)
|
|
455
|
+
|
|
456
|
+
if view == "compact":
|
|
457
|
+
lines = _view_compact(model_name, fields, rels, reverse)
|
|
458
|
+
elif view == "tree":
|
|
459
|
+
lines = _view_tree(model_name, fields, rels, fields_by, reverse)
|
|
460
|
+
elif view == "pretty":
|
|
461
|
+
lines = _view_pretty(model_name, fields, rels, fields_by, reverse)
|
|
462
|
+
elif view == "flow":
|
|
463
|
+
lines = _view_flow(model_name, fields, rels, reverse)
|
|
464
|
+
else:
|
|
465
|
+
lines = _view_db(model_name, fields, rels, reverse)
|
|
466
|
+
|
|
467
|
+
if not create_file:
|
|
468
|
+
typer.echo("")
|
|
469
|
+
for ln in lines:
|
|
470
|
+
typer.echo(ln)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
out_path = out.resolve() if out else _default_outfile(project_root, model_name)
|
|
474
|
+
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
475
|
+
typer.echo(out_path.as_posix())
|
|
476
|
+
_open_in_editor(out_path)
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ...parser.resolve import resolve_module_to_file
|
|
7
|
+
from ...errors import InvalidPathError
|
|
8
|
+
from ...core.project import find_project_root
|
|
9
|
+
from ...core.resolve_target import resolve_target_file
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_resolve(app: typer.Typer) -> None:
|
|
13
|
+
@app.command(help="Resolve a module path or filename to a project file.")
|
|
14
|
+
def resolve(
|
|
15
|
+
target: str = typer.Argument(..., help="Module path or filename"),
|
|
16
|
+
):
|
|
17
|
+
root = find_project_root(Path.cwd())
|
|
18
|
+
|
|
19
|
+
mod_hit = resolve_module_to_file(target, root)
|
|
20
|
+
if mod_hit:
|
|
21
|
+
typer.echo(mod_hit.resolve().relative_to(root))
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
file_hit, root = resolve_target_file(target)
|
|
25
|
+
if file_hit.exists():
|
|
26
|
+
typer.echo(file_hit.resolve().relative_to(root))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
raise InvalidPathError(message="Not found", path=Path(target))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import time
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ...errors import InvalidPathError
|
|
7
|
+
from ...core.project import find_project_root
|
|
8
|
+
from ...db.cache import get_cache
|
|
9
|
+
|
|
10
|
+
def validate_folder(path: str) -> Path:
|
|
11
|
+
folder = Path(path).resolve()
|
|
12
|
+
if not folder.exists() or not folder.is_dir():
|
|
13
|
+
raise InvalidPathError(message="Invalid folder", path=folder)
|
|
14
|
+
return folder
|
|
15
|
+
|
|
16
|
+
def register_scan(app: typer.Typer) -> None:
|
|
17
|
+
@app.command(help="Scan and index project files")
|
|
18
|
+
def scan(
|
|
19
|
+
path: str = typer.Argument(".", help="Folder to scan"),
|
|
20
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show progress"),
|
|
21
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force rescan all files"),
|
|
22
|
+
):
|
|
23
|
+
folder = validate_folder(path)
|
|
24
|
+
project_root = find_project_root(folder)
|
|
25
|
+
|
|
26
|
+
start_time = time.perf_counter()
|
|
27
|
+
|
|
28
|
+
with get_cache(project_root) as cache:
|
|
29
|
+
if not force and not cache.needs_rescan():
|
|
30
|
+
files = cache.get_cached_files()
|
|
31
|
+
elapsed = time.perf_counter() - start_time
|
|
32
|
+
py_count = sum(1 for f in files if f.suffix == ".py")
|
|
33
|
+
java_count = sum(1 for f in files if f.suffix == ".java")
|
|
34
|
+
typer.echo(f"✓ Cache up to date")
|
|
35
|
+
typer.echo(f"Cached {len(files)} files (py={py_count}, java={java_count}) in {elapsed:.3f}s")
|
|
36
|
+
typer.echo(f"Database: {cache.db_path}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if verbose:
|
|
40
|
+
typer.echo(f"Scanning: {project_root}")
|
|
41
|
+
|
|
42
|
+
scanned = cache.scan_project(verbose=verbose)
|
|
43
|
+
files = cache.get_cached_files()
|
|
44
|
+
elapsed = time.perf_counter() - start_time
|
|
45
|
+
|
|
46
|
+
py_count = sum(1 for f in files if f.suffix == ".py")
|
|
47
|
+
java_count = sum(1 for f in files if f.suffix == ".java")
|
|
48
|
+
|
|
49
|
+
typer.echo(f"✓ Indexed {scanned} files")
|
|
50
|
+
typer.echo(f"Total {len(files)} files (py={py_count}, java={java_count}) in {elapsed:.3f}s")
|
|
51
|
+
typer.echo(f"Database: {cache.db_path}")
|