flowforge-map 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 (113) hide show
  1. flowforge/__init__.py +0 -0
  2. flowforge/_classic.html +289 -0
  3. flowforge/_dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  4. flowforge/_dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  5. flowforge/_dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  6. flowforge/_dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  7. flowforge/_dashboard/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  8. flowforge/_dashboard/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  9. flowforge/_dashboard/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  10. flowforge/_dashboard/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  11. flowforge/_dashboard/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  12. flowforge/_dashboard/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  13. flowforge/_dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  14. flowforge/_dashboard/assets/index-BGT5seIN.css +1 -0
  15. flowforge/_dashboard/assets/index-BoGikST6.js +52 -0
  16. flowforge/_dashboard/favicon.svg +1 -0
  17. flowforge/_dashboard/index.html +16 -0
  18. flowforge/_example/trip-planner/.env.example +3 -0
  19. flowforge/_example/trip-planner/.gitignore +2 -0
  20. flowforge/_example/trip-planner/.mcp.json +20 -0
  21. flowforge/_example/trip-planner/README.md +48 -0
  22. flowforge/_example/trip-planner/config.py +27 -0
  23. flowforge/_example/trip-planner/mcp_servers/currency.py +10 -0
  24. flowforge/_example/trip-planner/planner.py +113 -0
  25. flowforge/_example/trip-planner/requirements.txt +3 -0
  26. flowforge/_example/trip-planner/services/__init__.py +0 -0
  27. flowforge/_example/trip-planner/services/geo.py +49 -0
  28. flowforge/_example/trip-planner/services/llm.py +18 -0
  29. flowforge/_example/trip-planner/services/mcp_tools.py +26 -0
  30. flowforge/_example/trip-planner/services/weather.py +25 -0
  31. flowforge/_example/trip-planner/tools/packing_list.py +24 -0
  32. flowforge/_example/trip-planner/trips/.gitkeep +0 -0
  33. flowforge/_workflows/diamond_mock.json +71 -0
  34. flowforge/_workflows/stripe_to_razorpay.json +38 -0
  35. flowforge/agent/__init__.py +5 -0
  36. flowforge/agent/server.py +333 -0
  37. flowforge/api/__init__.py +0 -0
  38. flowforge/api/agent.py +79 -0
  39. flowforge/api/checks.py +255 -0
  40. flowforge/api/connectors.py +289 -0
  41. flowforge/api/flowmap.py +174 -0
  42. flowforge/api/runs.py +284 -0
  43. flowforge/api/scan.py +117 -0
  44. flowforge/api/security.py +31 -0
  45. flowforge/api/stats.py +80 -0
  46. flowforge/checks/__init__.py +13 -0
  47. flowforge/checks/models.py +41 -0
  48. flowforge/checks/probes.py +359 -0
  49. flowforge/cli.py +108 -0
  50. flowforge/connectors/__init__.py +0 -0
  51. flowforge/connectors/catalog.py +84 -0
  52. flowforge/connectors/models.py +149 -0
  53. flowforge/connectors/presets.py +192 -0
  54. flowforge/connectors/registry.py +85 -0
  55. flowforge/connectors/runtime.py +47 -0
  56. flowforge/connectors/secrets.py +53 -0
  57. flowforge/example_data.py +247 -0
  58. flowforge/flow/__init__.py +328 -0
  59. flowforge/flow/advice.py +132 -0
  60. flowforge/flow/curly.py +884 -0
  61. flowforge/flow/index.py +116 -0
  62. flowforge/flow/order.py +181 -0
  63. flowforge/flow/reach.py +145 -0
  64. flowforge/flow/rewrite.py +146 -0
  65. flowforge/flowmap/__init__.py +1 -0
  66. flowforge/flowmap/diff.py +145 -0
  67. flowforge/flowmap/fixture.py +283 -0
  68. flowforge/flowmap/models.py +258 -0
  69. flowforge/flowmap/planner.py +131 -0
  70. flowforge/flowmap/service.py +379 -0
  71. flowforge/flowmap/validate.py +228 -0
  72. flowforge/home.py +58 -0
  73. flowforge/main.py +137 -0
  74. flowforge/nodes/__init__.py +3 -0
  75. flowforge/nodes/base.py +74 -0
  76. flowforge/nodes/check_node.py +50 -0
  77. flowforge/nodes/http_node.py +161 -0
  78. flowforge/nodes/llm_node.py +158 -0
  79. flowforge/nodes/local_node.py +164 -0
  80. flowforge/nodes/mcp_node.py +243 -0
  81. flowforge/nodes/mock_node.py +40 -0
  82. flowforge/scan/__init__.py +6 -0
  83. flowforge/scan/facts.py +47 -0
  84. flowforge/scan/files.py +112 -0
  85. flowforge/scan/manifests.py +65 -0
  86. flowforge/scan/mcp_config.py +75 -0
  87. flowforge/scan/models.py +73 -0
  88. flowforge/scan/patterns.py +86 -0
  89. flowforge/scan/python_ast.py +246 -0
  90. flowforge/scan/scanner.py +344 -0
  91. flowforge/scan/services.py +58 -0
  92. flowforge/scan/urls.py +60 -0
  93. flowforge/scheduler/__init__.py +0 -0
  94. flowforge/scheduler/cache.py +76 -0
  95. flowforge/scheduler/critical_path.py +65 -0
  96. flowforge/scheduler/durations.py +35 -0
  97. flowforge/scheduler/executor.py +390 -0
  98. flowforge/scheduler/graph.py +108 -0
  99. flowforge/scheduler/rate_limit.py +36 -0
  100. flowforge/schema.py +81 -0
  101. flowforge/security/__init__.py +0 -0
  102. flowforge/security/finders.py +127 -0
  103. flowforge/security/findings.py +169 -0
  104. flowforge/security/logfilter.py +129 -0
  105. flowforge/spa.py +52 -0
  106. flowforge/state.py +73 -0
  107. flowforge/storage.py +481 -0
  108. flowforge/templating.py +60 -0
  109. flowforge_map-0.1.0.dist-info/METADATA +335 -0
  110. flowforge_map-0.1.0.dist-info/RECORD +113 -0
  111. flowforge_map-0.1.0.dist-info/WHEEL +4 -0
  112. flowforge_map-0.1.0.dist-info/entry_points.txt +3 -0
  113. flowforge_map-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,132 @@
