arcgraph 0.1.0rc7__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 +2211 -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 +213 -0
- arcgraph/change/pins.py +420 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +999 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1113 -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 +695 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +867 -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 +1038 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +28 -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 +122 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1431 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -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 +31 -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 +546 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2794 -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 +84 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +603 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +454 -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 +455 -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/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 +593 -0
- arcgraph/interfaces/workbench.py +572 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +449 -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 +654 -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.0rc7.dist-info/METADATA +659 -0
- arcgraph-0.1.0rc7.dist-info/RECORD +163 -0
- arcgraph-0.1.0rc7.dist-info/WHEEL +4 -0
- arcgraph-0.1.0rc7.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0rc7.dist-info/licenses/LICENSE +203 -0
arcgraph/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""ArcGraph package."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomllib
|
|
7
|
+
|
|
8
|
+
from arcgraph.core.schemas import READ_SCHEMA_VERSION, SCHEMA_VERSION
|
|
9
|
+
|
|
10
|
+
_DISTRIBUTION_NAME = "arcgraph"
|
|
11
|
+
_UNKNOWN_VERSION = "0.0.0+unknown"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _version_from_pyproject() -> str | None:
|
|
15
|
+
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
16
|
+
if not pyproject_path.exists():
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
20
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
21
|
+
return None
|
|
22
|
+
project = pyproject.get("project")
|
|
23
|
+
if not isinstance(project, dict):
|
|
24
|
+
return None
|
|
25
|
+
version = project.get("version")
|
|
26
|
+
return version if isinstance(version, str) else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _package_version() -> str:
|
|
30
|
+
source_version = _version_from_pyproject()
|
|
31
|
+
if source_version is not None:
|
|
32
|
+
return source_version
|
|
33
|
+
try:
|
|
34
|
+
return importlib.metadata.version(_DISTRIBUTION_NAME)
|
|
35
|
+
except importlib.metadata.PackageNotFoundError:
|
|
36
|
+
return _UNKNOWN_VERSION
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__version__ = _package_version()
|
|
40
|
+
|
|
41
|
+
__all__ = ["READ_SCHEMA_VERSION", "SCHEMA_VERSION", "__version__"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"build_version": "0.1.0rc7",
|
|
3
|
+
"schema_version": "1.0.0",
|
|
4
|
+
"source": {
|
|
5
|
+
"commit_sha": "653f20412237f53658b34cbde813f6060bee64fc",
|
|
6
|
+
"tree_sha": "68609b8222d5e05ab7b9c09dab22ecb19dc284d4",
|
|
7
|
+
"working_tree_clean": true,
|
|
8
|
+
"working_tree_status_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""ARQ worker extraction for ArcGraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
from arcgraph.adapters.common import (
|
|
8
|
+
AdapterAnalysis,
|
|
9
|
+
SemanticAdapter,
|
|
10
|
+
call_name,
|
|
11
|
+
keyword,
|
|
12
|
+
literal_str,
|
|
13
|
+
module_node_id,
|
|
14
|
+
)
|
|
15
|
+
from arcgraph.core.ids import function_id, queue_id, worker_id
|
|
16
|
+
from arcgraph.core.schemas import Edge, Evidence, FactResolution, FileRecord, Node
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ARQAdapter(SemanticAdapter):
|
|
20
|
+
name = "arq"
|
|
21
|
+
capabilities = ("entrypoint:worker_task", "resource:queue", "edge:enqueues")
|
|
22
|
+
required_facts = ("function",)
|
|
23
|
+
|
|
24
|
+
def analyze(
|
|
25
|
+
self,
|
|
26
|
+
files: list[FileRecord],
|
|
27
|
+
parsed_files: dict[str, ast.Module],
|
|
28
|
+
nodes: list[Node],
|
|
29
|
+
module_names: set[str],
|
|
30
|
+
) -> AdapterAnalysis:
|
|
31
|
+
del module_names
|
|
32
|
+
analysis = AdapterAnalysis()
|
|
33
|
+
concrete_node_ids = {node.id for node in nodes}
|
|
34
|
+
emitted_node_ids = set(concrete_node_ids)
|
|
35
|
+
worker_targets: dict[tuple[str, str], str] = {}
|
|
36
|
+
for file_record in files:
|
|
37
|
+
tree = parsed_files.get(file_record.path)
|
|
38
|
+
if tree is None:
|
|
39
|
+
continue
|
|
40
|
+
constants = self._string_constants(tree)
|
|
41
|
+
for stmt in tree.body:
|
|
42
|
+
if isinstance(stmt, ast.ClassDef) and stmt.name == "WorkerSettings":
|
|
43
|
+
self._analyze_worker_settings(
|
|
44
|
+
file_record,
|
|
45
|
+
stmt,
|
|
46
|
+
constants,
|
|
47
|
+
analysis,
|
|
48
|
+
worker_targets,
|
|
49
|
+
concrete_node_ids,
|
|
50
|
+
emitted_node_ids,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for file_record in files:
|
|
54
|
+
tree = parsed_files.get(file_record.path)
|
|
55
|
+
if tree is None:
|
|
56
|
+
continue
|
|
57
|
+
self._enqueue_edges(
|
|
58
|
+
file_record,
|
|
59
|
+
tree,
|
|
60
|
+
self._string_constants(tree),
|
|
61
|
+
worker_targets,
|
|
62
|
+
analysis,
|
|
63
|
+
emitted_node_ids,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
analysis.nodes.sort(key=lambda node: node.id)
|
|
67
|
+
analysis.edges.sort(key=lambda edge: (edge.source, edge.target, edge.kind))
|
|
68
|
+
return analysis
|
|
69
|
+
|
|
70
|
+
def _analyze_worker_settings(
|
|
71
|
+
self,
|
|
72
|
+
file_record: FileRecord,
|
|
73
|
+
stmt: ast.ClassDef,
|
|
74
|
+
constants: dict[str, str],
|
|
75
|
+
analysis: AdapterAnalysis,
|
|
76
|
+
worker_targets: dict[tuple[str, str], str],
|
|
77
|
+
concrete_node_ids: set[str],
|
|
78
|
+
emitted_node_ids: set[str],
|
|
79
|
+
) -> None:
|
|
80
|
+
queue_name = "arq:default"
|
|
81
|
+
functions: list[tuple[str, ast.AST, bool]] = []
|
|
82
|
+
for item in stmt.body:
|
|
83
|
+
if not isinstance(item, ast.Assign):
|
|
84
|
+
continue
|
|
85
|
+
target_names = [
|
|
86
|
+
target.id for target in item.targets if isinstance(target, ast.Name)
|
|
87
|
+
]
|
|
88
|
+
if "queue_name" in target_names:
|
|
89
|
+
queue_name = self._string_value(item.value, constants) or queue_name
|
|
90
|
+
elif "functions" in target_names:
|
|
91
|
+
functions.extend(
|
|
92
|
+
(name, item, False) for name in self._function_names(item.value)
|
|
93
|
+
)
|
|
94
|
+
elif "cron_jobs" in target_names:
|
|
95
|
+
functions.extend(
|
|
96
|
+
(name, item, True) for name in self._cron_function_names(item.value)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
queue_node_id = queue_id(queue_name)
|
|
100
|
+
analysis.nodes.append(
|
|
101
|
+
Node(
|
|
102
|
+
id=queue_node_id,
|
|
103
|
+
kind="queue",
|
|
104
|
+
name=queue_name,
|
|
105
|
+
qualname=queue_name,
|
|
106
|
+
path=file_record.path,
|
|
107
|
+
start_line=stmt.lineno,
|
|
108
|
+
end_line=getattr(stmt, "end_lineno", stmt.lineno),
|
|
109
|
+
properties={"backend": "arq"},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
unique_functions: dict[str, tuple[ast.AST, bool]] = {}
|
|
113
|
+
for function_name, evidence_node, scheduled in functions:
|
|
114
|
+
existing = unique_functions.get(function_name)
|
|
115
|
+
if existing is None or (scheduled and not existing[1]):
|
|
116
|
+
unique_functions[function_name] = (evidence_node, scheduled)
|
|
117
|
+
|
|
118
|
+
for function_name, (evidence_node, scheduled) in unique_functions.items():
|
|
119
|
+
task_id = worker_id(f"{queue_name}:{function_name}")
|
|
120
|
+
handler_id = function_id(f"{file_record.module}.{function_name}")
|
|
121
|
+
worker_targets[(queue_name, function_name)] = task_id
|
|
122
|
+
handler_resolved = handler_id in concrete_node_ids
|
|
123
|
+
if not handler_resolved and handler_id not in emitted_node_ids:
|
|
124
|
+
analysis.nodes.append(
|
|
125
|
+
Node(
|
|
126
|
+
id=handler_id,
|
|
127
|
+
kind="function",
|
|
128
|
+
name=function_name.rsplit(".", 1)[-1],
|
|
129
|
+
qualname=f"{file_record.module}.{function_name}",
|
|
130
|
+
properties={
|
|
131
|
+
"adapter": self.name,
|
|
132
|
+
"external_reference": True,
|
|
133
|
+
"resolution_status": "unresolved",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
emitted_node_ids.add(handler_id)
|
|
138
|
+
analysis.nodes.append(
|
|
139
|
+
Node(
|
|
140
|
+
id=task_id,
|
|
141
|
+
kind="worker_task",
|
|
142
|
+
name=function_name,
|
|
143
|
+
qualname=f"{queue_name}:{function_name}",
|
|
144
|
+
path=file_record.path,
|
|
145
|
+
start_line=getattr(evidence_node, "lineno", stmt.lineno),
|
|
146
|
+
end_line=getattr(evidence_node, "end_lineno", stmt.lineno),
|
|
147
|
+
properties={
|
|
148
|
+
"queue_name": queue_name,
|
|
149
|
+
"handler": handler_id,
|
|
150
|
+
"scheduled": scheduled,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
analysis.edges.append(
|
|
155
|
+
self._edge(
|
|
156
|
+
task_id,
|
|
157
|
+
handler_id,
|
|
158
|
+
"invokes",
|
|
159
|
+
file_record,
|
|
160
|
+
evidence_node,
|
|
161
|
+
f"WorkerSettings.{'cron_jobs' if scheduled else 'functions'} -> {function_name}",
|
|
162
|
+
confidence="confirmed" if handler_resolved else "unresolved",
|
|
163
|
+
resolution=(
|
|
164
|
+
None
|
|
165
|
+
if handler_resolved
|
|
166
|
+
else FactResolution(
|
|
167
|
+
status="unresolved",
|
|
168
|
+
strategy="arq_worker_settings_literal",
|
|
169
|
+
candidate_count=0,
|
|
170
|
+
detail="worker handler is outside the indexed function registry",
|
|
171
|
+
)
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
analysis.edges.append(
|
|
176
|
+
self._edge(
|
|
177
|
+
task_id,
|
|
178
|
+
queue_node_id,
|
|
179
|
+
"consumes",
|
|
180
|
+
file_record,
|
|
181
|
+
evidence_node,
|
|
182
|
+
f"WorkerSettings.queue_name = {queue_name}",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _enqueue_edges(
|
|
187
|
+
self,
|
|
188
|
+
file_record: FileRecord,
|
|
189
|
+
tree: ast.Module,
|
|
190
|
+
constants: dict[str, str],
|
|
191
|
+
worker_targets: dict[tuple[str, str], str],
|
|
192
|
+
analysis: AdapterAnalysis,
|
|
193
|
+
node_ids: set[str],
|
|
194
|
+
) -> None:
|
|
195
|
+
source_stack = self._function_line_index(file_record, tree)
|
|
196
|
+
for node in ast.walk(tree):
|
|
197
|
+
if not isinstance(node, ast.Call):
|
|
198
|
+
continue
|
|
199
|
+
name = call_name(node.func)
|
|
200
|
+
if not (
|
|
201
|
+
(name is not None and name.endswith(".enqueue_job"))
|
|
202
|
+
or (
|
|
203
|
+
isinstance(node.func, ast.Attribute)
|
|
204
|
+
and node.func.attr == "enqueue_job"
|
|
205
|
+
)
|
|
206
|
+
):
|
|
207
|
+
continue
|
|
208
|
+
if not node.args:
|
|
209
|
+
continue
|
|
210
|
+
task_name = self._string_value(node.args[0], constants)
|
|
211
|
+
if not task_name:
|
|
212
|
+
continue
|
|
213
|
+
queue_name = self._string_value(keyword(node, "_queue_name"), constants)
|
|
214
|
+
target = worker_targets.get((queue_name, task_name)) if queue_name else None
|
|
215
|
+
resolved_target = target is not None
|
|
216
|
+
if target is None and queue_name:
|
|
217
|
+
target = worker_id(f"{queue_name}:{task_name}")
|
|
218
|
+
elif target is None:
|
|
219
|
+
matches = {
|
|
220
|
+
target_id
|
|
221
|
+
for (_, worker_name), target_id in worker_targets.items()
|
|
222
|
+
if worker_name == task_name
|
|
223
|
+
}
|
|
224
|
+
target = next(iter(matches)) if len(matches) == 1 else None
|
|
225
|
+
resolved_target = target is not None
|
|
226
|
+
if target is None:
|
|
227
|
+
target = worker_id(f"arq:external:{queue_name or '*'}:{task_name}")
|
|
228
|
+
if not resolved_target and target not in node_ids:
|
|
229
|
+
analysis.nodes.append(
|
|
230
|
+
Node(
|
|
231
|
+
id=target,
|
|
232
|
+
kind="worker_task",
|
|
233
|
+
name=task_name,
|
|
234
|
+
qualname=f"{queue_name or '*'}:{task_name}",
|
|
235
|
+
properties={
|
|
236
|
+
"adapter": self.name,
|
|
237
|
+
"external_reference": True,
|
|
238
|
+
"resolution_status": "unresolved",
|
|
239
|
+
"queue_name": queue_name,
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
node_ids.add(target)
|
|
244
|
+
source = self._source_for_line(source_stack, node.lineno)
|
|
245
|
+
source = source or module_node_id(file_record)
|
|
246
|
+
if source:
|
|
247
|
+
analysis.edges.append(
|
|
248
|
+
self._edge(
|
|
249
|
+
source,
|
|
250
|
+
target,
|
|
251
|
+
"enqueues",
|
|
252
|
+
file_record,
|
|
253
|
+
node,
|
|
254
|
+
f"enqueue_job({task_name})",
|
|
255
|
+
confidence="confirmed" if resolved_target else "unresolved",
|
|
256
|
+
resolution=(
|
|
257
|
+
None
|
|
258
|
+
if resolved_target
|
|
259
|
+
else FactResolution(
|
|
260
|
+
status="unresolved",
|
|
261
|
+
strategy="arq_enqueue_job_literal",
|
|
262
|
+
candidate_count=0,
|
|
263
|
+
detail=(
|
|
264
|
+
"literal task name is outside the indexed "
|
|
265
|
+
"ARQ worker registry"
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _string_constants(tree: ast.Module) -> dict[str, str]:
|
|
274
|
+
constants: dict[str, str] = {}
|
|
275
|
+
for stmt in tree.body:
|
|
276
|
+
if not isinstance(stmt, ast.Assign):
|
|
277
|
+
continue
|
|
278
|
+
value = literal_str(stmt.value)
|
|
279
|
+
if value is None:
|
|
280
|
+
continue
|
|
281
|
+
for target in stmt.targets:
|
|
282
|
+
if isinstance(target, ast.Name):
|
|
283
|
+
constants[target.id] = value
|
|
284
|
+
return constants
|
|
285
|
+
|
|
286
|
+
def _string_value(
|
|
287
|
+
self, node: ast.AST | None, constants: dict[str, str]
|
|
288
|
+
) -> str | None:
|
|
289
|
+
value = literal_str(node)
|
|
290
|
+
if value is not None:
|
|
291
|
+
return value
|
|
292
|
+
if isinstance(node, ast.Name):
|
|
293
|
+
return constants.get(node.id)
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _function_names(node: ast.AST) -> list[str]:
|
|
298
|
+
if not isinstance(node, (ast.List, ast.Tuple)):
|
|
299
|
+
return []
|
|
300
|
+
return [name for item in node.elts if (name := call_name(item))]
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _cron_function_names(node: ast.AST) -> list[str]:
|
|
304
|
+
if not isinstance(node, (ast.List, ast.Tuple)):
|
|
305
|
+
return []
|
|
306
|
+
names: list[str] = []
|
|
307
|
+
for item in node.elts:
|
|
308
|
+
if (
|
|
309
|
+
isinstance(item, ast.Call)
|
|
310
|
+
and call_name(item.func) == "cron"
|
|
311
|
+
and item.args
|
|
312
|
+
):
|
|
313
|
+
name = call_name(item.args[0])
|
|
314
|
+
if name:
|
|
315
|
+
names.append(name)
|
|
316
|
+
return names
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _function_line_index(
|
|
320
|
+
file_record: FileRecord, tree: ast.Module
|
|
321
|
+
) -> list[tuple[int, int, str]]:
|
|
322
|
+
ranges: list[tuple[int, int, str]] = []
|
|
323
|
+
for stmt in tree.body:
|
|
324
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
325
|
+
ranges.append(
|
|
326
|
+
(
|
|
327
|
+
stmt.lineno,
|
|
328
|
+
getattr(stmt, "end_lineno", stmt.lineno),
|
|
329
|
+
function_id(f"{file_record.module}.{stmt.name}"),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
return ranges
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _source_for_line(ranges: list[tuple[int, int, str]], line: int) -> str | None:
|
|
336
|
+
for start, end, node_id in ranges:
|
|
337
|
+
if start <= line <= end:
|
|
338
|
+
return node_id
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _edge(
|
|
343
|
+
source: str,
|
|
344
|
+
target: str,
|
|
345
|
+
kind: str,
|
|
346
|
+
file_record: FileRecord,
|
|
347
|
+
node: ast.AST,
|
|
348
|
+
detail: str,
|
|
349
|
+
*,
|
|
350
|
+
confidence: str = "confirmed",
|
|
351
|
+
resolution: FactResolution | None = None,
|
|
352
|
+
) -> Edge:
|
|
353
|
+
return Edge(
|
|
354
|
+
source=source,
|
|
355
|
+
target=target,
|
|
356
|
+
kind=kind,
|
|
357
|
+
confidence=confidence, # type: ignore[arg-type]
|
|
358
|
+
resolution=resolution or FactResolution(),
|
|
359
|
+
evidence=[
|
|
360
|
+
Evidence(
|
|
361
|
+
kind="arq_worker",
|
|
362
|
+
path=file_record.path,
|
|
363
|
+
start_line=getattr(node, "lineno", None),
|
|
364
|
+
end_line=getattr(node, "end_lineno", getattr(node, "lineno", None)),
|
|
365
|
+
column=getattr(node, "col_offset", None),
|
|
366
|
+
detail=detail,
|
|
367
|
+
)
|
|
368
|
+
],
|
|
369
|
+
)
|