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.
Files changed (56) hide show
  1. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/PKG-INFO +1 -1
  2. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/__main__.py +10 -0
  3. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/__init__.py +33 -4
  4. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/PKG-INFO +1 -1
  5. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/pyproject.toml +1 -1
  6. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_pipeline.py +73 -0
  7. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/LICENSE +0 -0
  8. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/README.md +0 -0
  9. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/__init__.py +0 -0
  10. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/_bundled_skill.md +0 -0
  11. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/analyze.py +0 -0
  12. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/build.py +0 -0
  13. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/cache.py +0 -0
  14. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/cluster.py +0 -0
  15. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/detect.py +0 -0
  16. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/export.py +0 -0
  17. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/_ids.py +0 -0
  18. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/agentforce.py +0 -0
  19. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/apex.py +0 -0
  20. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/aura.py +0 -0
  21. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/automation.py +0 -0
  22. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/config.py +0 -0
  23. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/doc.py +0 -0
  24. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/flow.py +0 -0
  25. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/layout.py +0 -0
  26. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/lwc.py +0 -0
  27. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/object.py +0 -0
  28. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/profile.py +0 -0
  29. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/extract/visualforce.py +0 -0
  30. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/llm.py +0 -0
  31. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/report.py +0 -0
  32. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/security.py +0 -0
  33. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/serve.py +0 -0
  34. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/validate.py +0 -0
  35. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf/watch.py +0 -0
  36. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/SOURCES.txt +0 -0
  37. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/dependency_links.txt +0 -0
  38. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/entry_points.txt +0 -0
  39. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/requires.txt +0 -0
  40. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/graphify_sf.egg-info/top_level.txt +0 -0
  41. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/setup.cfg +0 -0
  42. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_analyze.py +0 -0
  43. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_build.py +0 -0
  44. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_cli.py +0 -0
  45. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_cluster.py +0 -0
  46. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect.py +0 -0
  47. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect_docs.py +0 -0
  48. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_detect_skip_dirs.py +0 -0
  49. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_agentforce.py +0 -0
  50. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_apex.py +0 -0
  51. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_config.py +0 -0
  52. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_doc.py +0 -0
  53. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_flow.py +0 -0
  54. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_lwc.py +0 -0
  55. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_extract_object.py +0 -0
  56. {graphify_sf-0.3.6 → graphify_sf-0.3.7}/tests/test_llm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphify-sf
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required.
5
5
  Author: Ray Kuo
6
6
  License-Expression: MIT
@@ -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 Exception:
261
- # Fall back to sequential if parallel fails (e.g. on Windows)
262
- # Clear any partial results from completed futures to prevent duplicates
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphify-sf
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required.
5
5
  Author: Ray Kuo
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphify-sf"
7
- version = "0.3.6"
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