1
+ """What could run together (D25), from the drawn flow and its weights.
2
+
3
+ Each step starts as early as its lines allow (its earliest start). Steps of one entry point that would start at the
4
+ same moment, but that the code awaits one after another, are one piece of advice: running them together saves the
5
+ time of all but the slowest. Steps the code already gathers, and steps in a loop, get none.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ from collections import defaultdict
12
+
13
+ from flowforge.flow.rewrite import Call, change, prompt
14
+ from flowforge.flowmap.models import Advice, Flow
15
+ from flowforge.scheduler.graph import DAG, topological_order
16
+
17
+ MIN_SAVING_MS = 50 # below this, advice is noise
18
+
19
+
20
+ def _starts(flow: Flow) -> dict[str, float]:
21
+ parents: dict[str, list[str]] = {n.id: [] for n in flow.nodes}
22
+ children: dict[str, list[str]] = {n.id: [] for n in flow.nodes}
23
+ for e in flow.edges:
24
+ parents[e.target].append(e.source)
25
+ children[e.source].append(e.target)
26
+ weight = {n.id: n.estimate_ms or 0.0 for n in flow.nodes}
27
+ start: dict[str, float] = {}
28
+ for node in topological_order(DAG(nodes=[n.id for n in flow.nodes], parents=parents, children=children)):
29
+ start[node] = max((start[p] + weight[p] for p in parents[node]), default=0.0)
30
+ return start
31
+
32
+
33
+ def advise(flow: Flow, meta: dict[str, dict], sources: dict[str, str] | None = None,
34
+ modules: dict[str, ast.Module] | None = None) -> list[Advice]:
35
+ start = _starts(flow)
36
+ by_id = {n.id: n for n in flow.nodes}
37
+ groups: dict[tuple[str, float], list[str]] = defaultdict(list)
38
+ for node_id, m in meta.items():
39
+ if not m["gathered"] and not m["loop"]:
40
+ groups[(m["entry"], round(start[node_id], 3))].append(node_id)
41
+ out: list[Advice] = []
42
+ for (_, _), ids in groups.items():
43
+ stmts = {meta[i]["stmt"] for i in ids}
44
+ if len(ids) < 2 or len(stmts) < 2:
45
+ continue
46
+ weights = [by_id[i].estimate_ms or 0.0 for i in ids]
47
+ saved = sum(weights) - max(weights)
48
+ if saved < MIN_SAVING_MS:
49
+ continue
50
+ ids.sort(key=lambda i: meta[i]["line"])
51
+ if all(meta[i].get("awaited", meta[i]["async"]) for i in ids):
52
+ how, hint = "start them together with asyncio.gather", "for example with asyncio.gather"
53
+ elif all(meta[i]["async"] for i in ids): # a plain call inside async code blocks the loop: give it a thread
54
+ how = hint = "start them together with asyncio.gather, running the plain calls in asyncio.to_thread"
55
+ else:
56
+ how, hint = ("they could run at the same time, for example in a thread pool",
57
+ "for example in a thread pool (concurrent.futures)")
58
+ names = [meta[i]["name"] for i in ids]
59
+ text = f"None of these uses another's result: {how}."
60
+ caveat = _caveat(flow, ids, meta, names)
61
+ advice = Advice(nodes=ids, refs=[meta[i]["ref"] for i in ids], text=text, saved_ms=round(saved, 1),
62
+ caveat=caveat)
63
+ lang = meta[ids[0]].get("lang")
64
+ if sources is not None and lang is not None and all(meta[i].get("lang") is lang for i in ids):
65
+ calls = [Call(stmt=meta[i]["node"], func=meta[i]["func"], path=meta[i]["func"].path,
66
+ words=by_id[i].what or by_id[i].title, ref=meta[i]["ref"], weight=by_id[i].estimate_ms or 0.0,
67
+ line=meta[i]["line"]) for i in ids]
68
+ if lang.name == "python":
69
+ func = meta[ids[0]]["func"]
70
+ py = [Call(c.stmt, c.func.node, c.path, c.words, c.ref, c.weight, c.line) for c in calls]
71
+ if modules is not None and all(isinstance(c.stmt, ast.stmt) for c in py):
72
+ advice.change = change(py, sources[func.path], modules[func.path])
73
+ else:
74
+ gather = lang.gather(meta[ids[0]]["func"])
75
+ hint = f"for example with {gather}"
76
+ advice.text = f"None of these uses another's result: start them together with {gather}."
77
+ advice.change = lang.change(calls)
78
+ advice.prompt = prompt(calls, by_id[meta[ids[0]]["entry"]].title, advice.saved_ms, caveat, hint)
79
+ out.append(advice)
80
+ return sorted(out, key=lambda a: -a.saved_ms)
81
+
82
+
83
+ def _caveat(flow: Flow, ids: list[str], meta: dict[str, dict], names: list[str]) -> str | None:
84
+ """Steps that need no other call's result, only an early return's go-ahead: they could start even before that
85
+ check, but when the code stops there, those calls are wasted."""
86
+ into: dict[str, list[str]] = defaultdict(list)
87
+ for e in flow.edges:
88
+ into[e.target].append(e.source)
89
+ by_id = {n.id: n for n in flow.nodes}
90
+ early, guard = [], None
91
+ for i in ids:
92
+ sources = into[i]
93
+ if sources and not meta[i]["ifs"] and not meta[i].get("needs_data") \
94
+ and all(by_id[s].kind == "decision" for s in sources):
95
+ stops = [s for s in sources if any(e.source == s and by_id[e.target].kind == "end" for e in flow.edges)]
96
+ if stops and len(stops) == len(sources):
97
+ early.append(meta[i]["name"])
98
+ guard = by_id[stops[0]].evidence[0].ref
99
+ if not early or guard is None:
100
+ return None
101
+ who = early[0] if len(early) == 1 else f"{', '.join(early[:-1])} and {early[-1]}"
102
+ return (f"{who} only need what was known before the check at {guard}, so they could start even earlier, "
103
+ "but when the code stops there, those calls are wasted.")
104
+
105
+
106
+ def written_ms(flow: Flow, meta: dict[str, dict]) -> float:
107
+ """The slowest entry point as the code is written. Entry points are separate actions, so they are never added
108
+ together (D32)."""
109
+ weight = {n.id: n.estimate_ms or 0.0 for n in flow.nodes}
110
+ return round(max((written_of(p, weight) for p in parts_by_entry(meta).values()), default=0.0), 1)
111
+
112
+
113
+ def parts_by_entry(meta: dict[str, dict]) -> dict[str, list[list[str]]]:
114
+ """Each entry point's calls as written: one group per call, or one per statement that already gathers its calls."""
115
+ out: dict[str, list[list[str]]] = defaultdict(list)
116
+ gathered: dict[tuple[str, int], list[str]] = {}
117
+ for node_id, m in meta.items():
118
+ if m["gathered"]:
119
+ key = (m["entry"], m["stmt"])
120
+ if key not in gathered:
121
+ gathered[key] = []
122
+ out[m["entry"]].append(gathered[key])
123
+ gathered[key].append(node_id)
124
+ else:
125
+ out[m["entry"]].append([node_id])
126
+ return dict(out)
127
+
128
+
129
+ def written_of(parts: list[list[str]], weight: dict[str, float]) -> float:
130
+ """One call after another; a group the code runs together counts once, at its slowest. Calls not in `weight`
131
+ (hidden from the map) count nothing."""
132
+ return sum(max((weight[i] for i in p if i in weight), default=0.0) for p in parts)