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.
Files changed (68) hide show
  1. aja_codeintel-0.1.0.dist-info/METADATA +436 -0
  2. aja_codeintel-0.1.0.dist-info/RECORD +68 -0
  3. aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
  4. aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
  5. aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
  7. codeintel_cli/__init__.py +1 -0
  8. codeintel_cli/__main__.py +4 -0
  9. codeintel_cli/cli.py +41 -0
  10. codeintel_cli/commands/__init__.py +1 -0
  11. codeintel_cli/commands/graph/__init__.py +18 -0
  12. codeintel_cli/commands/graph/deps_cmd.py +35 -0
  13. codeintel_cli/commands/graph/related_cmd.py +121 -0
  14. codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
  15. codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
  16. codeintel_cli/commands/nav/__init__.py +12 -0
  17. codeintel_cli/commands/nav/copy_cmd.py +101 -0
  18. codeintel_cli/commands/nav/open_cmd.py +18 -0
  19. codeintel_cli/commands/nav/where_cmd.py +21 -0
  20. codeintel_cli/commands/project/__init__.py +26 -0
  21. codeintel_cli/commands/project/context_cmd.py +326 -0
  22. codeintel_cli/commands/project/folder_cmd.py +51 -0
  23. codeintel_cli/commands/project/imports_cmd.py +90 -0
  24. codeintel_cli/commands/project/models_cmd.py +98 -0
  25. codeintel_cli/commands/project/modeltree_cmd.py +476 -0
  26. codeintel_cli/commands/project/new.py +0 -0
  27. codeintel_cli/commands/project/resolve_cmd.py +29 -0
  28. codeintel_cli/commands/project/scan_cmd.py +51 -0
  29. codeintel_cli/commands/project/servicemap_cmd.py +180 -0
  30. codeintel_cli/commands/project/tree_cmd.py +203 -0
  31. codeintel_cli/commands/project/version_cmd.py +14 -0
  32. codeintel_cli/context/java_context.py +180 -0
  33. codeintel_cli/context/java_rel.py +299 -0
  34. codeintel_cli/context/java_service.py +291 -0
  35. codeintel_cli/context/python_context.py +91 -0
  36. codeintel_cli/context/python_rel.py +251 -0
  37. codeintel_cli/context/python_service.py +205 -0
  38. codeintel_cli/core/fuzzy.py +72 -0
  39. codeintel_cli/core/opener.py +37 -0
  40. codeintel_cli/core/project.py +34 -0
  41. codeintel_cli/core/resolve_folder.py +68 -0
  42. codeintel_cli/core/resolve_model_target.py +92 -0
  43. codeintel_cli/core/resolve_target.py +53 -0
  44. codeintel_cli/core/timing.py +13 -0
  45. codeintel_cli/core/where.py +77 -0
  46. codeintel_cli/db/__init__.py +7 -0
  47. codeintel_cli/db/cache.py +224 -0
  48. codeintel_cli/db/operations.py +333 -0
  49. codeintel_cli/db/schema.py +102 -0
  50. codeintel_cli/errors.py +78 -0
  51. codeintel_cli/graph/__init__.py +1 -0
  52. codeintel_cli/graph/builder.py +149 -0
  53. codeintel_cli/graph/query.py +30 -0
  54. codeintel_cli/graph/traverse.py +49 -0
  55. codeintel_cli/lang/__init__.py +0 -0
  56. codeintel_cli/lang/java/__init__.py +0 -0
  57. codeintel_cli/lang/java/engine.py +18 -0
  58. codeintel_cli/lang/java/models.py +105 -0
  59. codeintel_cli/lang/java/resolve.py +49 -0
  60. codeintel_cli/lang/python/__init__.py +0 -0
  61. codeintel_cli/lang/python/engine.py +8 -0
  62. codeintel_cli/lang/python/models.py +86 -0
  63. codeintel_cli/lang/router.py +24 -0
  64. codeintel_cli/parser/imports.py +26 -0
  65. codeintel_cli/parser/resolve.py +49 -0
  66. codeintel_cli/parser/symbols.py +92 -0
  67. codeintel_cli/scanner/__init__.py +0 -0
  68. codeintel_cli/scanner/scanner.py +41 -0
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import typer
4
+
5
+ from ..errors import InvalidPathError
6
+ from .project import find_project_root
7
+ from .fuzzy import rank_paths, fuzzy_is_confident
8
+ from ..scanner.scanner import find_all_supported_files
9
+
10
+ def _is_domain_model_path(p: Path) -> bool:
11
+ parts = {x.lower() for x in p.parts}
12
+ return bool(parts & {"model", "models", "entity", "entities", "domain"})
13
+
14
+ def _java_model_annotation_hint(path: Path) -> bool:
15
+ try:
16
+ t = path.read_text(encoding="utf-8", errors="ignore")
17
+ except Exception:
18
+ return False
19
+ return any(x in t for x in ("@Entity", "@Table", "@Embeddable", "@MappedSuperclass"))
20
+
21
+ def _has_class_definition(path: Path) -> bool:
22
+ try:
23
+ text = path.read_text(encoding="utf-8", errors="ignore")
24
+ return "class " in text and len(text.strip()) > 50
25
+ except Exception:
26
+ return False
27
+
28
+ def _is_java_model_file(p: Path) -> bool:
29
+ if p.suffix.lower() != ".java":
30
+ return False
31
+ if p.name == "__init__.py":
32
+ return False
33
+ if not (_is_domain_model_path(p) or _java_model_annotation_hint(p)):
34
+ return False
35
+ return _has_class_definition(p)
36
+
37
+ def _is_python_model_file(p: Path) -> bool:
38
+ if p.suffix.lower() != ".py":
39
+ return False
40
+ if p.name in {"__init__.py", "models.py"}:
41
+ return False
42
+ if not _is_domain_model_path(p):
43
+ return False
44
+ return _has_class_definition(p)
45
+
46
+ def resolve_model_target_file(query: str, root: str = ".", top: int = 3) -> tuple[Path, Path]:
47
+ root_path = Path(root).resolve()
48
+ project_root = find_project_root(root_path)
49
+
50
+ q = Path(query)
51
+ if q.exists() and q.is_file():
52
+ return q.resolve(), project_root
53
+
54
+ files = find_all_supported_files(project_root)
55
+ model_files: list[Path] = [p for p in files if _is_java_model_file(p) or _is_python_model_file(p)]
56
+
57
+ if not model_files:
58
+ raise InvalidPathError(message="No model/entity files found", path=project_root)
59
+
60
+ qstem_lower = Path(query).stem.lower()
61
+ exact_matches = [f for f in model_files if f.stem.lower() == qstem_lower]
62
+
63
+ if len(exact_matches) == 1:
64
+ return exact_matches[0].resolve(), project_root
65
+
66
+ if len(exact_matches) > 1:
67
+ exact_matches = exact_matches[:top]
68
+
69
+ ranked = rank_paths(query, exact_matches if exact_matches else model_files, project_root, top=top)
70
+
71
+ if not ranked:
72
+ raise InvalidPathError(message="Model not found", path=Path(query))
73
+
74
+ if fuzzy_is_confident(ranked):
75
+ top_score = ranked[0][1]
76
+ ties = [x for x in ranked if abs(x[1] - top_score) < 0.02]
77
+ if len(ties) == 1:
78
+ return ranked[0][0].resolve(), project_root
79
+
80
+ typer.echo("Select model:")
81
+ for i, (p, score) in enumerate(ranked, 1):
82
+ try:
83
+ rel = p.relative_to(project_root)
84
+ except Exception:
85
+ rel = p
86
+ typer.echo(f"{i}. {rel.as_posix()} score={score:.2f}")
87
+
88
+ choice = typer.prompt("Select number (0=cancel)", type=int, default=0)
89
+ if choice == 0 or choice < 1 or choice > len(ranked):
90
+ raise typer.Exit()
91
+
92
+ return ranked[choice - 1][0].resolve(), project_root
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import typer
4
+
5
+ from ..errors import InvalidPathError
6
+ from .project import find_project_root
7
+ from .fuzzy import rank_paths, fuzzy_is_confident
8
+ from ..scanner.scanner import find_all_supported_files
9
+
10
+
11
+ def resolve_target_file(query: str, root: str = ".", top: int = 3) -> tuple[Path, Path]:
12
+ root_path = Path(root).resolve()
13
+ project_root = find_project_root(root_path)
14
+
15
+ q = Path(query)
16
+ if q.exists() and q.is_file():
17
+ return q.resolve(), project_root
18
+
19
+ all_files = find_all_supported_files(project_root)
20
+
21
+ qstem_lower = Path(query).stem.lower()
22
+ exact_matches = [f for f in all_files if f.stem.lower() == qstem_lower and f.name != "__init__.py"]
23
+
24
+ if len(exact_matches) == 1:
25
+ return exact_matches[0].resolve(), project_root
26
+
27
+ if len(exact_matches) > 1:
28
+ exact_matches = exact_matches[:top]
29
+
30
+ ranked = rank_paths(query, all_files if not exact_matches else exact_matches, project_root, top=top)
31
+
32
+ if not ranked:
33
+ raise InvalidPathError(message="File not found", path=Path(query))
34
+
35
+ if fuzzy_is_confident(ranked):
36
+ top_score = ranked[0][1]
37
+ ties = [x for x in ranked if abs(x[1] - top_score) < 0.02]
38
+ if len(ties) == 1:
39
+ return ranked[0][0].resolve(), project_root
40
+
41
+ typer.echo("Did you mean:")
42
+ for i, (p, score) in enumerate(ranked, 1):
43
+ try:
44
+ rel = p.relative_to(project_root)
45
+ except Exception:
46
+ rel = p
47
+ typer.echo(f"{i}. {rel.as_posix()} score={score:.2f}")
48
+
49
+ choice = typer.prompt("Select number (0=cancel)", type=int, default=0)
50
+ if choice == 0 or choice < 1 or choice > len(ranked):
51
+ raise typer.Exit()
52
+
53
+ return ranked[choice - 1][0].resolve(), project_root
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import typer
5
+
6
+
7
+ def start_timer() -> float:
8
+ return time.perf_counter()
9
+
10
+
11
+ def end_timer(t0: float) -> None:
12
+ dt = time.perf_counter() - t0
13
+ typer.echo(f"Finished in {dt:.3f}s")
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def _py_file_to_module(path: Path, root: Path) -> str | None:
7
+ if path.suffix.lower() != ".py":
8
+ return None
9
+ try:
10
+ rel = path.resolve().relative_to(root.resolve())
11
+ except Exception:
12
+ return None
13
+ parts = list(rel.parts)
14
+ if not parts:
15
+ return None
16
+ if parts[-1] == "__init__.py":
17
+ parts = parts[:-1]
18
+ else:
19
+ parts[-1] = Path(parts[-1]).stem
20
+ if not parts:
21
+ return None
22
+ return ".".join(parts)
23
+
24
+
25
+ def _java_file_to_module(path: Path, root: Path) -> str | None:
26
+ if path.suffix.lower() != ".java":
27
+ return None
28
+
29
+ p = path.resolve()
30
+ try:
31
+ rel = p.relative_to(root.resolve())
32
+ except Exception:
33
+ rel = p
34
+
35
+ parts = list(rel.parts)
36
+ if not parts:
37
+ return None
38
+
39
+ low = [x.lower() for x in parts]
40
+
41
+ def build_from(start_idx: int) -> str | None:
42
+ if start_idx >= len(parts):
43
+ return None
44
+ segs = parts[start_idx:]
45
+ if not segs:
46
+ return None
47
+ last = Path(segs[-1]).stem
48
+ segs = list(segs[:-1]) + [last]
49
+ segs = [s for s in segs if s and s not in (".", "..")]
50
+ if not segs:
51
+ return None
52
+ return ".".join(segs)
53
+
54
+ if "src" in low:
55
+ i = low.index("src")
56
+ if i + 3 < len(parts) and low[i + 1] in ("main", "test") and low[i + 2] == "java":
57
+ out = build_from(i + 3)
58
+ if out:
59
+ return out
60
+
61
+ if "java" in low:
62
+ i = low.index("java")
63
+ out = build_from(i + 1)
64
+ if out:
65
+ return out
66
+
67
+ if "com" in low:
68
+ i = low.index("com")
69
+ out = build_from(i)
70
+ if out:
71
+ return out
72
+
73
+ return None
74
+
75
+
76
+ def file_to_module(path: Path, root: Path) -> str | None:
77
+ return _py_file_to_module(path, root) or _java_file_to_module(path, root)
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .cache import get_cache, CacheManager
4
+ from .schema import init_db, get_db_path
5
+ from .operations import needs_rescan
6
+
7
+ __all__ = ["get_cache", "CacheManager", "init_db", "get_db_path", "needs_rescan"]
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .schema import init_db, get_db_path
8
+ from .operations import (
9
+ is_file_cached,
10
+ upsert_file,
11
+ clear_file_data,
12
+ insert_import,
13
+ insert_symbol,
14
+ insert_model,
15
+ get_all_files,
16
+ get_file_id,
17
+ get_file_imports,
18
+ get_file_symbols,
19
+ get_models,
20
+ needs_rescan,
21
+ get_imports_map_for_paths,
22
+ get_all_file_id_map,
23
+ get_unresolved_import_rows,
24
+ get_file_path_and_lang,
25
+ update_import_resolved,
26
+ )
27
+ from ..scanner.scanner import find_all_supported_files
28
+
29
+
30
+ def _encode_py_import(mod: str, level: int) -> str:
31
+ mod = (mod or "").strip()
32
+ level = int(level or 0)
33
+ if level <= 0:
34
+ return mod
35
+ return ("." * level) + mod
36
+
37
+
38
+ class CacheManager:
39
+ def __init__(self, project_root: Path):
40
+ self.project_root = project_root.resolve()
41
+ self.db_path = get_db_path(self.project_root)
42
+ self.conn: sqlite3.Connection | None = None
43
+
44
+ def __enter__(self):
45
+ self.conn = init_db(self.db_path)
46
+ return self
47
+
48
+ def __exit__(self, exc_type, exc_val, exc_tb):
49
+ if not self.conn:
50
+ return
51
+ if exc_type is None:
52
+ self.conn.commit()
53
+ else:
54
+ self.conn.rollback()
55
+ self.conn.close()
56
+
57
+ def needs_rescan(self) -> bool:
58
+ if not self.conn:
59
+ return True
60
+ return needs_rescan(self.conn, self.project_root)
61
+
62
+ def scan_project(self, verbose: bool = False) -> int:
63
+ if not self.conn:
64
+ raise RuntimeError("CacheManager not initialized")
65
+
66
+ files = find_all_supported_files(self.project_root)
67
+ scanned = 0
68
+
69
+ for file_path in files:
70
+ file_path = file_path.resolve()
71
+ if is_file_cached(self.conn, file_path):
72
+ continue
73
+
74
+ try:
75
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
76
+ except Exception:
77
+ content = ""
78
+
79
+ try:
80
+ rel_path = str(file_path.relative_to(self.project_root))
81
+ except Exception:
82
+ rel_path = str(file_path)
83
+
84
+ language = file_path.suffix.lstrip(".").lower()
85
+ file_id = upsert_file(self.conn, file_path, rel_path, language, content)
86
+
87
+ clear_file_data(self.conn, file_id)
88
+ self._index_file(file_id, file_path, content, language)
89
+
90
+ scanned += 1
91
+ if verbose and scanned % 10 == 0:
92
+ print(f"Indexed {scanned} files...")
93
+
94
+ self._resolve_import_edges()
95
+ self.conn.commit()
96
+ return scanned
97
+
98
+ def _index_file(self, file_id: int, file_path: Path, content: str, language: str) -> None:
99
+ if language == "py":
100
+ self._index_python_file(file_id, file_path, content)
101
+ elif language == "java":
102
+ self._index_java_file(file_id, file_path, content)
103
+
104
+ def _index_python_file(self, file_id: int, file_path: Path, content: str) -> None:
105
+ import ast
106
+
107
+ try:
108
+ tree = ast.parse(content)
109
+ except Exception:
110
+ return
111
+
112
+ for node in ast.walk(tree):
113
+ if isinstance(node, ast.Import):
114
+ for alias in node.names:
115
+ if alias.name:
116
+ insert_import(self.conn, file_id, alias.name, None)
117
+ elif isinstance(node, ast.ImportFrom):
118
+ mod = node.module or ""
119
+ lvl = int(getattr(node, "level", 0) or 0)
120
+ insert_import(self.conn, file_id, _encode_py_import(mod, lvl), None)
121
+ elif isinstance(node, ast.ClassDef):
122
+ insert_symbol(self.conn, file_id, node.name, "class", getattr(node, "lineno", 1))
123
+ elif isinstance(node, ast.FunctionDef):
124
+ insert_symbol(self.conn, file_id, node.name, "function", getattr(node, "lineno", 1))
125
+
126
+ try:
127
+ from ..lang.router import extract_models_for_file
128
+
129
+ models = extract_models_for_file(file_path, self.project_root)
130
+ for model in models or []:
131
+ fields = [(f.name, f.type, f.name.lower() == "id") for f in (model.fields or [])]
132
+ insert_model(self.conn, file_id, model.name, fields, [])
133
+ except Exception:
134
+ pass
135
+
136
+ def _index_java_file(self, file_id: int, file_path: Path, content: str) -> None:
137
+ import re
138
+
139
+ for m in re.finditer(r"import\s+([\w.]+)\s*;", content):
140
+ imp = (m.group(1) or "").strip()
141
+ if imp:
142
+ insert_import(self.conn, file_id, imp, None)
143
+
144
+ for m in re.finditer(r"\b(class|interface|enum|record)\s+([A-Za-z_]\w*)", content):
145
+ insert_symbol(self.conn, file_id, m.group(2), m.group(1), None)
146
+
147
+ try:
148
+ from ..context.java_rel import java_fields_and_rels
149
+
150
+ fields, rels = java_fields_and_rels(file_path)
151
+ if fields:
152
+ relationships = [{"kind": r.kind, "target": r.target, "field": r.field} for r in rels]
153
+ insert_model(self.conn, file_id, file_path.stem, fields, relationships)
154
+ except Exception:
155
+ pass
156
+
157
+ def _resolve_import_edges(self) -> None:
158
+ if not self.conn:
159
+ return
160
+
161
+ from ..parser.resolve import resolve_import_to_file
162
+ from ..lang.java.resolve import resolve_java_import_to_file
163
+
164
+ file_id_by_path = get_all_file_id_map(self.conn)
165
+ rows = get_unresolved_import_rows(self.conn)
166
+
167
+ for imp_row_id, src_file_id, imp in rows:
168
+ meta = get_file_path_and_lang(self.conn, src_file_id)
169
+ if not meta:
170
+ continue
171
+ src_path_str, lang = meta
172
+ src_path = Path(src_path_str)
173
+
174
+ target: Path | None = None
175
+
176
+ if lang == "py":
177
+ s = (imp or "").strip()
178
+ if not s:
179
+ continue
180
+ level = 0
181
+ while level < len(s) and s[level] == ".":
182
+ level += 1
183
+ mod = s[level:] if level > 0 else s
184
+ target = resolve_import_to_file(mod, level, src_path, self.project_root)
185
+
186
+ elif lang == "java":
187
+ s = (imp or "").strip()
188
+ if not s or s.endswith(".*"):
189
+ continue
190
+ target = resolve_java_import_to_file(s, self.project_root)
191
+
192
+ if not target:
193
+ continue
194
+
195
+ tid = file_id_by_path.get(str(target.resolve()))
196
+ if tid:
197
+ update_import_resolved(self.conn, imp_row_id, tid)
198
+
199
+ def get_cached_files(self) -> list[Path]:
200
+ if not self.conn:
201
+ return []
202
+ return get_all_files(self.conn)
203
+
204
+ def get_file_data(self, file_path: Path) -> dict[str, Any]:
205
+ if not self.conn:
206
+ return {}
207
+ file_id = get_file_id(self.conn, file_path)
208
+ if not file_id:
209
+ return {}
210
+ return {"imports": get_file_imports(self.conn, file_id), "symbols": get_file_symbols(self.conn, file_id)}
211
+
212
+ def get_all_models(self) -> list[dict]:
213
+ if not self.conn:
214
+ return []
215
+ return get_models(self.conn)
216
+
217
+ def get_imports_map(self, files: list[Path]) -> dict[str, list[str]]:
218
+ if not self.conn:
219
+ return {}
220
+ return get_imports_map_for_paths(self.conn, files)
221
+
222
+
223
+ def get_cache(project_root: Path) -> CacheManager:
224
+ return CacheManager(project_root)