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,440 @@
1
+ """Celery task and queue extraction for ArcGraph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+
7
+ from arcgraph.adapters.common import (
8
+ AdapterAnalysis,
9
+ ImportAliases,
10
+ SemanticAdapter,
11
+ call_name,
12
+ import_aliases,
13
+ keyword,
14
+ literal_str,
15
+ module_node_id,
16
+ resolve_alias,
17
+ )
18
+ from arcgraph.core.ids import function_id, queue_id, worker_id
19
+ from arcgraph.core.schemas import Edge, Evidence, FactResolution, FileRecord, Node
20
+
21
+
22
+ class CeleryAdapter(SemanticAdapter):
23
+ """Extract Celery tasks, queues, and enqueue edges."""
24
+
25
+ name = "celery"
26
+ capabilities = ("entrypoint:worker_task", "resource:queue", "edge:enqueues")
27
+ required_facts = ("function",)
28
+
29
+ _TASK_DECORATORS = {"task", "shared_task"}
30
+ _ENQUEUE_METHODS = {"delay", "apply_async", "s", "si", "signature"}
31
+ _SEND_FUNCTIONS = {"send_task"}
32
+
33
+ # ── detect ──────────────────────────────────────────────
34
+
35
+ def detect(
36
+ self,
37
+ files: list[FileRecord],
38
+ parsed_files: dict[str, ast.Module],
39
+ nodes: list[Node],
40
+ module_names: set[str],
41
+ ) -> bool:
42
+ del nodes, module_names
43
+ for file_record in files:
44
+ tree = parsed_files.get(file_record.path)
45
+ if tree is None:
46
+ continue
47
+ for stmt in tree.body:
48
+ if isinstance(stmt, ast.ImportFrom) and stmt.module:
49
+ if stmt.module.startswith("celery"):
50
+ return True
51
+ if isinstance(stmt, ast.Import):
52
+ if any(a.name.startswith("celery") for a in stmt.names):
53
+ return True
54
+ return False
55
+
56
+ # ── analyze ─────────────────────────────────────────────
57
+
58
+ def analyze(
59
+ self,
60
+ files: list[FileRecord],
61
+ parsed_files: dict[str, ast.Module],
62
+ nodes: list[Node],
63
+ module_names: set[str],
64
+ ) -> AdapterAnalysis:
65
+ del nodes
66
+ analysis = AdapterAnalysis()
67
+
68
+ # Pass 1: find all Celery app instances
69
+ app_names: dict[str, set[str]] = {} # file path -> set of app variable names
70
+ for file_record in files:
71
+ tree = parsed_files.get(file_record.path)
72
+ if tree is None:
73
+ continue
74
+ app_names[file_record.path] = self._find_celery_apps(tree)
75
+
76
+ # Pass 2: extract @app.task / @shared_task decorated functions
77
+ task_registry: dict[str, str] = {} # task_name -> worker node id
78
+ for file_record in files:
79
+ tree = parsed_files.get(file_record.path)
80
+ if tree is None:
81
+ continue
82
+ imports = import_aliases(file_record, tree, module_names)
83
+ apps = app_names.get(file_record.path, set())
84
+ self._extract_tasks(
85
+ file_record,
86
+ tree,
87
+ imports,
88
+ apps,
89
+ task_registry,
90
+ analysis,
91
+ )
92
+
93
+ # Pass 3: find enqueue calls (task.delay(), task.apply_async(), send_task())
94
+ for file_record in files:
95
+ tree = parsed_files.get(file_record.path)
96
+ if tree is None:
97
+ continue
98
+ imports = import_aliases(file_record, tree, module_names)
99
+ apps = app_names.get(file_record.path, set())
100
+ self._extract_enqueue_edges(
101
+ file_record,
102
+ tree,
103
+ imports,
104
+ apps,
105
+ task_registry,
106
+ analysis,
107
+ )
108
+
109
+ analysis.nodes.sort(key=lambda n: n.id)
110
+ analysis.edges.sort(key=lambda e: (e.source, e.target, e.kind))
111
+ return analysis
112
+
113
+ # ── celery apps ─────────────────────────────────────────
114
+
115
+ @staticmethod
116
+ def _find_celery_apps(tree: ast.Module) -> set[str]:
117
+ """Find variable names assigned a Celery() instance."""
118
+ apps: set[str] = set()
119
+ for stmt in tree.body:
120
+ if not isinstance(stmt, ast.Assign):
121
+ continue
122
+ if not isinstance(stmt.value, ast.Call):
123
+ continue
124
+ func = call_name(stmt.value.func)
125
+ if func and func.rsplit(".", 1)[-1] == "Celery":
126
+ for target in stmt.targets:
127
+ if isinstance(target, ast.Name):
128
+ apps.add(target.id)
129
+ return apps
130
+
131
+ # ── task extraction ─────────────────────────────────────
132
+
133
+ def _extract_tasks(
134
+ self,
135
+ file_record: FileRecord,
136
+ tree: ast.Module,
137
+ imports: ImportAliases,
138
+ apps: set[str],
139
+ task_registry: dict[str, str],
140
+ analysis: AdapterAnalysis,
141
+ ) -> None:
142
+ for stmt in tree.body:
143
+ if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
144
+ continue
145
+ task_info = self._task_decorator_info(stmt, imports, apps)
146
+ if task_info is None:
147
+ continue
148
+
149
+ task_name, queue_name, bind, decorator_node = task_info
150
+
151
+ # Default task name follows Celery convention: module.function_name
152
+ if not task_name:
153
+ task_name = f"{file_record.module}.{stmt.name}"
154
+
155
+ # Default queue
156
+ if not queue_name:
157
+ queue_name = "celery"
158
+
159
+ q_id = queue_id(f"celery:{queue_name}")
160
+ w_id = worker_id(f"celery:{task_name}")
161
+ handler_id = function_id(f"{file_record.module}.{stmt.name}")
162
+
163
+ task_registry[task_name] = w_id
164
+ # Also register by short name for .delay() resolution
165
+ task_registry[stmt.name] = w_id
166
+
167
+ # Ensure queue node exists (dedup handled by AdapterRegistry)
168
+ analysis.nodes.append(
169
+ Node(
170
+ id=q_id,
171
+ kind="queue",
172
+ name=queue_name,
173
+ qualname=f"celery:{queue_name}",
174
+ path=file_record.path,
175
+ start_line=stmt.lineno,
176
+ end_line=getattr(stmt, "end_lineno", stmt.lineno),
177
+ properties={"backend": "celery"},
178
+ )
179
+ )
180
+
181
+ analysis.nodes.append(
182
+ Node(
183
+ id=w_id,
184
+ kind="worker_task",
185
+ name=stmt.name,
186
+ qualname=task_name,
187
+ path=file_record.path,
188
+ start_line=stmt.lineno,
189
+ end_line=getattr(stmt, "end_lineno", stmt.lineno),
190
+ properties={
191
+ "queue_name": queue_name,
192
+ "handler": handler_id,
193
+ "bind": bind,
194
+ "celery_task_name": task_name,
195
+ },
196
+ )
197
+ )
198
+
199
+ # worker_task -> handler function
200
+ analysis.edges.append(
201
+ self._edge(
202
+ w_id,
203
+ handler_id,
204
+ "invokes",
205
+ file_record,
206
+ decorator_node,
207
+ f"@task {task_name} -> {stmt.name}",
208
+ )
209
+ )
210
+ # worker_task -> queue
211
+ analysis.edges.append(
212
+ self._edge(
213
+ w_id,
214
+ q_id,
215
+ "consumes",
216
+ file_record,
217
+ decorator_node,
218
+ f"task {task_name} consumes queue {queue_name}",
219
+ )
220
+ )
221
+
222
+ def _task_decorator_info(
223
+ self,
224
+ stmt: ast.FunctionDef | ast.AsyncFunctionDef,
225
+ imports: ImportAliases,
226
+ apps: set[str],
227
+ ) -> tuple[str | None, str | None, bool, ast.AST] | None:
228
+ """Check if function has @app.task or @shared_task. Returns (name, queue, bind, node)."""
229
+ for decorator in stmt.decorator_list:
230
+ # @shared_task or @app.task (no-call form)
231
+ if isinstance(decorator, ast.Name):
232
+ resolved = resolve_alias(decorator.id, imports) or decorator.id
233
+ short = resolved.rsplit(".", 1)[-1]
234
+ if short in self._TASK_DECORATORS:
235
+ return None, None, False, decorator
236
+
237
+ # @app.task or @shared_task (as attribute, no call)
238
+ if isinstance(decorator, ast.Attribute):
239
+ if (
240
+ isinstance(decorator.value, ast.Name)
241
+ and decorator.value.id in apps
242
+ and decorator.attr == "task"
243
+ ):
244
+ return None, None, False, decorator
245
+
246
+ # @shared_task(...) or @app.task(...)
247
+ if isinstance(decorator, ast.Call):
248
+ func = decorator.func
249
+ is_task = False
250
+
251
+ if isinstance(func, ast.Name):
252
+ resolved = resolve_alias(func.id, imports) or func.id
253
+ short = resolved.rsplit(".", 1)[-1]
254
+ is_task = short in self._TASK_DECORATORS
255
+ elif isinstance(func, ast.Attribute):
256
+ if (
257
+ isinstance(func.value, ast.Name)
258
+ and func.value.id in apps
259
+ and func.attr == "task"
260
+ ):
261
+ is_task = True
262
+ elif func.attr == "shared_task":
263
+ is_task = True
264
+
265
+ if is_task:
266
+ task_name = literal_str(keyword(decorator, "name"))
267
+ queue_name = literal_str(keyword(decorator, "queue"))
268
+ bind = self._bool_keyword(decorator, "bind")
269
+ return task_name, queue_name, bind, decorator
270
+
271
+ return None
272
+
273
+ # ── enqueue edges ───────────────────────────────────────
274
+
275
+ def _extract_enqueue_edges(
276
+ self,
277
+ file_record: FileRecord,
278
+ tree: ast.Module,
279
+ imports: ImportAliases,
280
+ apps: set[str],
281
+ task_registry: dict[str, str],
282
+ analysis: AdapterAnalysis,
283
+ ) -> None:
284
+ source_ranges = self._function_line_index(file_record, tree)
285
+
286
+ for node in ast.walk(tree):
287
+ if not isinstance(node, ast.Call):
288
+ continue
289
+
290
+ target_id: str | None = None
291
+ unresolved_external_task = False
292
+
293
+ # Pattern 1: task_func.delay(...) / task_func.apply_async(...)
294
+ if isinstance(node.func, ast.Attribute):
295
+ method = node.func.attr
296
+ if method in self._ENQUEUE_METHODS:
297
+ task_ref = call_name(node.func.value)
298
+ if task_ref:
299
+ target_id = task_registry.get(task_ref)
300
+ if target_id is None:
301
+ # Try resolving through imports
302
+ resolved = resolve_alias(task_ref, imports)
303
+ if resolved:
304
+ target_id = task_registry.get(resolved)
305
+
306
+ # Pattern 2: app.send_task("task.name", ...)
307
+ if target_id is None:
308
+ func = call_name(node.func)
309
+ else:
310
+ func = None
311
+ if func and node.args:
312
+ short = func.rsplit(".", 1)[-1]
313
+ if short in self._SEND_FUNCTIONS:
314
+ task_name = literal_str(node.args[0])
315
+ if task_name:
316
+ target_id = task_registry.get(task_name)
317
+ if target_id is None:
318
+ # Preserve the literal external reference without
319
+ # claiming that ArcGraph resolved its implementation.
320
+ target_id = worker_id(f"celery:{task_name}")
321
+ unresolved_external_task = True
322
+ if not any(
323
+ candidate.id == target_id
324
+ for candidate in analysis.nodes
325
+ ):
326
+ analysis.nodes.append(
327
+ Node(
328
+ id=target_id,
329
+ kind="worker_task",
330
+ name=task_name,
331
+ qualname=task_name,
332
+ properties={
333
+ "adapter": self.name,
334
+ "external_reference": True,
335
+ "resolution_status": "unresolved",
336
+ "task_name": task_name,
337
+ },
338
+ )
339
+ )
340
+
341
+ if target_id is None:
342
+ continue
343
+
344
+ source = self._source_for_line(source_ranges, node.lineno)
345
+ if not source:
346
+ source = module_node_id(file_record)
347
+
348
+ analysis.edges.append(
349
+ self._edge(
350
+ source,
351
+ target_id,
352
+ "enqueues",
353
+ file_record,
354
+ node,
355
+ f"enqueue {call_name(node.func) or 'task'}",
356
+ confidence=(
357
+ "unresolved" if unresolved_external_task else "confirmed"
358
+ ),
359
+ resolution=(
360
+ FactResolution(
361
+ status="unresolved",
362
+ strategy="celery_send_task_literal",
363
+ candidate_count=0,
364
+ detail="literal task name is outside the indexed registry",
365
+ )
366
+ if unresolved_external_task
367
+ else None
368
+ ),
369
+ )
370
+ )
371
+
372
+ # ── helpers ──────────────────────────────────────────────
373
+
374
+ @staticmethod
375
+ def _bool_keyword(call: ast.Call, name: str) -> bool:
376
+ for kw in call.keywords:
377
+ if kw.arg == name and isinstance(kw.value, ast.Constant):
378
+ return bool(kw.value.value)
379
+ return False
380
+
381
+ @staticmethod
382
+ def _function_line_index(
383
+ file_record: FileRecord,
384
+ tree: ast.Module,
385
+ ) -> list[tuple[int, int, str]]:
386
+ ranges: list[tuple[int, int, str]] = []
387
+ for stmt in tree.body:
388
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
389
+ ranges.append(
390
+ (
391
+ stmt.lineno,
392
+ getattr(stmt, "end_lineno", stmt.lineno),
393
+ function_id(f"{file_record.module}.{stmt.name}"),
394
+ )
395
+ )
396
+ return ranges
397
+
398
+ @staticmethod
399
+ def _source_for_line(
400
+ ranges: list[tuple[int, int, str]],
401
+ line: int,
402
+ ) -> str | None:
403
+ for start, end, node_id in ranges:
404
+ if start <= line <= end:
405
+ return node_id
406
+ return None
407
+
408
+ @staticmethod
409
+ def _edge(
410
+ source: str,
411
+ target: str,
412
+ kind: str,
413
+ file_record: FileRecord,
414
+ node: ast.AST,
415
+ detail: str,
416
+ *,
417
+ confidence: str = "confirmed",
418
+ resolution: FactResolution | None = None,
419
+ ) -> Edge:
420
+ return Edge(
421
+ source=source,
422
+ target=target,
423
+ kind=kind,
424
+ confidence=confidence,
425
+ resolution=resolution or FactResolution(),
426
+ evidence=[
427
+ Evidence(
428
+ kind="celery_task",
429
+ path=file_record.path,
430
+ start_line=getattr(node, "lineno", None),
431
+ end_line=getattr(
432
+ node,
433
+ "end_lineno",
434
+ getattr(node, "lineno", None),
435
+ ),
436
+ column=getattr(node, "col_offset", None),
437
+ detail=detail,
438
+ )
439
+ ],
440
+ )