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.
Files changed (165) hide show
  1. arcgraph/__init__.py +41 -0
  2. arcgraph/_build_provenance.json +10 -0
  3. arcgraph/adapters/__init__.py +6 -0
  4. arcgraph/adapters/arq_adapter.py +369 -0
  5. arcgraph/adapters/celery_adapter.py +440 -0
  6. arcgraph/adapters/common.py +436 -0
  7. arcgraph/adapters/django_adapter.py +765 -0
  8. arcgraph/adapters/fastapi_adapter.py +415 -0
  9. arcgraph/adapters/logging_adapter.py +335 -0
  10. arcgraph/adapters/mcp_adapter.py +100 -0
  11. arcgraph/adapters/pydantic_adapter.py +290 -0
  12. arcgraph/adapters/pytest_adapter.py +221 -0
  13. arcgraph/adapters/pytest_collection.py +96 -0
  14. arcgraph/adapters/pytest_fixtures.py +371 -0
  15. arcgraph/adapters/pytest_types.py +251 -0
  16. arcgraph/adapters/registry.py +34 -0
  17. arcgraph/adapters/service_container_adapter.py +548 -0
  18. arcgraph/adapters/sqlalchemy_adapter.py +446 -0
  19. arcgraph/adapters/typer_click_adapter.py +292 -0
  20. arcgraph/analyzers/__init__.py +1 -0
  21. arcgraph/analyzers/bindings.py +782 -0
  22. arcgraph/analyzers/calls/__init__.py +6 -0
  23. arcgraph/analyzers/calls/analyzer.py +2921 -0
  24. arcgraph/analyzers/calls/constants.py +702 -0
  25. arcgraph/analyzers/calls/context.py +298 -0
  26. arcgraph/analyzers/calls/lexical.py +237 -0
  27. arcgraph/analyzers/coverage.py +826 -0
  28. arcgraph/analyzers/exports.py +46 -0
  29. arcgraph/analyzers/har_trace.py +323 -0
  30. arcgraph/analyzers/imports.py +190 -0
  31. arcgraph/analyzers/otel_trace.py +352 -0
  32. arcgraph/analyzers/precise_references.py +1199 -0
  33. arcgraph/analyzers/python_scopes.py +56 -0
  34. arcgraph/analyzers/runtime_trace.py +516 -0
  35. arcgraph/analyzers/similarity.py +430 -0
  36. arcgraph/analyzers/symbols.py +363 -0
  37. arcgraph/analyzers/type_unions.py +89 -0
  38. arcgraph/analyzers/types.py +2293 -0
  39. arcgraph/assets/__init__.py +1 -0
  40. arcgraph/assets/workbench/__init__.py +1 -0
  41. arcgraph/assets/workbench/index.html +2398 -0
  42. arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
  43. arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
  44. arcgraph/change/__init__.py +10 -0
  45. arcgraph/change/baseline.py +145 -0
  46. arcgraph/change/changed_regions.py +728 -0
  47. arcgraph/change/contracts.py +927 -0
  48. arcgraph/change/errors.py +88 -0
  49. arcgraph/change/evidence.py +289 -0
  50. arcgraph/change/graph_delta.py +1422 -0
  51. arcgraph/change/identities.py +1277 -0
  52. arcgraph/change/paths.py +234 -0
  53. arcgraph/change/pins.py +426 -0
  54. arcgraph/change/planner.py +504 -0
  55. arcgraph/change/protected_surfaces.py +328 -0
  56. arcgraph/change/provider.py +46 -0
  57. arcgraph/change/service.py +1030 -0
  58. arcgraph/change/source_provider.py +311 -0
  59. arcgraph/change/store.py +1138 -0
  60. arcgraph/change/trusted_runners.py +154 -0
  61. arcgraph/change/verdicts.py +141 -0
  62. arcgraph/change/verification_plan.py +113 -0
  63. arcgraph/change/verifier.py +579 -0
  64. arcgraph/core/__init__.py +1 -0
  65. arcgraph/core/assurance.py +343 -0
  66. arcgraph/core/binding_metrics.py +75 -0
  67. arcgraph/core/cleanup.py +701 -0
  68. arcgraph/core/confidence_profiles.py +108 -0
  69. arcgraph/core/evidence_manifest.py +862 -0
  70. arcgraph/core/expression_kinds.py +11 -0
  71. arcgraph/core/force_graph_export.py +1125 -0
  72. arcgraph/core/freshness.py +94 -0
  73. arcgraph/core/graph_store.py +1099 -0
  74. arcgraph/core/ids.py +206 -0
  75. arcgraph/core/language_tiers.py +150 -0
  76. arcgraph/core/merge.py +649 -0
  77. arcgraph/core/metadata.py +30 -0
  78. arcgraph/core/operation_lock.py +218 -0
  79. arcgraph/core/passes.py +142 -0
  80. arcgraph/core/payload_policy.py +708 -0
  81. arcgraph/core/precise_references.py +124 -0
  82. arcgraph/core/query_engine.py +3078 -0
  83. arcgraph/core/recovery.py +18 -0
  84. arcgraph/core/scanner.py +1454 -0
  85. arcgraph/core/schemas.py +700 -0
  86. arcgraph/core/semantic.py +263 -0
  87. arcgraph/core/semantic_metrics.py +619 -0
  88. arcgraph/core/sharing_retry.py +84 -0
  89. arcgraph/core/structural.py +260 -0
  90. arcgraph/core/target_resolver.py +486 -0
  91. arcgraph/core/type_metrics.py +67 -0
  92. arcgraph/core/unresolved_classification.py +471 -0
  93. arcgraph/core/utils.py +100 -0
  94. arcgraph/core/visual_contract.py +240 -0
  95. arcgraph/core/visual_slice_engine.py +933 -0
  96. arcgraph/interfaces/__init__.py +1 -0
  97. arcgraph/interfaces/agent_capabilities.py +493 -0
  98. arcgraph/interfaces/agent_help.py +313 -0
  99. arcgraph/interfaces/benchmark.py +548 -0
  100. arcgraph/interfaces/ci.py +1992 -0
  101. arcgraph/interfaces/cli.py +2799 -0
  102. arcgraph/interfaces/cli_change.py +690 -0
  103. arcgraph/interfaces/cli_feedback.py +176 -0
  104. arcgraph/interfaces/cli_ops.py +69 -0
  105. arcgraph/interfaces/cli_support.py +113 -0
  106. arcgraph/interfaces/cli_trial.py +42 -0
  107. arcgraph/interfaces/cli_visual.py +601 -0
  108. arcgraph/interfaces/cli_workspace.py +300 -0
  109. arcgraph/interfaces/client_setup.py +481 -0
  110. arcgraph/interfaces/docs.py +1255 -0
  111. arcgraph/interfaces/human_output.py +409 -0
  112. arcgraph/interfaces/local_state.py +577 -0
  113. arcgraph/interfaces/mcp_server.py +457 -0
  114. arcgraph/interfaces/mcp_tools.py +1209 -0
  115. arcgraph/interfaces/metrics.py +571 -0
  116. arcgraph/interfaces/ops.py +330 -0
  117. arcgraph/interfaces/precision.py +1670 -0
  118. arcgraph/interfaces/project_probe.py +75 -0
  119. arcgraph/interfaces/reports.py +1121 -0
  120. arcgraph/interfaces/stdio_encoding.py +29 -0
  121. arcgraph/interfaces/trace.py +592 -0
  122. arcgraph/interfaces/trial_feedback.py +519 -0
  123. arcgraph/interfaces/trial_setup.py +535 -0
  124. arcgraph/interfaces/visual_server.py +354 -0
  125. arcgraph/interfaces/visual_slice.py +168 -0
  126. arcgraph/interfaces/visual_smoke.py +666 -0
  127. arcgraph/interfaces/workbench.py +576 -0
  128. arcgraph/interfaces/workspace.py +1513 -0
  129. arcgraph/pipeline/__init__.py +1 -0
  130. arcgraph/pipeline/contracts.py +122 -0
  131. arcgraph/pipeline/external_frontend.py +451 -0
  132. arcgraph/pipeline/external_l3_frontends.py +368 -0
  133. arcgraph/pipeline/external_protocol.py +358 -0
  134. arcgraph/pipeline/frontends.py +437 -0
  135. arcgraph/pipeline/indexer.py +661 -0
  136. arcgraph/pipeline/openapi_frontend.py +708 -0
  137. arcgraph/pipeline/python_frontend.py +298 -0
  138. arcgraph/pipeline/reindexer.py +1333 -0
  139. arcgraph/pipeline/scip_frontend.py +726 -0
  140. arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
  141. arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
  142. arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
  143. arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
  144. arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
  145. arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
  146. arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
  147. arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
  148. arcgraph/pipeline/typescript_extractor.mjs +177 -0
  149. arcgraph/pipeline/typescript_frameworks.py +947 -0
  150. arcgraph/pipeline/typescript_frontend.py +656 -0
  151. arcgraph/pipeline/typescript_references.mjs +167 -0
  152. arcgraph/providers/__init__.py +27 -0
  153. arcgraph/providers/context_provider.py +1258 -0
  154. arcgraph/providers/flow_payload.py +117 -0
  155. arcgraph/providers/memory_connector.py +216 -0
  156. arcgraph/providers/orchestrator.py +475 -0
  157. arcgraph/providers/relationship_payload.py +102 -0
  158. arcgraph/providers/risk_provider.py +843 -0
  159. arcgraph/providers/why_provider.py +349 -0
  160. arcgraph/version_info.py +246 -0
  161. arcgraph-0.1.0.dist-info/METADATA +676 -0
  162. arcgraph-0.1.0.dist-info/RECORD +165 -0
  163. arcgraph-0.1.0.dist-info/WHEEL +4 -0
  164. arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
  165. 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
+ )