apisec-code-bolt 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.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class-Based View (CBV) extractor for FastAPI/Starlette applications.
|
|
3
|
+
|
|
4
|
+
This module handles:
|
|
5
|
+
1. fastapi-utils CBV pattern: @cbv(router) class UserAPI
|
|
6
|
+
2. Starlette HTTPEndpoint: class UserEndpoint(HTTPEndpoint)
|
|
7
|
+
3. Generic REST resource classes
|
|
8
|
+
4. ViewSet patterns (Django-REST-Framework style)
|
|
9
|
+
5. Custom base class inheritance
|
|
10
|
+
|
|
11
|
+
CRITICAL: Enterprise applications often use CBV for:
|
|
12
|
+
- Grouping related endpoints
|
|
13
|
+
- Shared dependencies across endpoints
|
|
14
|
+
- Inheritance-based authorization
|
|
15
|
+
- RESTful resource patterns
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ..base import ParsedClass, ParsedDecorator, ParsedFile, ParsedFunction
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Data Types
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CBVRoute:
|
|
35
|
+
"""A route extracted from a class-based view."""
|
|
36
|
+
|
|
37
|
+
# Route info
|
|
38
|
+
path: str
|
|
39
|
+
method: str # GET, POST, etc.
|
|
40
|
+
|
|
41
|
+
# Handler info
|
|
42
|
+
handler_method: str # Method name (e.g., "list", "create")
|
|
43
|
+
handler_class: str # Class name
|
|
44
|
+
handler_qualified: str # Fully qualified name
|
|
45
|
+
|
|
46
|
+
# Location
|
|
47
|
+
file_path: Path
|
|
48
|
+
line: int
|
|
49
|
+
|
|
50
|
+
# CBV type
|
|
51
|
+
cbv_type: str # "cbv_decorator", "http_endpoint", "viewset", "generic"
|
|
52
|
+
|
|
53
|
+
# Additional metadata
|
|
54
|
+
is_async: bool = False
|
|
55
|
+
dependencies: list[str] = field(default_factory=list)
|
|
56
|
+
tags: list[str] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
# Confidence
|
|
59
|
+
confidence: float = 0.8
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CBVClass:
|
|
64
|
+
"""A class-based view definition."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
qualified_name: str
|
|
68
|
+
file_path: Path
|
|
69
|
+
line: int
|
|
70
|
+
|
|
71
|
+
# Type of CBV
|
|
72
|
+
cbv_type: str # "cbv_decorator", "http_endpoint", "viewset", "generic"
|
|
73
|
+
|
|
74
|
+
# Associated router (for @cbv(router) pattern)
|
|
75
|
+
router_var: str | None = None
|
|
76
|
+
|
|
77
|
+
# Base path (from decorator or class attribute)
|
|
78
|
+
base_path: str = ""
|
|
79
|
+
|
|
80
|
+
# HTTP method handlers
|
|
81
|
+
routes: list[CBVRoute] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
# Class-level dependencies
|
|
84
|
+
dependencies: list[str] = field(default_factory=list)
|
|
85
|
+
|
|
86
|
+
# Confidence
|
|
87
|
+
confidence: float = 0.8
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Method Patterns
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# HTTP method names that correspond to routes
|
|
96
|
+
HTTP_METHOD_PATTERNS = {
|
|
97
|
+
# Direct method names (HTTPEndpoint style)
|
|
98
|
+
"get": "GET",
|
|
99
|
+
"post": "POST",
|
|
100
|
+
"put": "PUT",
|
|
101
|
+
"patch": "PATCH",
|
|
102
|
+
"delete": "DELETE",
|
|
103
|
+
"head": "HEAD",
|
|
104
|
+
"options": "OPTIONS",
|
|
105
|
+
# CRUD-style names
|
|
106
|
+
"list": "GET",
|
|
107
|
+
"retrieve": "GET",
|
|
108
|
+
"create": "POST",
|
|
109
|
+
"update": "PUT",
|
|
110
|
+
"partial_update": "PATCH",
|
|
111
|
+
"destroy": "DELETE",
|
|
112
|
+
# Action-style names
|
|
113
|
+
"index": "GET",
|
|
114
|
+
"show": "GET",
|
|
115
|
+
"new": "GET",
|
|
116
|
+
"edit": "GET",
|
|
117
|
+
"store": "POST",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Base classes that indicate CBV
|
|
121
|
+
CBV_BASE_CLASSES = {
|
|
122
|
+
# Starlette
|
|
123
|
+
"HTTPEndpoint",
|
|
124
|
+
"WebSocketEndpoint",
|
|
125
|
+
"starlette.endpoints.HTTPEndpoint",
|
|
126
|
+
"starlette.endpoints.WebSocketEndpoint",
|
|
127
|
+
# fastapi-utils
|
|
128
|
+
"Resource",
|
|
129
|
+
"CRUDResource",
|
|
130
|
+
# Generic patterns
|
|
131
|
+
"APIView",
|
|
132
|
+
"ViewSet",
|
|
133
|
+
"ModelViewSet",
|
|
134
|
+
"GenericViewSet",
|
|
135
|
+
"GenericAPIView",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# CBV Extractor
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CBVExtractor:
|
|
145
|
+
"""
|
|
146
|
+
Extracts routes from class-based views.
|
|
147
|
+
|
|
148
|
+
Handles multiple CBV patterns:
|
|
149
|
+
|
|
150
|
+
1. fastapi-utils @cbv decorator:
|
|
151
|
+
@cbv(router)
|
|
152
|
+
class UserAPI:
|
|
153
|
+
@router.get("/")
|
|
154
|
+
def list(self): ...
|
|
155
|
+
|
|
156
|
+
2. Starlette HTTPEndpoint:
|
|
157
|
+
class UserEndpoint(HTTPEndpoint):
|
|
158
|
+
async def get(self, request): ...
|
|
159
|
+
async def post(self, request): ...
|
|
160
|
+
|
|
161
|
+
3. ViewSet pattern:
|
|
162
|
+
class UserViewSet(ViewSet):
|
|
163
|
+
def list(self, request): ...
|
|
164
|
+
def retrieve(self, request, pk): ...
|
|
165
|
+
|
|
166
|
+
Usage:
|
|
167
|
+
extractor = CBVExtractor()
|
|
168
|
+
|
|
169
|
+
for parsed in parsed_files:
|
|
170
|
+
extractor.process_file(parsed)
|
|
171
|
+
|
|
172
|
+
cbv_classes = extractor.get_all_cbv_classes()
|
|
173
|
+
routes = extractor.get_all_routes()
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, project_root: Path | None = None):
|
|
177
|
+
"""Initialize the extractor."""
|
|
178
|
+
self._project_root = project_root
|
|
179
|
+
self._cbv_classes: dict[str, CBVClass] = {}
|
|
180
|
+
self._routes: list[CBVRoute] = []
|
|
181
|
+
self._router_vars: dict[Path, set[str]] = {}
|
|
182
|
+
|
|
183
|
+
def process_file(self, parsed: ParsedFile) -> None:
|
|
184
|
+
"""Process a parsed file to extract CBV routes."""
|
|
185
|
+
if not parsed.success:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
file_path = parsed.path
|
|
189
|
+
self._router_vars[file_path] = set()
|
|
190
|
+
|
|
191
|
+
# Identify router variables
|
|
192
|
+
self._identify_routers(parsed)
|
|
193
|
+
|
|
194
|
+
# Process each class
|
|
195
|
+
for cls in parsed.classes:
|
|
196
|
+
cbv = self._analyze_class(cls, parsed)
|
|
197
|
+
if cbv:
|
|
198
|
+
self._cbv_classes[cbv.qualified_name] = cbv
|
|
199
|
+
self._routes.extend(cbv.routes)
|
|
200
|
+
|
|
201
|
+
def _identify_routers(self, parsed: ParsedFile) -> None:
|
|
202
|
+
"""Identify router variables in the file."""
|
|
203
|
+
file_path = parsed.path
|
|
204
|
+
|
|
205
|
+
for assign in parsed.assignments:
|
|
206
|
+
if assign.source_type == "call":
|
|
207
|
+
called = assign.source_call or ""
|
|
208
|
+
if "Router" in called or "FastAPI" in called:
|
|
209
|
+
self._router_vars[file_path].add(assign.target)
|
|
210
|
+
|
|
211
|
+
def _analyze_class(
|
|
212
|
+
self,
|
|
213
|
+
cls: ParsedClass,
|
|
214
|
+
parsed: ParsedFile,
|
|
215
|
+
) -> CBVClass | None:
|
|
216
|
+
"""Analyze a class to determine if it's a CBV."""
|
|
217
|
+
|
|
218
|
+
# Check 1: @cbv decorator
|
|
219
|
+
cbv_decorator = self._find_cbv_decorator(cls)
|
|
220
|
+
if cbv_decorator:
|
|
221
|
+
return self._extract_cbv_decorated_class(cls, cbv_decorator, parsed)
|
|
222
|
+
|
|
223
|
+
# Check 2: HTTPEndpoint base class
|
|
224
|
+
if self._is_http_endpoint(cls):
|
|
225
|
+
return self._extract_http_endpoint_class(cls, parsed)
|
|
226
|
+
|
|
227
|
+
# Check 3: ViewSet base class
|
|
228
|
+
if self._is_viewset(cls):
|
|
229
|
+
return self._extract_viewset_class(cls, parsed)
|
|
230
|
+
|
|
231
|
+
# Check 4: Generic CBV patterns — only if the class looks like a view
|
|
232
|
+
if self._has_http_methods(cls) and self._looks_like_view_class(cls):
|
|
233
|
+
return self._extract_generic_cbv_class(cls, parsed)
|
|
234
|
+
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _find_cbv_decorator(self, cls: ParsedClass) -> ParsedDecorator | None:
|
|
238
|
+
"""Find @cbv decorator on a class."""
|
|
239
|
+
for dec in cls.decorators:
|
|
240
|
+
if dec.name == "cbv" or dec.name.endswith(".cbv"):
|
|
241
|
+
return dec
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _is_http_endpoint(self, cls: ParsedClass) -> bool:
|
|
245
|
+
"""Check if class inherits from HTTPEndpoint."""
|
|
246
|
+
return any(base in CBV_BASE_CLASSES or "HTTPEndpoint" in base for base in cls.base_classes)
|
|
247
|
+
|
|
248
|
+
def _is_viewset(self, cls: ParsedClass) -> bool:
|
|
249
|
+
"""Check if class is a ViewSet."""
|
|
250
|
+
return any("ViewSet" in base or "APIView" in base for base in cls.base_classes)
|
|
251
|
+
|
|
252
|
+
def _has_http_methods(self, cls: ParsedClass) -> bool:
|
|
253
|
+
"""Check if class has methods named after HTTP methods."""
|
|
254
|
+
http_methods = {"get", "post", "put", "patch", "delete", "head", "options"}
|
|
255
|
+
return any(method.name.lower() in http_methods for method in cls.methods)
|
|
256
|
+
|
|
257
|
+
_VIEW_CLASS_SUFFIXES = ("View", "Endpoint", "Resource", "Handler", "Controller", "API")
|
|
258
|
+
_VIEW_RELATED_BASES = frozenset(
|
|
259
|
+
{
|
|
260
|
+
"HTTPEndpoint",
|
|
261
|
+
"WebSocketEndpoint",
|
|
262
|
+
"APIView",
|
|
263
|
+
"ViewSet",
|
|
264
|
+
"ModelViewSet",
|
|
265
|
+
"GenericViewSet",
|
|
266
|
+
"GenericAPIView",
|
|
267
|
+
"Resource",
|
|
268
|
+
"CRUDResource",
|
|
269
|
+
"View",
|
|
270
|
+
"MethodView",
|
|
271
|
+
"BaseHTTPHandler",
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
_VIEW_DECORATOR_INDICATORS = ("route", "cbv", "register", "api_view")
|
|
275
|
+
|
|
276
|
+
def _looks_like_view_class(self, cls: ParsedClass) -> bool:
|
|
277
|
+
"""Return True only if the class has structural signals of being an HTTP view.
|
|
278
|
+
|
|
279
|
+
Checks class name suffix, base classes, and decorators. This prevents
|
|
280
|
+
service/plugin/repository classes from being treated as route handlers.
|
|
281
|
+
"""
|
|
282
|
+
if cls.name.endswith(self._VIEW_CLASS_SUFFIXES):
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
for base in cls.base_classes:
|
|
286
|
+
short = base.rsplit(".", 1)[-1]
|
|
287
|
+
if short in self._VIEW_RELATED_BASES:
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
for dec in cls.decorators:
|
|
291
|
+
dec_lower = dec.name.lower()
|
|
292
|
+
if any(ind in dec_lower for ind in self._VIEW_DECORATOR_INDICATORS):
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
# =========================================================================
|
|
298
|
+
# CBV Decorated Class Extraction
|
|
299
|
+
# =========================================================================
|
|
300
|
+
|
|
301
|
+
def _extract_cbv_decorated_class(
|
|
302
|
+
self,
|
|
303
|
+
cls: ParsedClass,
|
|
304
|
+
cbv_decorator: ParsedDecorator,
|
|
305
|
+
parsed: ParsedFile,
|
|
306
|
+
) -> CBVClass:
|
|
307
|
+
"""Extract routes from @cbv decorated class."""
|
|
308
|
+
file_path = parsed.path
|
|
309
|
+
self._router_vars.get(file_path, set())
|
|
310
|
+
|
|
311
|
+
# Get router variable from decorator
|
|
312
|
+
router_var = None
|
|
313
|
+
if cbv_decorator.positional_args:
|
|
314
|
+
router_var = str(cbv_decorator.positional_args[0])
|
|
315
|
+
|
|
316
|
+
cbv = CBVClass(
|
|
317
|
+
name=cls.name,
|
|
318
|
+
qualified_name=cls.qualified_name.full,
|
|
319
|
+
file_path=file_path,
|
|
320
|
+
line=cls.location.line,
|
|
321
|
+
cbv_type="cbv_decorator",
|
|
322
|
+
router_var=router_var,
|
|
323
|
+
confidence=0.95,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Extract routes from decorated methods
|
|
327
|
+
for method in cls.methods:
|
|
328
|
+
route = self._extract_decorated_method_route(method, cls, router_var, parsed)
|
|
329
|
+
if route:
|
|
330
|
+
cbv.routes.append(route)
|
|
331
|
+
|
|
332
|
+
return cbv
|
|
333
|
+
|
|
334
|
+
def _extract_decorated_method_route(
|
|
335
|
+
self,
|
|
336
|
+
method: ParsedFunction,
|
|
337
|
+
cls: ParsedClass,
|
|
338
|
+
router_var: str | None,
|
|
339
|
+
parsed: ParsedFile,
|
|
340
|
+
) -> CBVRoute | None:
|
|
341
|
+
"""Extract route from a decorated method in CBV."""
|
|
342
|
+
file_path = parsed.path
|
|
343
|
+
|
|
344
|
+
# Look for route decorator
|
|
345
|
+
for dec in method.decorators:
|
|
346
|
+
route_info = self._parse_route_decorator(dec, router_var)
|
|
347
|
+
if route_info:
|
|
348
|
+
path, http_method = route_info
|
|
349
|
+
|
|
350
|
+
return CBVRoute(
|
|
351
|
+
path=path,
|
|
352
|
+
method=http_method,
|
|
353
|
+
handler_method=method.name,
|
|
354
|
+
handler_class=cls.name,
|
|
355
|
+
handler_qualified=f"{cls.qualified_name.full}.{method.name}",
|
|
356
|
+
file_path=file_path,
|
|
357
|
+
line=method.location.line,
|
|
358
|
+
cbv_type="cbv_decorator",
|
|
359
|
+
is_async=method.is_async,
|
|
360
|
+
confidence=0.95,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
def _parse_route_decorator(
|
|
366
|
+
self,
|
|
367
|
+
dec: ParsedDecorator,
|
|
368
|
+
router_var: str | None,
|
|
369
|
+
) -> tuple[str, str] | None:
|
|
370
|
+
"""Parse a route decorator to extract path and method."""
|
|
371
|
+
dec_name = dec.name.lower()
|
|
372
|
+
full_name = dec.qualified_name.full if dec.qualified_name else dec.name
|
|
373
|
+
|
|
374
|
+
# HTTP methods
|
|
375
|
+
http_methods = {"get", "post", "put", "patch", "delete", "head", "options"}
|
|
376
|
+
|
|
377
|
+
# Direct method decorator
|
|
378
|
+
if dec_name in http_methods:
|
|
379
|
+
path = self._extract_path_from_decorator(dec)
|
|
380
|
+
return (path, dec_name.upper())
|
|
381
|
+
|
|
382
|
+
# router.method pattern
|
|
383
|
+
parts = full_name.split(".")
|
|
384
|
+
if len(parts) >= 2:
|
|
385
|
+
method_name = parts[-1].lower()
|
|
386
|
+
var_name = parts[-2]
|
|
387
|
+
|
|
388
|
+
if method_name in http_methods:
|
|
389
|
+
if router_var is None or var_name == router_var:
|
|
390
|
+
path = self._extract_path_from_decorator(dec)
|
|
391
|
+
return (path, method_name.upper())
|
|
392
|
+
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
def _extract_path_from_decorator(self, dec: ParsedDecorator) -> str:
|
|
396
|
+
"""Extract path from route decorator."""
|
|
397
|
+
if dec.positional_args:
|
|
398
|
+
first = dec.positional_args[0]
|
|
399
|
+
if isinstance(first, str):
|
|
400
|
+
return first
|
|
401
|
+
|
|
402
|
+
if "path" in dec.arguments:
|
|
403
|
+
return str(dec.arguments["path"])
|
|
404
|
+
|
|
405
|
+
return "/"
|
|
406
|
+
|
|
407
|
+
# =========================================================================
|
|
408
|
+
# HTTPEndpoint Class Extraction
|
|
409
|
+
# =========================================================================
|
|
410
|
+
|
|
411
|
+
def _extract_http_endpoint_class(
|
|
412
|
+
self,
|
|
413
|
+
cls: ParsedClass,
|
|
414
|
+
parsed: ParsedFile,
|
|
415
|
+
) -> CBVClass:
|
|
416
|
+
"""Extract routes from HTTPEndpoint class."""
|
|
417
|
+
file_path = parsed.path
|
|
418
|
+
|
|
419
|
+
cbv = CBVClass(
|
|
420
|
+
name=cls.name,
|
|
421
|
+
qualified_name=cls.qualified_name.full,
|
|
422
|
+
file_path=file_path,
|
|
423
|
+
line=cls.location.line,
|
|
424
|
+
cbv_type="http_endpoint",
|
|
425
|
+
confidence=0.9,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# HTTP methods are methods named get, post, etc.
|
|
429
|
+
for method in cls.methods:
|
|
430
|
+
method_name = method.name.lower()
|
|
431
|
+
|
|
432
|
+
if method_name in HTTP_METHOD_PATTERNS:
|
|
433
|
+
http_method = HTTP_METHOD_PATTERNS[method_name]
|
|
434
|
+
|
|
435
|
+
cbv.routes.append(
|
|
436
|
+
CBVRoute(
|
|
437
|
+
path="{endpoint_path}", # Determined by routing
|
|
438
|
+
method=http_method,
|
|
439
|
+
handler_method=method.name,
|
|
440
|
+
handler_class=cls.name,
|
|
441
|
+
handler_qualified=f"{cls.qualified_name.full}.{method.name}",
|
|
442
|
+
file_path=file_path,
|
|
443
|
+
line=method.location.line,
|
|
444
|
+
cbv_type="http_endpoint",
|
|
445
|
+
is_async=method.is_async,
|
|
446
|
+
confidence=0.9,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return cbv
|
|
451
|
+
|
|
452
|
+
# =========================================================================
|
|
453
|
+
# ViewSet Class Extraction
|
|
454
|
+
# =========================================================================
|
|
455
|
+
|
|
456
|
+
def _extract_viewset_class(
|
|
457
|
+
self,
|
|
458
|
+
cls: ParsedClass,
|
|
459
|
+
parsed: ParsedFile,
|
|
460
|
+
) -> CBVClass:
|
|
461
|
+
"""Extract routes from ViewSet class."""
|
|
462
|
+
file_path = parsed.path
|
|
463
|
+
|
|
464
|
+
cbv = CBVClass(
|
|
465
|
+
name=cls.name,
|
|
466
|
+
qualified_name=cls.qualified_name.full,
|
|
467
|
+
file_path=file_path,
|
|
468
|
+
line=cls.location.line,
|
|
469
|
+
cbv_type="viewset",
|
|
470
|
+
confidence=0.85,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Extract standard CRUD actions
|
|
474
|
+
for method in cls.methods:
|
|
475
|
+
method_name = method.name.lower()
|
|
476
|
+
|
|
477
|
+
if method_name in HTTP_METHOD_PATTERNS:
|
|
478
|
+
http_method = HTTP_METHOD_PATTERNS[method_name]
|
|
479
|
+
|
|
480
|
+
# Determine path based on method name
|
|
481
|
+
path = self._viewset_method_to_path(method_name)
|
|
482
|
+
|
|
483
|
+
cbv.routes.append(
|
|
484
|
+
CBVRoute(
|
|
485
|
+
path=path,
|
|
486
|
+
method=http_method,
|
|
487
|
+
handler_method=method.name,
|
|
488
|
+
handler_class=cls.name,
|
|
489
|
+
handler_qualified=f"{cls.qualified_name.full}.{method.name}",
|
|
490
|
+
file_path=file_path,
|
|
491
|
+
line=method.location.line,
|
|
492
|
+
cbv_type="viewset",
|
|
493
|
+
is_async=method.is_async,
|
|
494
|
+
confidence=0.8,
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return cbv
|
|
499
|
+
|
|
500
|
+
def _viewset_method_to_path(self, method_name: str) -> str:
|
|
501
|
+
"""Convert viewset method name to path."""
|
|
502
|
+
if method_name in {"list", "create", "index", "store"}:
|
|
503
|
+
return "/"
|
|
504
|
+
elif method_name in {
|
|
505
|
+
"retrieve",
|
|
506
|
+
"show",
|
|
507
|
+
"update",
|
|
508
|
+
"partial_update",
|
|
509
|
+
"destroy",
|
|
510
|
+
"delete",
|
|
511
|
+
"edit",
|
|
512
|
+
}:
|
|
513
|
+
return "/{id}"
|
|
514
|
+
else:
|
|
515
|
+
return "/"
|
|
516
|
+
|
|
517
|
+
# =========================================================================
|
|
518
|
+
# Generic CBV Extraction
|
|
519
|
+
# =========================================================================
|
|
520
|
+
|
|
521
|
+
def _extract_generic_cbv_class(
|
|
522
|
+
self,
|
|
523
|
+
cls: ParsedClass,
|
|
524
|
+
parsed: ParsedFile,
|
|
525
|
+
) -> CBVClass:
|
|
526
|
+
"""Extract routes from generic class with HTTP methods."""
|
|
527
|
+
file_path = parsed.path
|
|
528
|
+
|
|
529
|
+
cbv = CBVClass(
|
|
530
|
+
name=cls.name,
|
|
531
|
+
qualified_name=cls.qualified_name.full,
|
|
532
|
+
file_path=file_path,
|
|
533
|
+
line=cls.location.line,
|
|
534
|
+
cbv_type="generic",
|
|
535
|
+
confidence=0.6, # Lower confidence for generic detection
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
for method in cls.methods:
|
|
539
|
+
method_name = method.name.lower()
|
|
540
|
+
|
|
541
|
+
if method_name in HTTP_METHOD_PATTERNS:
|
|
542
|
+
http_method = HTTP_METHOD_PATTERNS[method_name]
|
|
543
|
+
|
|
544
|
+
cbv.routes.append(
|
|
545
|
+
CBVRoute(
|
|
546
|
+
path="{class_path}",
|
|
547
|
+
method=http_method,
|
|
548
|
+
handler_method=method.name,
|
|
549
|
+
handler_class=cls.name,
|
|
550
|
+
handler_qualified=f"{cls.qualified_name.full}.{method.name}",
|
|
551
|
+
file_path=file_path,
|
|
552
|
+
line=method.location.line,
|
|
553
|
+
cbv_type="generic",
|
|
554
|
+
is_async=method.is_async,
|
|
555
|
+
confidence=0.6,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
return cbv
|
|
560
|
+
|
|
561
|
+
# =========================================================================
|
|
562
|
+
# Query Methods
|
|
563
|
+
# =========================================================================
|
|
564
|
+
|
|
565
|
+
def get_all_cbv_classes(self) -> dict[str, CBVClass]:
|
|
566
|
+
"""Get all detected CBV classes."""
|
|
567
|
+
return dict(self._cbv_classes)
|
|
568
|
+
|
|
569
|
+
def get_all_routes(self) -> list[CBVRoute]:
|
|
570
|
+
"""Get all detected CBV routes."""
|
|
571
|
+
return list(self._routes)
|
|
572
|
+
|
|
573
|
+
def get_routes_for_class(self, class_name: str) -> list[CBVRoute]:
|
|
574
|
+
"""Get routes for a specific class."""
|
|
575
|
+
return [r for r in self._routes if r.handler_class == class_name]
|
|
576
|
+
|
|
577
|
+
def get_classes_for_file(self, file_path: Path) -> list[CBVClass]:
|
|
578
|
+
"""Get CBV classes in a specific file."""
|
|
579
|
+
return [c for c in self._cbv_classes.values() if c.file_path == file_path]
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# =============================================================================
|
|
583
|
+
# Convenience Functions
|
|
584
|
+
# =============================================================================
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def extract_cbv_routes(
|
|
588
|
+
parsed_files: list[ParsedFile],
|
|
589
|
+
project_root: Path | None = None,
|
|
590
|
+
) -> tuple[list[CBVRoute], dict[str, CBVClass]]:
|
|
591
|
+
"""
|
|
592
|
+
Extract CBV routes from parsed files.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
parsed_files: List of successfully parsed files
|
|
596
|
+
project_root: Optional project root
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Tuple of (routes, cbv_classes)
|
|
600
|
+
"""
|
|
601
|
+
extractor = CBVExtractor(project_root)
|
|
602
|
+
|
|
603
|
+
for parsed in parsed_files:
|
|
604
|
+
extractor.process_file(parsed)
|
|
605
|
+
|
|
606
|
+
return extractor.get_all_routes(), extractor.get_all_cbv_classes()
|