aja-codeintel 0.1.0__tar.gz → 0.1.2__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.
Files changed (79) hide show
  1. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/PKG-INFO +1 -1
  2. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/PKG-INFO +1 -1
  3. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/SOURCES.txt +6 -0
  4. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/__init__.py +3 -2
  5. aja_codeintel-0.1.2/codeintel_cli/commands/project/endpoints_cmd.py +146 -0
  6. aja_codeintel-0.1.2/codeintel_cli/endpoints/__init__.py +1 -0
  7. aja_codeintel-0.1.2/codeintel_cli/endpoints/java_spring.py +153 -0
  8. aja_codeintel-0.1.2/codeintel_cli/endpoints/models.py +23 -0
  9. aja_codeintel-0.1.2/codeintel_cli/endpoints/python_web.py +99 -0
  10. aja_codeintel-0.1.2/codeintel_cli/endpoints/scan.py +102 -0
  11. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/pyproject.toml +1 -1
  12. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/LICENSE +0 -0
  13. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/README.md +0 -0
  14. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/dependency_links.txt +0 -0
  15. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/entry_points.txt +0 -0
  16. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/requires.txt +0 -0
  17. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/top_level.txt +0 -0
  18. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/__init__.py +0 -0
  19. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/__main__.py +0 -0
  20. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/cli.py +0 -0
  21. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/__init__.py +0 -0
  22. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/__init__.py +0 -0
  23. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/deps_cmd.py +0 -0
  24. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/related_cmd.py +0 -0
  25. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/relsymbols_cmd.py +0 -0
  26. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/reverse_related_cmd.py +0 -0
  27. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/__init__.py +0 -0
  28. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/copy_cmd.py +0 -0
  29. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/open_cmd.py +0 -0
  30. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/where_cmd.py +0 -0
  31. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/context_cmd.py +0 -0
  32. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/folder_cmd.py +0 -0
  33. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/imports_cmd.py +0 -0
  34. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/models_cmd.py +0 -0
  35. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/modeltree_cmd.py +0 -0
  36. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/new.py +0 -0
  37. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/resolve_cmd.py +0 -0
  38. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/scan_cmd.py +0 -0
  39. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/servicemap_cmd.py +0 -0
  40. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/tree_cmd.py +0 -0
  41. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/version_cmd.py +0 -0
  42. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_context.py +0 -0
  43. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_rel.py +0 -0
  44. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_service.py +0 -0
  45. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_context.py +0 -0
  46. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_rel.py +0 -0
  47. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_service.py +0 -0
  48. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/fuzzy.py +0 -0
  49. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/opener.py +0 -0
  50. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/project.py +0 -0
  51. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_folder.py +0 -0
  52. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_model_target.py +0 -0
  53. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_target.py +0 -0
  54. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/timing.py +0 -0
  55. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/where.py +0 -0
  56. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/__init__.py +0 -0
  57. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/cache.py +0 -0
  58. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/operations.py +0 -0
  59. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/schema.py +0 -0
  60. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/errors.py +0 -0
  61. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/__init__.py +0 -0
  62. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/builder.py +0 -0
  63. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/query.py +0 -0
  64. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/traverse.py +0 -0
  65. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/__init__.py +0 -0
  66. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/__init__.py +0 -0
  67. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/engine.py +0 -0
  68. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/models.py +0 -0
  69. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/resolve.py +0 -0
  70. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/__init__.py +0 -0
  71. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/engine.py +0 -0
  72. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/models.py +0 -0
  73. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/router.py +0 -0
  74. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/imports.py +0 -0
  75. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/resolve.py +0 -0
  76. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/symbols.py +0 -0
  77. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/scanner/__init__.py +0 -0
  78. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/scanner/scanner.py +0 -0
  79. {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aja-codeintel
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: CodeIntel CLI tool
5
5
  Author: Your Name
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aja-codeintel
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: CodeIntel CLI tool
5
5
  Author: Your Name
6
6
  License: MIT License
@@ -23,6 +23,7 @@ codeintel_cli/commands/nav/open_cmd.py
23
23
  codeintel_cli/commands/nav/where_cmd.py
24
24
  codeintel_cli/commands/project/__init__.py
25
25
  codeintel_cli/commands/project/context_cmd.py
26
+ codeintel_cli/commands/project/endpoints_cmd.py
26
27
  codeintel_cli/commands/project/folder_cmd.py
27
28
  codeintel_cli/commands/project/imports_cmd.py
28
29
  codeintel_cli/commands/project/models_cmd.py
@@ -51,6 +52,11 @@ codeintel_cli/db/__init__.py
51
52
  codeintel_cli/db/cache.py
52
53
  codeintel_cli/db/operations.py
53
54
  codeintel_cli/db/schema.py
55
+ codeintel_cli/endpoints/__init__.py
56
+ codeintel_cli/endpoints/java_spring.py
57
+ codeintel_cli/endpoints/models.py
58
+ codeintel_cli/endpoints/python_web.py
59
+ codeintel_cli/endpoints/scan.py
54
60
  codeintel_cli/graph/__init__.py
55
61
  codeintel_cli/graph/builder.py
56
62
  codeintel_cli/graph/query.py
@@ -11,7 +11,7 @@ from .tree_cmd import register_tree
11
11
  from .modeltree_cmd import register_modeltree
12
12
  from .models_cmd import register_models
13
13
  from .servicemap_cmd import register_servicemap
14
-
14
+ from .endpoints_cmd import register_endpoints
15
15
 
16
16
  def register_project_commands(app: typer.Typer) -> None:
17
17
  register_scan(app)
@@ -23,4 +23,5 @@ def register_project_commands(app: typer.Typer) -> None:
23
23
  register_tree(app)
24
24
  register_modeltree(app)
25
25
  register_models(app)
26
- register_servicemap(app)
26
+ register_servicemap(app)
27
+ register_endpoints(app)
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from ...errors import InvalidPathError
11
+ from ...core.project import find_project_root
12
+ from ...endpoints.scan import EndpointScanOptions, iter_supported_source_files
13
+ from ...endpoints.java_spring import extract_spring_endpoints
14
+ from ...endpoints.python_web import extract_python_endpoints
15
+
16
+
17
+ def _validate_folder(path: str) -> Path:
18
+ folder = Path(path).resolve()
19
+ if not folder.exists() or not folder.is_dir():
20
+ raise InvalidPathError(message="Invalid folder", path=folder)
21
+ return folder
22
+
23
+
24
+ def _norm_path(p: str) -> str:
25
+ return p.replace("\\", "/")
26
+
27
+
28
+ def _guess_access(path: str) -> str:
29
+ if path.startswith("/admin"):
30
+ return "ADMIN"
31
+ if path.startswith("/me"):
32
+ return "CUSTOMER"
33
+ if path.startswith("/api/auth"):
34
+ return "PUBLIC"
35
+ return "PUBLIC"
36
+
37
+
38
+ def register_endpoints(app: typer.Typer) -> None:
39
+ @app.command()
40
+ def endpoints(
41
+ path: str = typer.Argument(".", help="Folder to scan"),
42
+ lang: str = typer.Option("all", "--lang", help="all | py | java"),
43
+ hidden: bool = typer.Option(False, "--hidden", help="Include hidden dotfiles/folders"),
44
+ no_gitignore: bool = typer.Option(False, "--no-gitignore", help="Do not apply .gitignore rules"),
45
+ fmt: str = typer.Option("clean", "--format", help="clean | text | json"),
46
+ only_main: bool = typer.Option(True, "--only-main/--all-sources", help="Only scan src/main (skip tests)"),
47
+ only_api: bool = typer.Option(False, "--only-api", help="Only show paths starting with /api"),
48
+ dedupe: bool = typer.Option(True, "--dedupe/--no-dedupe", help="Remove duplicates (method+path)"),
49
+ ):
50
+ folder = _validate_folder(path)
51
+ project_root = find_project_root(folder)
52
+
53
+ opts = EndpointScanOptions(
54
+ show_hidden=hidden,
55
+ use_gitignore=not no_gitignore,
56
+ )
57
+
58
+ lang_norm = lang.strip().lower()
59
+ if lang_norm not in {"all", "py", "java"}:
60
+ raise typer.BadParameter("Invalid --lang. Use: all | py | java")
61
+
62
+ t0 = time.perf_counter()
63
+ all_eps = []
64
+
65
+ files = list(iter_supported_source_files(project_root, opts))
66
+
67
+ for f in files:
68
+ suf = f.suffix.lower()
69
+ if lang_norm == "py" and suf != ".py":
70
+ continue
71
+ if lang_norm == "java" and suf != ".java":
72
+ continue
73
+
74
+ try:
75
+ src = f.read_text(encoding="utf-8", errors="ignore")
76
+ except Exception:
77
+ continue
78
+
79
+ if suf == ".java":
80
+ all_eps.extend(extract_spring_endpoints(src, str(f)))
81
+ elif suf == ".py":
82
+ all_eps.extend(extract_python_endpoints(src, str(f)))
83
+
84
+ if only_main:
85
+ filtered = []
86
+ for e in all_eps:
87
+ fp = _norm_path(e.file)
88
+ if "/src/test/" in fp:
89
+ continue
90
+ if "/src/main/" in fp or "/main/" in fp:
91
+ filtered.append(e)
92
+ all_eps = filtered
93
+
94
+ if only_api:
95
+ all_eps = [e for e in all_eps if e.path.startswith("/api")]
96
+
97
+ if dedupe:
98
+ seen = set()
99
+ uniq = []
100
+ for e in all_eps:
101
+ key = (e.method, e.path)
102
+ if key in seen:
103
+ continue
104
+ seen.add(key)
105
+ uniq.append(e)
106
+ all_eps = uniq
107
+
108
+ all_eps.sort(key=lambda e: (e.path, e.method, e.file, e.line))
109
+ elapsed = time.perf_counter() - t0
110
+
111
+ fmt_norm = fmt.strip().lower()
112
+
113
+ if fmt_norm == "json":
114
+ typer.echo(json.dumps([e.to_dict() for e in all_eps], indent=2))
115
+ return
116
+
117
+ if fmt_norm == "text":
118
+ typer.echo(f"Project: {project_root}")
119
+ typer.echo(f"Found {len(all_eps)} endpoints in {elapsed:.3f}s")
120
+ typer.echo("")
121
+ for e in all_eps:
122
+ roles = set(e.roles or [])
123
+ role_str = f" {{{', '.join(sorted(roles))}}}" if roles else f" {{{_guess_access(e.path)}}}"
124
+ typer.echo(f"{e.method:6} {e.path:40}{role_str} {e.file}:{e.line} {e.handler} <{e.framework}>")
125
+ return
126
+
127
+ typer.echo(f"Found {len(all_eps)} endpoints in {elapsed:.3f}s")
128
+ typer.echo("")
129
+
130
+ methods_by_path: dict[str, set[str]] = defaultdict(set)
131
+ roles_by_key: dict[tuple[str, str], set[str]] = defaultdict(set)
132
+
133
+ for e in all_eps:
134
+ methods_by_path[e.path].add(e.method)
135
+ if e.roles:
136
+ roles_by_key[(e.path, e.method)].update(e.roles)
137
+
138
+ paths = sorted(methods_by_path.keys())
139
+ for idx, p in enumerate(paths, start=1):
140
+ methods = sorted(methods_by_path[p])
141
+ parts = []
142
+ for m in methods:
143
+ role_set = roles_by_key.get((p, m), set())
144
+ role_str = f"{{{', '.join(sorted(role_set))}}}" if role_set else f"{{{_guess_access(p)}}}"
145
+ parts.append(f"{m}{role_str}")
146
+ typer.echo(f"{idx:02d}. {p} [{', '.join(parts)}]")
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import List, Tuple, Optional, Set
5
+
6
+ from .models import Endpoint
7
+
8
+
9
+ _MAPPING_ANN = {
10
+ "GetMapping",
11
+ "PostMapping",
12
+ "PutMapping",
13
+ "DeleteMapping",
14
+ "PatchMapping",
15
+ "RequestMapping",
16
+ }
17
+
18
+ _ANNOT_RE = re.compile(r'@\s*([A-Za-z_]\w*)\s*(\((.*?)\))?', re.DOTALL)
19
+ _CLASS_RE = re.compile(r'\b(class|record)\s+([A-Za-z_]\w*)\b')
20
+ _METHOD_RE = re.compile(r'(public|protected|private)?\s*(static\s+)?[\w<>\[\], ?]+\s+([A-Za-z_]\w*)\s*\(')
21
+
22
+ _PREAUTHORIZE_RE = re.compile(r'@PreAuthorize\s*\(\s*"([^"]+)"\s*\)')
23
+ _SECURED_RE = re.compile(r'@Secured\s*\((.*?)\)')
24
+ _ROLESALLOWED_RE = re.compile(r'@RolesAllowed\s*\((.*?)\)')
25
+
26
+ _HASROLE_RE = re.compile(r"hasRole\s*\(\s*'([^']+)'\s*\)")
27
+ _HASANYROLE_RE = re.compile(r"hasAnyRole\s*\(\s*([^\)]+)\)")
28
+
29
+
30
+ def _extract_paths(arg_str: str) -> List[str]:
31
+ return re.findall(r'"([^"]+)"', arg_str or "")
32
+
33
+
34
+ def _normalize_join(base: str, sub: str) -> str:
35
+ if not base:
36
+ return sub or "/"
37
+ if not sub:
38
+ return base
39
+ if base.endswith("/") and sub.startswith("/"):
40
+ return base[:-1] + sub
41
+ if not base.endswith("/") and not sub.startswith("/"):
42
+ return base + "/" + sub
43
+ return base + sub
44
+
45
+
46
+ def _methods_from_annotation(ann: str, args: str) -> List[str]:
47
+ ann = ann.lower()
48
+ if ann == "getmapping":
49
+ return ["GET"]
50
+ if ann == "postmapping":
51
+ return ["POST"]
52
+ if ann == "putmapping":
53
+ return ["PUT"]
54
+ if ann == "deletemapping":
55
+ return ["DELETE"]
56
+ if ann == "patchmapping":
57
+ return ["PATCH"]
58
+ if ann == "requestmapping":
59
+ return re.findall(r'RequestMethod\.([A-Z]+)', args or "")
60
+ return []
61
+
62
+
63
+ def _roles_from_expr(expr: str) -> Set[str]:
64
+ roles: Set[str] = set()
65
+ for m in _HASROLE_RE.finditer(expr):
66
+ roles.add(m.group(1))
67
+ for m in _HASANYROLE_RE.finditer(expr):
68
+ for r in re.findall(r"'([^']+)'", m.group(1)):
69
+ roles.add(r)
70
+ return roles
71
+
72
+
73
+ def _roles_from_strings(arg: str) -> Set[str]:
74
+ out: Set[str] = set()
75
+ for s in re.findall(r'"([^"]+)"', arg or ""):
76
+ if s.startswith("ROLE_"):
77
+ s = s[5:]
78
+ out.add(s)
79
+ return out
80
+
81
+
82
+ def _extract_roles_block(text: str) -> Set[str]:
83
+ roles: Set[str] = set()
84
+ for m in _PREAUTHORIZE_RE.finditer(text):
85
+ roles |= _roles_from_expr(m.group(1))
86
+ for m in _SECURED_RE.finditer(text):
87
+ roles |= _roles_from_strings(m.group(1))
88
+ for m in _ROLESALLOWED_RE.finditer(text):
89
+ roles |= _roles_from_strings(m.group(1))
90
+ return roles
91
+
92
+
93
+ def extract_spring_endpoints(java_source: str, file_path: str) -> List[Endpoint]:
94
+ endpoints: List[Endpoint] = []
95
+ lines = java_source.splitlines()
96
+
97
+ mcls = _CLASS_RE.search(java_source)
98
+ if not mcls:
99
+ return []
100
+
101
+ class_pos = mcls.start()
102
+ controller = mcls.group(2)
103
+ pre = java_source[:class_pos]
104
+
105
+ if "@RestController" not in pre and "@Controller" not in pre:
106
+ return []
107
+
108
+ base_path = ""
109
+ for m in _ANNOT_RE.finditer(pre):
110
+ if m.group(1) == "RequestMapping":
111
+ ps = _extract_paths(m.group(3) or "")
112
+ if ps:
113
+ base_path = ps[0]
114
+
115
+ class_roles = _extract_roles_block(pre)
116
+
117
+ pending: List[Tuple[str, str, int]] = []
118
+
119
+ for i, line in enumerate(lines, start=1):
120
+ s = line.strip()
121
+
122
+ if s.startswith("@"):
123
+ am = _ANNOT_RE.search(s)
124
+ if am and am.group(1) in _MAPPING_ANN:
125
+ pending.append((am.group(1), am.group(3) or "", i))
126
+ continue
127
+
128
+ mm = _METHOD_RE.search(s)
129
+ if mm and pending:
130
+ handler = mm.group(3)
131
+ method_roles = _extract_roles_block("\n".join(lines[max(0, i - 8):i]))
132
+ roles = class_roles | method_roles
133
+
134
+ for ann, args, ln in pending:
135
+ for meth in _methods_from_annotation(ann, args):
136
+ paths = _extract_paths(args) or [""]
137
+ for p in paths:
138
+ full = _normalize_join(base_path, p)
139
+ endpoints.append(
140
+ Endpoint(
141
+ method=meth,
142
+ path=full,
143
+ file=file_path,
144
+ line=ln,
145
+ handler=handler,
146
+ framework="spring",
147
+ controller=controller,
148
+ roles=roles or None,
149
+ )
150
+ )
151
+ pending = []
152
+
153
+ return endpoints
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, asdict
4
+ from typing import Optional, Dict, Any, Set
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Endpoint:
9
+ method: str
10
+ path: str
11
+ file: str
12
+ line: int
13
+ handler: str
14
+ framework: str
15
+ controller: Optional[str] = None
16
+ roles: Optional[Set[str]] = None # e.g. {"ADMIN","CUSTOMER"}
17
+
18
+ def to_dict(self) -> Dict[str, Any]:
19
+ d = asdict(self)
20
+
21
+ if self.roles is not None:
22
+ d["roles"] = sorted(self.roles)
23
+ return d
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import List, Optional, Tuple
5
+
6
+ from .models import Endpoint
7
+
8
+ _FASTAPI_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
9
+
10
+
11
+ def _get_str(node: ast.AST) -> Optional[str]:
12
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
13
+ return node.value
14
+ return None
15
+
16
+
17
+ def _call_name(expr: ast.AST) -> Tuple[Optional[str], Optional[str]]:
18
+ if isinstance(expr, ast.Call):
19
+ func = expr.func
20
+ if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
21
+ return func.value.id, func.attr
22
+ return None, None
23
+
24
+
25
+ def _extract_flask_methods(dec_call: ast.Call) -> List[str]:
26
+ for kw in dec_call.keywords:
27
+ if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
28
+ ms: List[str] = []
29
+ for el in kw.value.elts:
30
+ s = _get_str(el)
31
+ if s:
32
+ ms.append(s.upper())
33
+ return sorted(set(ms)) if ms else ["GET"]
34
+ return ["GET"]
35
+
36
+
37
+ def extract_python_endpoints(py_source: str, file_path: str) -> List[Endpoint]:
38
+ try:
39
+ tree = ast.parse(py_source)
40
+ except SyntaxError:
41
+ return []
42
+
43
+ endpoints: List[Endpoint] = []
44
+
45
+ for node in ast.walk(tree):
46
+ if not isinstance(node, ast.FunctionDef):
47
+ continue
48
+
49
+ if not node.decorator_list:
50
+ continue
51
+
52
+ handler = node.name
53
+
54
+ for dec in node.decorator_list:
55
+ if not isinstance(dec, ast.Call):
56
+ continue
57
+
58
+ obj, meth = _call_name(dec)
59
+ if not obj or not meth:
60
+ continue
61
+
62
+ # path is usually first arg
63
+ pnode = dec.args[0] if dec.args else None
64
+ path = _get_str(pnode) if pnode else None
65
+ if not path or not path.startswith("/"):
66
+ continue
67
+
68
+ low = meth.lower()
69
+
70
+ # FastAPI: @app.get("/x") / @router.post("/x")
71
+ if low in _FASTAPI_METHODS:
72
+ endpoints.append(
73
+ Endpoint(
74
+ method=low.upper(),
75
+ path=path,
76
+ file=file_path,
77
+ line=getattr(dec, "lineno", getattr(node, "lineno", 1)),
78
+ handler=handler,
79
+ framework="fastapi",
80
+ )
81
+ )
82
+ continue
83
+
84
+ # Flask: @app.route("/x", methods=[...])
85
+ if low == "route":
86
+ ms = _extract_flask_methods(dec)
87
+ for m in ms:
88
+ endpoints.append(
89
+ Endpoint(
90
+ method=m,
91
+ path=path,
92
+ file=file_path,
93
+ line=getattr(dec, "lineno", getattr(node, "lineno", 1)),
94
+ handler=handler,
95
+ framework="flask",
96
+ )
97
+ )
98
+
99
+ return endpoints
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Iterable, Optional
6
+
7
+ # Optional: pip install pathspec
8
+ try:
9
+ import pathspec # type: ignore
10
+ except Exception: # pragma: no cover
11
+ pathspec = None
12
+
13
+ from ..core.project import find_project_root, SKIP_DIRS
14
+ from ..commands.project.tree_cmd import DEFAULT_IGNORE_NAMES # reuse your ignore set
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class EndpointScanOptions:
19
+ show_hidden: bool
20
+ use_gitignore: bool
21
+
22
+
23
+ def _is_hidden_name(name: str) -> bool:
24
+ return name.startswith(".")
25
+
26
+
27
+ def _load_gitignore_spec(root: Path):
28
+ if pathspec is None:
29
+ return None
30
+ gi = root / ".gitignore"
31
+ if not gi.exists():
32
+ return None
33
+
34
+ lines: list[str] = []
35
+ for line in gi.read_text(encoding="utf-8", errors="ignore").splitlines():
36
+ s = line.strip()
37
+ if not s or s.startswith("#"):
38
+ continue
39
+ lines.append(s)
40
+
41
+ if not lines:
42
+ return None
43
+ return pathspec.PathSpec.from_lines("gitwildmatch", lines)
44
+
45
+
46
+ def _gitignore_matches(spec, rel_posix: str, is_dir: bool) -> bool:
47
+ if spec is None:
48
+ return False
49
+ if spec.match_file(rel_posix):
50
+ return True
51
+ if is_dir and spec.match_file(rel_posix.rstrip("/") + "/"):
52
+ return True
53
+ return False
54
+
55
+
56
+ def _is_skipped(p: Path, root: Path, spec, opts: EndpointScanOptions) -> bool:
57
+ name = p.name
58
+
59
+ # reuse your directory skip policy + tree ignores
60
+ if any(part in SKIP_DIRS for part in p.parts):
61
+ return True
62
+ if name in DEFAULT_IGNORE_NAMES:
63
+ return True
64
+
65
+ if _is_hidden_name(name) and not opts.show_hidden:
66
+ return True
67
+
68
+ if opts.use_gitignore and spec is not None:
69
+ try:
70
+ rel = p.relative_to(root).as_posix()
71
+ except Exception:
72
+ rel = p.as_posix()
73
+ if _gitignore_matches(spec, rel, p.is_dir()):
74
+ return True
75
+
76
+ return False
77
+
78
+
79
+ def iter_supported_source_files(start: Path, opts: EndpointScanOptions) -> Iterable[Path]:
80
+ start = start.resolve()
81
+ project_root = find_project_root(start)
82
+ spec = _load_gitignore_spec(project_root) if opts.use_gitignore else None
83
+
84
+ # manual walk so we can prune dirs early
85
+ stack = [project_root]
86
+ while stack:
87
+ cur = stack.pop()
88
+ if _is_skipped(cur, project_root, spec, opts):
89
+ continue
90
+ try:
91
+ entries = list(cur.iterdir())
92
+ except Exception:
93
+ continue
94
+
95
+ for e in entries:
96
+ if _is_skipped(e, project_root, spec, opts):
97
+ continue
98
+ if e.is_dir():
99
+ stack.append(e)
100
+ elif e.is_file():
101
+ if e.suffix.lower() in {".java", ".py"}:
102
+ yield e.resolve()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aja-codeintel"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "CodeIntel CLI tool"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes
File without changes
File without changes