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.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. 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()