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,487 @@
1
+ """
2
+ Python-specific implementation of LanguageServices.
3
+
4
+ This module provides the Python implementations of the abstract service
5
+ protocols defined in parsing/services.py. It adapts the existing Python
6
+ parsing infrastructure to the language-agnostic interfaces.
7
+
8
+ DESIGN:
9
+ - Implements LanguageServices for Python
10
+ - Wraps existing components (CrossFileResolver, etc.) in protocol adapters
11
+ - Provides framework-specific services (FastAPI router registry, etc.)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from ..services import (
20
+ AnalysisContext,
21
+ LanguageServices,
22
+ ResolvedConstant,
23
+ ResolvedField,
24
+ ResolvedPath,
25
+ ResolvedType,
26
+ RouterInfo,
27
+ TypeResolver,
28
+ )
29
+ from ..services import (
30
+ ConstantResolver as ConstantResolverProtocol,
31
+ )
32
+ from ..services import (
33
+ PathResolver as PathResolverProtocol,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from ..base import ParsedFile
38
+
39
+
40
+ # =============================================================================
41
+ # Protocol Adapters
42
+ # =============================================================================
43
+
44
+ # These adapters wrap existing Python components to implement the abstract protocols
45
+
46
+
47
+ class PythonTypeResolverAdapter:
48
+ """
49
+ Adapter that wraps CrossFileResolver to implement TypeResolver protocol.
50
+ """
51
+
52
+ def __init__(self, cross_file_resolver):
53
+ """
54
+ Args:
55
+ cross_file_resolver: Python CrossFileResolver instance
56
+ """
57
+ self._resolver = cross_file_resolver
58
+
59
+ def resolve_type(
60
+ self,
61
+ name: str,
62
+ in_file: Path | None = None,
63
+ ) -> ResolvedType | None:
64
+ """Resolve a type reference."""
65
+ symbol = self._resolver.resolve_symbol(name, in_file)
66
+ if not symbol:
67
+ return None
68
+
69
+ return ResolvedType(
70
+ name=symbol.name,
71
+ qualified_name=symbol.qualified_name,
72
+ file_path=symbol.file_path,
73
+ is_class=symbol.is_class,
74
+ is_model=symbol.is_pydantic_model,
75
+ is_enum=symbol.is_enum if hasattr(symbol, "is_enum") else False,
76
+ is_external=symbol.is_external,
77
+ external_package=symbol.external_package,
78
+ base_classes=list(symbol.bases),
79
+ all_ancestors=list(symbol.all_bases) if hasattr(symbol, "all_bases") else [],
80
+ mro=list(symbol.mro) if hasattr(symbol, "mro") else [],
81
+ definition=symbol.definition,
82
+ )
83
+
84
+ def is_model_type(
85
+ self,
86
+ name: str,
87
+ in_file: Path | None = None,
88
+ ) -> bool:
89
+ """Check if type is a Pydantic model."""
90
+ return self._resolver.is_pydantic_model(name, in_file)
91
+
92
+ def get_model_fields(
93
+ self,
94
+ model_name: str,
95
+ in_file: Path | None = None,
96
+ include_inherited: bool = True,
97
+ ) -> list[ResolvedField]:
98
+ """Get fields for a model."""
99
+ raw_fields = self._resolver.get_model_fields(model_name, in_file, include_inherited)
100
+
101
+ return [
102
+ ResolvedField(
103
+ name=f.get("name", ""),
104
+ type_annotation=f.get("type_annotation"),
105
+ default_value=f.get("default_value"),
106
+ is_required=f.get("is_required", True),
107
+ nested_type_name=f.get("nested_model_name"),
108
+ alias=f.get("alias"),
109
+ description=f.get("description"),
110
+ constraints=f.get("constraints", {}),
111
+ defined_in_class=f.get("defined_in"),
112
+ )
113
+ for f in raw_fields
114
+ ]
115
+
116
+ def is_subclass_of(
117
+ self,
118
+ class_name: str,
119
+ base_name: str,
120
+ in_file: Path | None = None,
121
+ ) -> bool:
122
+ """Check inheritance relationship."""
123
+ symbol = self._resolver.resolve_symbol(class_name, in_file)
124
+ if not symbol:
125
+ return False
126
+
127
+ # Check direct bases and all ancestors
128
+ if base_name in symbol.bases:
129
+ return True
130
+ return bool(hasattr(symbol, "all_bases") and base_name in symbol.all_bases)
131
+
132
+ def get_all_models(self) -> dict[str, ResolvedType]:
133
+ """Get all Pydantic models."""
134
+ raw_models = self._resolver.get_all_pydantic_models()
135
+ result = {}
136
+
137
+ for qname, symbol in raw_models.items():
138
+ # symbol may be ParsedClass (from get_all_pydantic_models) or ResolvedSymbol
139
+ file_path = getattr(symbol, "file_path", None) or (
140
+ symbol.location.file if hasattr(symbol, "location") and symbol.location else None
141
+ )
142
+ bases = getattr(symbol, "bases", None) or getattr(symbol, "base_classes", [])
143
+ definition = getattr(symbol, "definition", symbol) # ParsedClass is its own definition
144
+ result[qname] = ResolvedType(
145
+ name=symbol.name,
146
+ qualified_name=symbol.qualified_name.full
147
+ if hasattr(symbol.qualified_name, "full")
148
+ else str(symbol.qualified_name),
149
+ file_path=file_path,
150
+ is_class=True,
151
+ is_model=True,
152
+ base_classes=list(bases),
153
+ definition=definition,
154
+ )
155
+
156
+ return result
157
+
158
+
159
+ class PythonConstantResolverAdapter:
160
+ """
161
+ Adapter that wraps Python ConstantResolver to implement the protocol.
162
+ """
163
+
164
+ def __init__(self, constant_resolver):
165
+ """
166
+ Args:
167
+ constant_resolver: Python ConstantResolver instance
168
+ """
169
+ self._resolver = constant_resolver
170
+
171
+ def resolve(
172
+ self,
173
+ expression: str,
174
+ in_file: Path | None = None,
175
+ ) -> ResolvedConstant | None:
176
+ """Resolve a constant expression."""
177
+ result = self._resolver.resolve(expression, in_file)
178
+ if not result:
179
+ return None
180
+
181
+ return ResolvedConstant(
182
+ name=result.name,
183
+ value=result.value,
184
+ source_file=result.source_file,
185
+ source_line=result.source_line,
186
+ source_type=result.source_type,
187
+ is_default=result.is_default,
188
+ override_sources=result.override_sources,
189
+ confidence=result.confidence,
190
+ )
191
+
192
+ def resolve_value(
193
+ self,
194
+ expression: str,
195
+ in_file: Path | None = None,
196
+ ) -> str | None:
197
+ """Get just the value."""
198
+ return self._resolver.resolve_value(expression, in_file)
199
+
200
+ def get_all_constants(self) -> dict[str, str]:
201
+ """Get all constants."""
202
+ return self._resolver.get_all_constants()
203
+
204
+
205
+ class PythonPathResolverAdapter:
206
+ """
207
+ Adapter that wraps Python PathResolver to implement the protocol.
208
+ """
209
+
210
+ def __init__(self, path_resolver):
211
+ """
212
+ Args:
213
+ path_resolver: Python PathResolver instance
214
+ """
215
+ self._resolver = path_resolver
216
+
217
+ def set_constant_resolver(self, resolver) -> None:
218
+ """Set constant resolver for path variable resolution."""
219
+ # Unwrap if it's an adapter
220
+ if isinstance(resolver, PythonConstantResolverAdapter):
221
+ self._resolver.set_constant_resolver(resolver._resolver)
222
+ elif hasattr(self._resolver, "set_constant_resolver"):
223
+ self._resolver.set_constant_resolver(resolver)
224
+
225
+ def resolve(
226
+ self,
227
+ path_expression: str,
228
+ file_path: Path,
229
+ additional_context: dict[str, str] | None = None,
230
+ ) -> ResolvedPath:
231
+ """Resolve a path expression."""
232
+ result = self._resolver.resolve(path_expression, file_path, additional_context)
233
+
234
+ return ResolvedPath(
235
+ path=result.path,
236
+ confidence=result.resolution_confidence,
237
+ is_fully_resolved=len(result.unresolved_vars) == 0,
238
+ unresolved_vars=result.unresolved_vars,
239
+ notes=result.notes if hasattr(result, "notes") else [],
240
+ )
241
+
242
+ def get_all_constants(self) -> dict[str, str]:
243
+ """Get all path constants."""
244
+ return self._resolver.get_all_constants()
245
+
246
+
247
+ class PythonRouterRegistryAdapter:
248
+ """
249
+ Adapter that wraps Python RouterRegistry to implement the protocol.
250
+ """
251
+
252
+ def __init__(self, router_registry):
253
+ """
254
+ Args:
255
+ router_registry: Python RouterRegistry instance (FastAPI-specific)
256
+ """
257
+ self._registry = router_registry
258
+
259
+ def set_constant_resolver(self, resolver) -> None:
260
+ """Set constant resolver for prefix resolution."""
261
+ if isinstance(resolver, PythonConstantResolverAdapter):
262
+ self._registry.set_constant_resolver(resolver._resolver)
263
+ elif hasattr(self._registry, "set_constant_resolver"):
264
+ self._registry.set_constant_resolver(resolver)
265
+
266
+ def resolve_path(
267
+ self,
268
+ router_var: str,
269
+ route_path: str,
270
+ file_path: Path,
271
+ ) -> str:
272
+ """Resolve full path with prefixes."""
273
+ return self._registry.resolve_path(router_var, route_path, file_path)
274
+
275
+ def get_router(self, name: str, file_path: Path) -> RouterInfo | None:
276
+ """Get router info."""
277
+ routers = self._registry.get_all_routers()
278
+
279
+ # Try qualified lookup
280
+ for qname, router in routers.items():
281
+ if router.name == name and router.file_path == file_path:
282
+ return RouterInfo(
283
+ name=router.name,
284
+ qualified_name=qname,
285
+ file_path=router.file_path,
286
+ router_type=router.router_type,
287
+ prefix=router.prefix,
288
+ tags=router.tags,
289
+ )
290
+
291
+ return None
292
+
293
+ def get_all_routers(self) -> dict[str, RouterInfo]:
294
+ """Get all routers."""
295
+ result = {}
296
+ for qname, router in self._registry.get_all_routers().items():
297
+ result[qname] = RouterInfo(
298
+ name=router.name,
299
+ qualified_name=qname,
300
+ file_path=router.file_path,
301
+ router_type=router.router_type,
302
+ prefix=router.prefix,
303
+ tags=router.tags,
304
+ )
305
+ return result
306
+
307
+ def get_router_dependencies(
308
+ self,
309
+ router_var: str,
310
+ file_path: Path,
311
+ ) -> list[str]:
312
+ """Return accumulated dependency names for a router from the tree."""
313
+ return self._registry.get_router_dependencies(router_var, file_path)
314
+
315
+
316
+ # =============================================================================
317
+ # Python Language Services
318
+ # =============================================================================
319
+
320
+
321
+ class PythonLanguageServices(LanguageServices):
322
+ """
323
+ Python-specific implementation of LanguageServices.
324
+
325
+ This factory builds all Python analysis services and wraps them
326
+ in protocol-compliant adapters.
327
+
328
+ Usage:
329
+ services = PythonLanguageServices()
330
+ context = services.build_context(
331
+ parsed_files,
332
+ project_root,
333
+ framework="fastapi"
334
+ )
335
+ """
336
+
337
+ def build_type_resolver(
338
+ self,
339
+ parsed_files: list[ParsedFile],
340
+ project_root: Path | None = None,
341
+ ) -> TypeResolver:
342
+ """Build Python type resolver (CrossFileResolver)."""
343
+ from .cross_file_resolver import build_cross_file_resolver
344
+
345
+ resolver = build_cross_file_resolver(parsed_files, project_root)
346
+ return PythonTypeResolverAdapter(resolver)
347
+
348
+ def build_constant_resolver(
349
+ self,
350
+ parsed_files: list[ParsedFile],
351
+ project_root: Path | None = None,
352
+ ) -> ConstantResolverProtocol | None:
353
+ """Build Python constant resolver."""
354
+ from .constant_resolver import build_constant_resolver
355
+
356
+ resolver = build_constant_resolver(parsed_files, project_root)
357
+ return PythonConstantResolverAdapter(resolver)
358
+
359
+ def build_path_resolver(
360
+ self,
361
+ parsed_files: list[ParsedFile],
362
+ project_root: Path | None = None,
363
+ ) -> PathResolverProtocol | None:
364
+ """Build Python path resolver."""
365
+ from .path_resolver import PathResolver as PythonPathResolver
366
+
367
+ resolver = PythonPathResolver(project_root)
368
+ for parsed in parsed_files:
369
+ resolver.process_file(parsed)
370
+
371
+ return PythonPathResolverAdapter(resolver)
372
+
373
+ def build_framework_services(
374
+ self,
375
+ framework: str,
376
+ parsed_files: list[ParsedFile],
377
+ project_root: Path | None = None,
378
+ ) -> dict[str, Any]:
379
+ """Build framework-specific services."""
380
+ services = {}
381
+
382
+ if framework.lower() == "fastapi":
383
+ services.update(self._build_fastapi_services(parsed_files, project_root))
384
+ elif framework.lower() == "flask":
385
+ services.update(self._build_flask_services(parsed_files, project_root))
386
+ # Add more frameworks as needed
387
+
388
+ return services
389
+
390
+ def _build_fastapi_services(
391
+ self,
392
+ parsed_files: list[ParsedFile],
393
+ project_root: Path | None,
394
+ ) -> dict[str, Any]:
395
+ """Build FastAPI-specific services."""
396
+ from .cbv_extractor import CBVExtractor
397
+ from .dynamic_route_detector import DynamicRouteDetector
398
+ from .router_registry import build_router_registry
399
+
400
+ # Router registry
401
+ registry = build_router_registry(parsed_files, project_root)
402
+
403
+ # Dynamic route detector
404
+ dynamic_detector = DynamicRouteDetector(project_root)
405
+ for parsed in parsed_files:
406
+ dynamic_detector.process_file(parsed)
407
+
408
+ # CBV extractor
409
+ cbv_extractor = CBVExtractor(project_root)
410
+ for parsed in parsed_files:
411
+ cbv_extractor.process_file(parsed)
412
+
413
+ return {
414
+ "router_registry": PythonRouterRegistryAdapter(registry),
415
+ "dynamic_route_detector": dynamic_detector,
416
+ "cbv_extractor": cbv_extractor,
417
+ # Keep raw registry for direct access if needed
418
+ "_raw_router_registry": registry,
419
+ }
420
+
421
+ def _build_flask_services(
422
+ self,
423
+ parsed_files: list[ParsedFile],
424
+ project_root: Path | None,
425
+ ) -> dict[str, Any]:
426
+ """Build Flask-specific services (placeholder)."""
427
+ # TODO: Implement Flask blueprint registry
428
+ return {}
429
+
430
+ # Override build_context to wire up Python-specific dependencies
431
+ def build_context(
432
+ self,
433
+ parsed_files: list[ParsedFile],
434
+ project_root: Path | None = None,
435
+ framework: str | None = None,
436
+ ) -> AnalysisContext:
437
+ """
438
+ Build complete analysis context for Python.
439
+
440
+ Wires up all services with proper dependencies.
441
+ """
442
+ # Build base services
443
+ type_resolver = self.build_type_resolver(parsed_files, project_root)
444
+ constant_resolver = self.build_constant_resolver(parsed_files, project_root)
445
+ path_resolver = self.build_path_resolver(parsed_files, project_root)
446
+
447
+ # Wire constant resolver into path resolver
448
+ if constant_resolver and path_resolver:
449
+ path_resolver.set_constant_resolver(constant_resolver)
450
+
451
+ # Build framework-specific services
452
+ framework_services = {}
453
+ router_registry = None
454
+
455
+ if framework:
456
+ framework_services = self.build_framework_services(
457
+ framework, parsed_files, project_root
458
+ )
459
+ router_registry = framework_services.get("router_registry")
460
+
461
+ # Wire constant resolver into router registry
462
+ if router_registry and constant_resolver:
463
+ router_registry.set_constant_resolver(constant_resolver)
464
+
465
+ # Store raw resolver for legacy plugins that need direct access
466
+ if isinstance(type_resolver, PythonTypeResolverAdapter):
467
+ framework_services["_raw_cross_file_resolver"] = type_resolver._resolver
468
+
469
+ return AnalysisContext(
470
+ type_resolver=type_resolver,
471
+ constant_resolver=constant_resolver,
472
+ path_resolver=path_resolver,
473
+ router_registry=router_registry,
474
+ project_root=project_root,
475
+ all_parsed_files=parsed_files,
476
+ language_services=framework_services,
477
+ )
478
+
479
+
480
+ # =============================================================================
481
+ # Factory Function
482
+ # =============================================================================
483
+
484
+
485
+ def get_python_language_services() -> PythonLanguageServices:
486
+ """Get a PythonLanguageServices instance."""
487
+ return PythonLanguageServices()