arcgraph 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.
- arcgraph/__init__.py +41 -0
- arcgraph/_build_provenance.json +10 -0
- arcgraph/adapters/__init__.py +6 -0
- arcgraph/adapters/arq_adapter.py +369 -0
- arcgraph/adapters/celery_adapter.py +440 -0
- arcgraph/adapters/common.py +436 -0
- arcgraph/adapters/django_adapter.py +765 -0
- arcgraph/adapters/fastapi_adapter.py +415 -0
- arcgraph/adapters/logging_adapter.py +335 -0
- arcgraph/adapters/mcp_adapter.py +100 -0
- arcgraph/adapters/pydantic_adapter.py +290 -0
- arcgraph/adapters/pytest_adapter.py +221 -0
- arcgraph/adapters/pytest_collection.py +96 -0
- arcgraph/adapters/pytest_fixtures.py +371 -0
- arcgraph/adapters/pytest_types.py +251 -0
- arcgraph/adapters/registry.py +34 -0
- arcgraph/adapters/service_container_adapter.py +548 -0
- arcgraph/adapters/sqlalchemy_adapter.py +446 -0
- arcgraph/adapters/typer_click_adapter.py +292 -0
- arcgraph/analyzers/__init__.py +1 -0
- arcgraph/analyzers/bindings.py +782 -0
- arcgraph/analyzers/calls/__init__.py +6 -0
- arcgraph/analyzers/calls/analyzer.py +2921 -0
- arcgraph/analyzers/calls/constants.py +702 -0
- arcgraph/analyzers/calls/context.py +298 -0
- arcgraph/analyzers/calls/lexical.py +237 -0
- arcgraph/analyzers/coverage.py +826 -0
- arcgraph/analyzers/exports.py +46 -0
- arcgraph/analyzers/har_trace.py +323 -0
- arcgraph/analyzers/imports.py +190 -0
- arcgraph/analyzers/otel_trace.py +352 -0
- arcgraph/analyzers/precise_references.py +1199 -0
- arcgraph/analyzers/python_scopes.py +56 -0
- arcgraph/analyzers/runtime_trace.py +516 -0
- arcgraph/analyzers/similarity.py +430 -0
- arcgraph/analyzers/symbols.py +363 -0
- arcgraph/analyzers/type_unions.py +89 -0
- arcgraph/analyzers/types.py +2293 -0
- arcgraph/assets/__init__.py +1 -0
- arcgraph/assets/workbench/__init__.py +1 -0
- arcgraph/assets/workbench/index.html +2398 -0
- arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
- arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
- arcgraph/change/__init__.py +10 -0
- arcgraph/change/baseline.py +145 -0
- arcgraph/change/changed_regions.py +728 -0
- arcgraph/change/contracts.py +927 -0
- arcgraph/change/errors.py +88 -0
- arcgraph/change/evidence.py +289 -0
- arcgraph/change/graph_delta.py +1422 -0
- arcgraph/change/identities.py +1277 -0
- arcgraph/change/paths.py +234 -0
- arcgraph/change/pins.py +426 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +1030 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1138 -0
- arcgraph/change/trusted_runners.py +154 -0
- arcgraph/change/verdicts.py +141 -0
- arcgraph/change/verification_plan.py +113 -0
- arcgraph/change/verifier.py +579 -0
- arcgraph/core/__init__.py +1 -0
- arcgraph/core/assurance.py +343 -0
- arcgraph/core/binding_metrics.py +75 -0
- arcgraph/core/cleanup.py +701 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +862 -0
- arcgraph/core/expression_kinds.py +11 -0
- arcgraph/core/force_graph_export.py +1125 -0
- arcgraph/core/freshness.py +94 -0
- arcgraph/core/graph_store.py +1099 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +30 -0
- arcgraph/core/operation_lock.py +218 -0
- arcgraph/core/passes.py +142 -0
- arcgraph/core/payload_policy.py +708 -0
- arcgraph/core/precise_references.py +124 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1454 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -0
- arcgraph/core/sharing_retry.py +84 -0
- arcgraph/core/structural.py +260 -0
- arcgraph/core/target_resolver.py +486 -0
- arcgraph/core/type_metrics.py +67 -0
- arcgraph/core/unresolved_classification.py +471 -0
- arcgraph/core/utils.py +100 -0
- arcgraph/core/visual_contract.py +240 -0
- arcgraph/core/visual_slice_engine.py +933 -0
- arcgraph/interfaces/__init__.py +1 -0
- arcgraph/interfaces/agent_capabilities.py +493 -0
- arcgraph/interfaces/agent_help.py +313 -0
- arcgraph/interfaces/benchmark.py +548 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2799 -0
- arcgraph/interfaces/cli_change.py +690 -0
- arcgraph/interfaces/cli_feedback.py +176 -0
- arcgraph/interfaces/cli_ops.py +69 -0
- arcgraph/interfaces/cli_support.py +113 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +601 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +481 -0
- arcgraph/interfaces/docs.py +1255 -0
- arcgraph/interfaces/human_output.py +409 -0
- arcgraph/interfaces/local_state.py +577 -0
- arcgraph/interfaces/mcp_server.py +457 -0
- arcgraph/interfaces/mcp_tools.py +1209 -0
- arcgraph/interfaces/metrics.py +571 -0
- arcgraph/interfaces/ops.py +330 -0
- arcgraph/interfaces/precision.py +1670 -0
- arcgraph/interfaces/project_probe.py +75 -0
- arcgraph/interfaces/reports.py +1121 -0
- arcgraph/interfaces/stdio_encoding.py +29 -0
- arcgraph/interfaces/trace.py +592 -0
- arcgraph/interfaces/trial_feedback.py +519 -0
- arcgraph/interfaces/trial_setup.py +535 -0
- arcgraph/interfaces/visual_server.py +354 -0
- arcgraph/interfaces/visual_slice.py +168 -0
- arcgraph/interfaces/visual_smoke.py +666 -0
- arcgraph/interfaces/workbench.py +576 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +451 -0
- arcgraph/pipeline/external_l3_frontends.py +368 -0
- arcgraph/pipeline/external_protocol.py +358 -0
- arcgraph/pipeline/frontends.py +437 -0
- arcgraph/pipeline/indexer.py +661 -0
- arcgraph/pipeline/openapi_frontend.py +708 -0
- arcgraph/pipeline/python_frontend.py +298 -0
- arcgraph/pipeline/reindexer.py +1333 -0
- arcgraph/pipeline/scip_frontend.py +726 -0
- arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
- arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
- arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
- arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
- arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
- arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
- arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
- arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
- arcgraph/pipeline/typescript_extractor.mjs +177 -0
- arcgraph/pipeline/typescript_frameworks.py +947 -0
- arcgraph/pipeline/typescript_frontend.py +656 -0
- arcgraph/pipeline/typescript_references.mjs +167 -0
- arcgraph/providers/__init__.py +27 -0
- arcgraph/providers/context_provider.py +1258 -0
- arcgraph/providers/flow_payload.py +117 -0
- arcgraph/providers/memory_connector.py +216 -0
- arcgraph/providers/orchestrator.py +475 -0
- arcgraph/providers/relationship_payload.py +102 -0
- arcgraph/providers/risk_provider.py +843 -0
- arcgraph/providers/why_provider.py +349 -0
- arcgraph/version_info.py +246 -0
- arcgraph-0.1.0.dist-info/METADATA +676 -0
- arcgraph-0.1.0.dist-info/RECORD +165 -0
- arcgraph-0.1.0.dist-info/WHEEL +4 -0
- arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""FastAPI route extraction for ArcGraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from arcgraph.adapters.common import (
|
|
9
|
+
AdapterAnalysis,
|
|
10
|
+
ImportAliases,
|
|
11
|
+
NodeIndex,
|
|
12
|
+
SemanticAdapter,
|
|
13
|
+
call_name,
|
|
14
|
+
combine_paths,
|
|
15
|
+
import_aliases,
|
|
16
|
+
keyword,
|
|
17
|
+
literal_str,
|
|
18
|
+
literal_str_list,
|
|
19
|
+
module_node_id,
|
|
20
|
+
resolve_alias,
|
|
21
|
+
resolve_module_name,
|
|
22
|
+
symbol_id_for_def,
|
|
23
|
+
)
|
|
24
|
+
from arcgraph.core.ids import function_id, route_id
|
|
25
|
+
from arcgraph.core.schemas import Edge, Evidence, FileRecord, Node
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class RouteTarget:
|
|
30
|
+
name: str
|
|
31
|
+
prefix: str
|
|
32
|
+
kind: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class RouteRegistration:
|
|
37
|
+
owner_key: str
|
|
38
|
+
owner_name: str
|
|
39
|
+
owner_kind: str
|
|
40
|
+
method: str
|
|
41
|
+
path: str
|
|
42
|
+
source_id: str
|
|
43
|
+
handler_name: str
|
|
44
|
+
dependencies: tuple[str, ...]
|
|
45
|
+
tags: tuple[str, ...]
|
|
46
|
+
file_record: FileRecord
|
|
47
|
+
decorator: ast.AST
|
|
48
|
+
start_line: int
|
|
49
|
+
end_line: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FastAPIAdapter(SemanticAdapter):
|
|
53
|
+
name = "fastapi"
|
|
54
|
+
capabilities = ("entrypoint:route", "edge:invokes", "edge:injects")
|
|
55
|
+
required_facts = ("module", "function")
|
|
56
|
+
|
|
57
|
+
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
|
|
58
|
+
|
|
59
|
+
def analyze(
|
|
60
|
+
self,
|
|
61
|
+
files: list[FileRecord],
|
|
62
|
+
parsed_files: dict[str, ast.Module],
|
|
63
|
+
nodes: list[Node],
|
|
64
|
+
module_names: set[str],
|
|
65
|
+
) -> AdapterAnalysis:
|
|
66
|
+
analysis = AdapterAnalysis()
|
|
67
|
+
node_index = NodeIndex(nodes)
|
|
68
|
+
registrations_by_owner: dict[str, list[RouteRegistration]] = {}
|
|
69
|
+
|
|
70
|
+
for file_record in files:
|
|
71
|
+
tree = parsed_files.get(file_record.path)
|
|
72
|
+
if tree is None:
|
|
73
|
+
continue
|
|
74
|
+
imports = import_aliases(file_record, tree, module_names)
|
|
75
|
+
route_targets = self._route_targets(tree, imports)
|
|
76
|
+
|
|
77
|
+
for stmt in tree.body:
|
|
78
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
79
|
+
source_id = symbol_id_for_def(file_record, stmt.name, function_id)
|
|
80
|
+
for decorator in stmt.decorator_list:
|
|
81
|
+
route_call = self._route_call(decorator, route_targets)
|
|
82
|
+
if route_call is None:
|
|
83
|
+
continue
|
|
84
|
+
route_target, method, call = route_call
|
|
85
|
+
route_path = combine_paths(
|
|
86
|
+
route_target.prefix,
|
|
87
|
+
self._route_path(call),
|
|
88
|
+
)
|
|
89
|
+
dependencies = sorted(
|
|
90
|
+
set(
|
|
91
|
+
self._dependencies_from_route(call)
|
|
92
|
+
+ self._dependencies_from_function(stmt)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
registration = RouteRegistration(
|
|
96
|
+
owner_key=self._owner_key(
|
|
97
|
+
file_record.module, route_target.name
|
|
98
|
+
),
|
|
99
|
+
owner_name=route_target.name,
|
|
100
|
+
owner_kind=route_target.kind,
|
|
101
|
+
method=method,
|
|
102
|
+
path=route_path,
|
|
103
|
+
source_id=source_id,
|
|
104
|
+
handler_name=stmt.name,
|
|
105
|
+
dependencies=tuple(dependencies),
|
|
106
|
+
tags=tuple(literal_str_list(keyword(call, "tags"))),
|
|
107
|
+
file_record=file_record,
|
|
108
|
+
decorator=decorator,
|
|
109
|
+
start_line=getattr(decorator, "lineno", stmt.lineno),
|
|
110
|
+
end_line=getattr(stmt, "end_lineno", stmt.lineno),
|
|
111
|
+
)
|
|
112
|
+
registrations_by_owner.setdefault(
|
|
113
|
+
registration.owner_key, []
|
|
114
|
+
).append(registration)
|
|
115
|
+
|
|
116
|
+
for registrations in registrations_by_owner.values():
|
|
117
|
+
for registration in registrations:
|
|
118
|
+
imports = import_aliases(
|
|
119
|
+
registration.file_record,
|
|
120
|
+
parsed_files[registration.file_record.path],
|
|
121
|
+
module_names,
|
|
122
|
+
)
|
|
123
|
+
self._add_route(analysis, node_index, registration, imports)
|
|
124
|
+
|
|
125
|
+
for file_record in files:
|
|
126
|
+
tree = parsed_files.get(file_record.path)
|
|
127
|
+
if tree is None:
|
|
128
|
+
continue
|
|
129
|
+
imports = import_aliases(file_record, tree, module_names)
|
|
130
|
+
route_targets = self._route_targets(tree, imports)
|
|
131
|
+
|
|
132
|
+
self._include_router_edges(
|
|
133
|
+
analysis,
|
|
134
|
+
node_index,
|
|
135
|
+
file_record,
|
|
136
|
+
tree,
|
|
137
|
+
imports,
|
|
138
|
+
module_names,
|
|
139
|
+
parsed_files,
|
|
140
|
+
route_targets,
|
|
141
|
+
registrations_by_owner,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
analysis.nodes.sort(key=lambda node: node.id)
|
|
145
|
+
analysis.edges.sort(key=lambda edge: (edge.source, edge.target, edge.kind))
|
|
146
|
+
return analysis
|
|
147
|
+
|
|
148
|
+
def _route_targets(
|
|
149
|
+
self, tree: ast.Module, imports: ImportAliases
|
|
150
|
+
) -> dict[str, RouteTarget]:
|
|
151
|
+
targets: dict[str, RouteTarget] = {}
|
|
152
|
+
for stmt in tree.body:
|
|
153
|
+
if not isinstance(stmt, ast.Assign):
|
|
154
|
+
continue
|
|
155
|
+
if not isinstance(stmt.value, ast.Call):
|
|
156
|
+
continue
|
|
157
|
+
factory = resolve_alias(call_name(stmt.value.func), imports)
|
|
158
|
+
short = factory.rsplit(".", 1)[-1] if factory else None
|
|
159
|
+
if short not in {"APIRouter", "FastAPI"}:
|
|
160
|
+
continue
|
|
161
|
+
prefix = (
|
|
162
|
+
literal_str(keyword(stmt.value, "prefix"))
|
|
163
|
+
if short == "APIRouter"
|
|
164
|
+
else None
|
|
165
|
+
) or ""
|
|
166
|
+
kind = "router" if short == "APIRouter" else "app"
|
|
167
|
+
for target in stmt.targets:
|
|
168
|
+
if isinstance(target, ast.Name):
|
|
169
|
+
targets[target.id] = RouteTarget(
|
|
170
|
+
name=target.id,
|
|
171
|
+
prefix=prefix,
|
|
172
|
+
kind=kind,
|
|
173
|
+
)
|
|
174
|
+
return targets
|
|
175
|
+
|
|
176
|
+
def _route_call(
|
|
177
|
+
self, decorator: ast.expr, route_targets: dict[str, RouteTarget]
|
|
178
|
+
) -> tuple[RouteTarget, str, ast.Call] | None:
|
|
179
|
+
if not isinstance(decorator, ast.Call):
|
|
180
|
+
return None
|
|
181
|
+
if not isinstance(decorator.func, ast.Attribute):
|
|
182
|
+
return None
|
|
183
|
+
method = decorator.func.attr
|
|
184
|
+
if method not in self.HTTP_METHODS:
|
|
185
|
+
return None
|
|
186
|
+
if not isinstance(decorator.func.value, ast.Name):
|
|
187
|
+
return None
|
|
188
|
+
target_name = decorator.func.value.id
|
|
189
|
+
route_target = route_targets.get(target_name)
|
|
190
|
+
if route_target is None:
|
|
191
|
+
return None
|
|
192
|
+
return route_target, method, decorator
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _route_path(call: ast.Call) -> str:
|
|
196
|
+
if call.args:
|
|
197
|
+
return literal_str(call.args[0]) or ""
|
|
198
|
+
return literal_str(keyword(call, "path")) or ""
|
|
199
|
+
|
|
200
|
+
def _dependencies_from_route(self, call: ast.Call) -> list[str]:
|
|
201
|
+
values: list[str] = []
|
|
202
|
+
dependencies = keyword(call, "dependencies")
|
|
203
|
+
if isinstance(dependencies, (ast.List, ast.Tuple)):
|
|
204
|
+
for item in dependencies.elts:
|
|
205
|
+
dependency = self._dependency_name(item)
|
|
206
|
+
if dependency:
|
|
207
|
+
values.append(dependency)
|
|
208
|
+
return values
|
|
209
|
+
|
|
210
|
+
def _dependencies_from_function(
|
|
211
|
+
self, stmt: ast.FunctionDef | ast.AsyncFunctionDef
|
|
212
|
+
) -> list[str]:
|
|
213
|
+
values: list[str] = []
|
|
214
|
+
positional_args = stmt.args.posonlyargs + stmt.args.args
|
|
215
|
+
defaults = [None] * (len(positional_args) - len(stmt.args.defaults)) + list(
|
|
216
|
+
stmt.args.defaults
|
|
217
|
+
)
|
|
218
|
+
for default in [*defaults, *stmt.args.kw_defaults]:
|
|
219
|
+
if default is None:
|
|
220
|
+
continue
|
|
221
|
+
dependency = self._dependency_name(default)
|
|
222
|
+
if dependency:
|
|
223
|
+
values.append(dependency)
|
|
224
|
+
return values
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _dependency_name(node: ast.AST) -> str | None:
|
|
228
|
+
if not isinstance(node, ast.Call):
|
|
229
|
+
return None
|
|
230
|
+
dependency_func = call_name(node.func)
|
|
231
|
+
dependency_kind = dependency_func.rsplit(".", 1)[-1] if dependency_func else ""
|
|
232
|
+
if dependency_kind not in {"Depends", "Security"} or not node.args:
|
|
233
|
+
return None
|
|
234
|
+
return call_name(node.args[0])
|
|
235
|
+
|
|
236
|
+
def _include_router_edges(
|
|
237
|
+
self,
|
|
238
|
+
analysis: AdapterAnalysis,
|
|
239
|
+
node_index: NodeIndex,
|
|
240
|
+
file_record: FileRecord,
|
|
241
|
+
tree: ast.Module,
|
|
242
|
+
imports: ImportAliases,
|
|
243
|
+
module_names: set[str],
|
|
244
|
+
parsed_files: dict[str, ast.Module],
|
|
245
|
+
route_targets: dict[str, RouteTarget],
|
|
246
|
+
registrations_by_owner: dict[str, list[RouteRegistration]],
|
|
247
|
+
) -> None:
|
|
248
|
+
for node in ast.walk(tree):
|
|
249
|
+
if not isinstance(node, ast.Call):
|
|
250
|
+
continue
|
|
251
|
+
if not self._is_include_router_call(node, route_targets):
|
|
252
|
+
continue
|
|
253
|
+
if not node.args:
|
|
254
|
+
continue
|
|
255
|
+
router_ref = call_name(node.args[0])
|
|
256
|
+
if not router_ref:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
module_name = resolve_module_name(router_ref, imports)
|
|
260
|
+
if module_name in module_names:
|
|
261
|
+
analysis.edges.append(
|
|
262
|
+
self._edge(
|
|
263
|
+
module_node_id(file_record),
|
|
264
|
+
f"mod:{module_name}",
|
|
265
|
+
"includes_router",
|
|
266
|
+
file_record,
|
|
267
|
+
node,
|
|
268
|
+
f"include_router({router_ref})",
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
include_prefix = literal_str(keyword(node, "prefix")) or ""
|
|
273
|
+
if not include_prefix:
|
|
274
|
+
continue
|
|
275
|
+
for owner_key in self._router_owner_keys(router_ref, imports):
|
|
276
|
+
for registration in registrations_by_owner.get(owner_key, []):
|
|
277
|
+
prefixed = self._with_include_prefix(registration, include_prefix)
|
|
278
|
+
if prefixed.path != registration.path:
|
|
279
|
+
reg_imports = import_aliases(
|
|
280
|
+
registration.file_record,
|
|
281
|
+
parsed_files[registration.file_record.path],
|
|
282
|
+
module_names,
|
|
283
|
+
)
|
|
284
|
+
self._add_route(analysis, node_index, prefixed, reg_imports)
|
|
285
|
+
|
|
286
|
+
def _add_route(
|
|
287
|
+
self,
|
|
288
|
+
analysis: AdapterAnalysis,
|
|
289
|
+
node_index: NodeIndex,
|
|
290
|
+
registration: RouteRegistration,
|
|
291
|
+
imports: ImportAliases,
|
|
292
|
+
) -> None:
|
|
293
|
+
node_id = route_id(registration.method, registration.path)
|
|
294
|
+
analysis.nodes.append(
|
|
295
|
+
Node(
|
|
296
|
+
id=node_id,
|
|
297
|
+
kind="route",
|
|
298
|
+
name=f"{registration.method.upper()} {registration.path}",
|
|
299
|
+
qualname=f"{registration.method.upper()} {registration.path}",
|
|
300
|
+
path=registration.file_record.path,
|
|
301
|
+
start_line=registration.start_line,
|
|
302
|
+
end_line=registration.end_line,
|
|
303
|
+
properties={
|
|
304
|
+
"method": registration.method.upper(),
|
|
305
|
+
"path": registration.path,
|
|
306
|
+
"router": registration.owner_name,
|
|
307
|
+
"router_kind": registration.owner_kind,
|
|
308
|
+
"handler": registration.source_id,
|
|
309
|
+
"dependencies": list(registration.dependencies),
|
|
310
|
+
"tags": list(registration.tags),
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
analysis.edges.append(
|
|
315
|
+
self._edge(
|
|
316
|
+
node_id,
|
|
317
|
+
registration.source_id,
|
|
318
|
+
"invokes",
|
|
319
|
+
registration.file_record,
|
|
320
|
+
registration.decorator,
|
|
321
|
+
(
|
|
322
|
+
f"{registration.method.upper()} {registration.path} "
|
|
323
|
+
f"-> {registration.handler_name}"
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
for dependency in registration.dependencies:
|
|
328
|
+
target_id = node_index.function_id_for(
|
|
329
|
+
dependency, registration.file_record, imports
|
|
330
|
+
)
|
|
331
|
+
if target_id:
|
|
332
|
+
analysis.edges.append(
|
|
333
|
+
self._edge(
|
|
334
|
+
node_id,
|
|
335
|
+
target_id,
|
|
336
|
+
"injects",
|
|
337
|
+
registration.file_record,
|
|
338
|
+
registration.decorator,
|
|
339
|
+
f"Depends({dependency})",
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _is_include_router_call(
|
|
345
|
+
node: ast.Call, route_targets: dict[str, RouteTarget]
|
|
346
|
+
) -> bool:
|
|
347
|
+
if not isinstance(node.func, ast.Attribute):
|
|
348
|
+
return False
|
|
349
|
+
if node.func.attr != "include_router":
|
|
350
|
+
return False
|
|
351
|
+
receiver = call_name(node.func.value)
|
|
352
|
+
return bool(receiver and route_targets.get(receiver, None))
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def _with_include_prefix(
|
|
356
|
+
registration: RouteRegistration, include_prefix: str
|
|
357
|
+
) -> RouteRegistration:
|
|
358
|
+
return RouteRegistration(
|
|
359
|
+
owner_key=registration.owner_key,
|
|
360
|
+
owner_name=registration.owner_name,
|
|
361
|
+
owner_kind=registration.owner_kind,
|
|
362
|
+
method=registration.method,
|
|
363
|
+
path=combine_paths(include_prefix, registration.path),
|
|
364
|
+
source_id=registration.source_id,
|
|
365
|
+
handler_name=registration.handler_name,
|
|
366
|
+
dependencies=registration.dependencies,
|
|
367
|
+
tags=registration.tags,
|
|
368
|
+
file_record=registration.file_record,
|
|
369
|
+
decorator=registration.decorator,
|
|
370
|
+
start_line=registration.start_line,
|
|
371
|
+
end_line=registration.end_line,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _router_owner_keys(router_ref: str, imports: ImportAliases) -> list[str]:
|
|
376
|
+
keys: list[str] = []
|
|
377
|
+
module_name = resolve_module_name(router_ref, imports)
|
|
378
|
+
variable_name = router_ref.rsplit(".", 1)[-1]
|
|
379
|
+
if module_name:
|
|
380
|
+
keys.append(FastAPIAdapter._owner_key(module_name, variable_name))
|
|
381
|
+
resolved = resolve_alias(router_ref, imports)
|
|
382
|
+
if resolved and "." in resolved:
|
|
383
|
+
module, variable = resolved.rsplit(".", 1)
|
|
384
|
+
keys.append(FastAPIAdapter._owner_key(module, variable))
|
|
385
|
+
return list(dict.fromkeys(keys))
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def _owner_key(module: str, name: str) -> str:
|
|
389
|
+
return f"{module}.{name}"
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _edge(
|
|
393
|
+
source: str,
|
|
394
|
+
target: str,
|
|
395
|
+
kind: str,
|
|
396
|
+
file_record: FileRecord,
|
|
397
|
+
node: ast.AST,
|
|
398
|
+
detail: str,
|
|
399
|
+
) -> Edge:
|
|
400
|
+
return Edge(
|
|
401
|
+
source=source,
|
|
402
|
+
target=target,
|
|
403
|
+
kind=kind,
|
|
404
|
+
confidence="confirmed",
|
|
405
|
+
evidence=[
|
|
406
|
+
Evidence(
|
|
407
|
+
kind="fastapi_route",
|
|
408
|
+
path=file_record.path,
|
|
409
|
+
start_line=getattr(node, "lineno", None),
|
|
410
|
+
end_line=getattr(node, "end_lineno", getattr(node, "lineno", None)),
|
|
411
|
+
column=getattr(node, "col_offset", None),
|
|
412
|
+
detail=detail,
|
|
413
|
+
)
|
|
414
|
+
],
|
|
415
|
+
)
|