graphify-sf 0.3.6__tar.gz → 0.3.7__tar.gz
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.
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/PKG-INFO +1 -1
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/__main__.py +10 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/__init__.py +33 -4
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/PKG-INFO +1 -1
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/pyproject.toml +1 -1
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_pipeline.py +73 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/LICENSE +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/README.md +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/__init__.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/_bundled_skill.md +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/analyze.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/build.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/cache.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/cluster.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/detect.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/export.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/_ids.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/agentforce.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/apex.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/aura.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/automation.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/config.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/doc.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/flow.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/layout.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/lwc.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/object.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/profile.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/visualforce.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/llm.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/report.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/security.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/serve.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/validate.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/watch.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/SOURCES.txt +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/dependency_links.txt +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/entry_points.txt +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/requires.txt +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/top_level.txt +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/setup.cfg +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_analyze.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_build.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_cli.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_cluster.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect_docs.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect_skip_dirs.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_agentforce.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_apex.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_config.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_doc.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_flow.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_lwc.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_object.py +0 -0
- {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_llm.py +0 -0
|
@@ -2328,4 +2328,14 @@ def main() -> None:
|
|
|
2328
2328
|
|
|
2329
2329
|
|
|
2330
2330
|
if __name__ == "__main__":
|
|
2331
|
+
# PyInstaller + multiprocessing: on macOS/Windows the default start method is
|
|
2332
|
+
# "spawn", which re-executes this frozen binary with internal bootstrap args
|
|
2333
|
+
# (e.g. `-B --multiprocessing-fork <fd>`). freeze_support() MUST run before our
|
|
2334
|
+
# CLI argument parser sees argv — otherwise the parser rejects those args with
|
|
2335
|
+
# "unknown command '-B'" and every pool worker dies, silently truncating the
|
|
2336
|
+
# graph. freeze_support() detects the bootstrap argv, runs the worker, and
|
|
2337
|
+
# exits; in a normal invocation it is a no-op.
|
|
2338
|
+
import multiprocessing
|
|
2339
|
+
|
|
2340
|
+
multiprocessing.freeze_support()
|
|
2331
2341
|
main()
|
|
@@ -7,8 +7,10 @@ Two-pass pipeline:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import os
|
|
10
11
|
import sys
|
|
11
12
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
13
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
|
|
14
16
|
from .agentforce import (
|
|
@@ -245,21 +247,48 @@ def extract(
|
|
|
245
247
|
|
|
246
248
|
# Phase 1: Extract single files
|
|
247
249
|
use_parallel = parallel and len(single_files) > 8
|
|
250
|
+
parallel_failed = False
|
|
248
251
|
if use_parallel:
|
|
252
|
+
# Allow overriding the multiprocessing start method (mainly for testing the
|
|
253
|
+
# spawn re-exec path on Linux, where the default is fork). On macOS/Windows
|
|
254
|
+
# the OS default is already "spawn"; freeze_support() in __main__ makes that
|
|
255
|
+
# work for the frozen binary. Unset → use the platform default context.
|
|
256
|
+
import multiprocessing as _mp
|
|
257
|
+
|
|
258
|
+
_start = os.environ.get("GRAPHIFY_SF_MP_START")
|
|
259
|
+
_ctx = _mp.get_context(_start) if _start else None
|
|
249
260
|
try:
|
|
250
|
-
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
261
|
+
with ProcessPoolExecutor(max_workers=max_workers, mp_context=_ctx) as executor:
|
|
251
262
|
futures = {executor.submit(_extract_file, p): p for p in single_files}
|
|
252
263
|
for fut in as_completed(futures):
|
|
253
264
|
try:
|
|
254
265
|
result = fut.result()
|
|
255
266
|
all_nodes.extend(result.get("nodes", []))
|
|
256
267
|
all_edges.extend(result.get("edges", []))
|
|
268
|
+
except BrokenProcessPool:
|
|
269
|
+
# The worker pool itself died (e.g. PyInstaller spawn
|
|
270
|
+
# bootstrap incompatibility on macOS). Partial results are
|
|
271
|
+
# unreliable — abort parallel and redo everything sequentially
|
|
272
|
+
# rather than emit a silently-truncated graph.
|
|
273
|
+
parallel_failed = True
|
|
274
|
+
break
|
|
257
275
|
except Exception as exc:
|
|
276
|
+
# A single file failed to parse — log and continue; this does
|
|
277
|
+
# not indicate a pool-level failure.
|
|
258
278
|
p = futures[fut]
|
|
259
279
|
print(f"[graphify-sf] WARNING: {p}: {exc}", file=sys.stderr)
|
|
260
|
-
except
|
|
261
|
-
#
|
|
262
|
-
|
|
280
|
+
except (BrokenProcessPool, OSError):
|
|
281
|
+
# The executor could not be created / a worker crashed at pool level.
|
|
282
|
+
parallel_failed = True
|
|
283
|
+
|
|
284
|
+
if parallel_failed:
|
|
285
|
+
print(
|
|
286
|
+
"[graphify-sf] WARNING: parallel worker pool failed — "
|
|
287
|
+
"falling back to sequential extraction (graph will be complete, just slower)",
|
|
288
|
+
file=sys.stderr,
|
|
289
|
+
)
|
|
290
|
+
# Discard partial parallel results to prevent duplicates, then redo
|
|
291
|
+
# the full single-file extraction sequentially below.
|
|
263
292
|
all_nodes.clear()
|
|
264
293
|
all_edges.clear()
|
|
265
294
|
use_parallel = False
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphify-sf"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.7"
|
|
8
8
|
description = "Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -187,3 +187,76 @@ def test_stub_node_exists_for_standard_object_after_extract(tmp_path):
|
|
|
187
187
|
|
|
188
188
|
lead_node = next(n for n in extraction["nodes"] if n["id"] == "object_lead")
|
|
189
189
|
assert lead_node.get("stub") is True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Parallel worker-pool failure → sequential fallback (0.3.7 macOS spawn bug)
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _make_multi_object_tree(root):
|
|
198
|
+
"""Create >8 single-file metadata entries so parallel extraction is used."""
|
|
199
|
+
objects = root / "force-app" / "main" / "default" / "objects"
|
|
200
|
+
for i in range(12):
|
|
201
|
+
obj = objects / f"Obj{i}__c"
|
|
202
|
+
(obj / "fields").mkdir(parents=True)
|
|
203
|
+
(obj / f"Obj{i}__c.object-meta.xml").write_text(
|
|
204
|
+
'<?xml version="1.0"?><CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">'
|
|
205
|
+
f"<label>Obj{i}</label></CustomObject>"
|
|
206
|
+
)
|
|
207
|
+
(obj / "fields" / f"F{i}__c.field-meta.xml").write_text(
|
|
208
|
+
'<?xml version="1.0"?><CustomField xmlns="http://soap.sforce.com/2006/04/metadata">'
|
|
209
|
+
f"<fullName>F{i}__c</fullName><type>Text</type><label>F{i}</label></CustomField>"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_parallel_pool_failure_falls_back_to_complete_sequential_graph(tmp_path, monkeypatch, capsys):
|
|
214
|
+
"""If the worker pool dies (e.g. PyInstaller spawn bootstrap on macOS), extraction
|
|
215
|
+
must fall back to sequential and still produce the COMPLETE graph — never a
|
|
216
|
+
silently-truncated one. Regression for the 0.3.6 macOS bug."""
|
|
217
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
218
|
+
|
|
219
|
+
from graphify_sf import extract as extract_mod
|
|
220
|
+
from graphify_sf.detect import detect
|
|
221
|
+
from graphify_sf.extract import extract
|
|
222
|
+
|
|
223
|
+
_make_multi_object_tree(tmp_path)
|
|
224
|
+
detection = detect(tmp_path)
|
|
225
|
+
|
|
226
|
+
# Baseline: what a healthy sequential run produces.
|
|
227
|
+
baseline = extract(detection, parallel=False)
|
|
228
|
+
baseline_ids = {n["id"] for n in baseline["nodes"]}
|
|
229
|
+
assert any(i.startswith("object_") for i in baseline_ids)
|
|
230
|
+
assert any(i.startswith("field_") for i in baseline_ids)
|
|
231
|
+
|
|
232
|
+
# Simulate a broken pool: the executor's futures all raise BrokenProcessPool.
|
|
233
|
+
class _BrokenExecutor:
|
|
234
|
+
def __init__(self, *a, **k):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
def __enter__(self):
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def __exit__(self, *a):
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def submit(self, *a, **k):
|
|
244
|
+
class _F:
|
|
245
|
+
def result(self_inner):
|
|
246
|
+
raise BrokenProcessPool("simulated worker crash")
|
|
247
|
+
|
|
248
|
+
return _F()
|
|
249
|
+
|
|
250
|
+
monkeypatch.setattr(extract_mod, "ProcessPoolExecutor", _BrokenExecutor)
|
|
251
|
+
# as_completed must just yield the futures back
|
|
252
|
+
monkeypatch.setattr(extract_mod, "as_completed", lambda futs: list(futs))
|
|
253
|
+
|
|
254
|
+
degraded = extract(detection, parallel=True)
|
|
255
|
+
degraded_ids = {n["id"] for n in degraded["nodes"]}
|
|
256
|
+
|
|
257
|
+
# The fallback graph must equal the healthy sequential graph — no truncation.
|
|
258
|
+
assert degraded_ids == baseline_ids, "pool failure must fall back to a COMPLETE graph"
|
|
259
|
+
# And it must be loud about it.
|
|
260
|
+
err = capsys.readouterr().err
|
|
261
|
+
assert "parallel worker pool failed" in err
|
|
262
|
+
assert "falling back to sequential" in err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|