@uipath/maestro-builder-sdk 6.0.0 → 6.0.1
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.
|
@@ -30,6 +30,8 @@ __all__ = [
|
|
|
30
30
|
"ConnectionError_",
|
|
31
31
|
"byoa_connections",
|
|
32
32
|
"byoa_listing",
|
|
33
|
+
"candidate_connections",
|
|
34
|
+
"candidate_listing",
|
|
33
35
|
"discover_connection",
|
|
34
36
|
"lookup_connection",
|
|
35
37
|
"merge_bindings",
|
|
@@ -130,6 +132,48 @@ def discover_connection(connector_key: str, name: str | None = None) -> dict:
|
|
|
130
132
|
return rows[0]
|
|
131
133
|
|
|
132
134
|
|
|
135
|
+
def candidate_connections(connector_key: str, name: str | None = None) -> list[dict]:
|
|
136
|
+
"""Every enabled connection for `connector_key`, own folder FIRST.
|
|
137
|
+
|
|
138
|
+
`discover_connection` answers "which one" and stops at the first non-empty
|
|
139
|
+
scope, which is right for picking a connection and wrong for retrying one:
|
|
140
|
+
when the own-folder listing holds exactly one, the tenant-wide connections
|
|
141
|
+
are never even looked at. So a lookup that exhausts the personal-workspace
|
|
142
|
+
connection reports a bad VALUE while the record sits in another folder's
|
|
143
|
+
connection (flow-builder-sdk#744, measured on two Jira connections where
|
|
144
|
+
only the second carried project `TS`).
|
|
145
|
+
|
|
146
|
+
Ordering is the same preference `discover_connection` encodes — the caller's
|
|
147
|
+
own folder is the likeliest and is tried first — but nothing is dropped.
|
|
148
|
+
Deduplicated by `Id`, because the tenant-wide listing repeats the own-folder
|
|
149
|
+
rows.
|
|
150
|
+
|
|
151
|
+
This does NOT decide anything, and in particular does not raise on several
|
|
152
|
+
matches: choosing among them is the caller's job, and the only honest way to
|
|
153
|
+
choose is evidence (a lookup that resolves on exactly one). That keeps this
|
|
154
|
+
module's rule intact — ambiguity is never resolved by guessing.
|
|
155
|
+
"""
|
|
156
|
+
ordered: list[dict] = []
|
|
157
|
+
seen: set[str] = set()
|
|
158
|
+
for all_folders in (False, True):
|
|
159
|
+
for row in _matching(_list(all_folders=all_folders), connector_key, name):
|
|
160
|
+
identifier = str(row.get("Id", ""))
|
|
161
|
+
if identifier and identifier in seen:
|
|
162
|
+
continue
|
|
163
|
+
seen.add(identifier)
|
|
164
|
+
ordered.append(row)
|
|
165
|
+
return ordered
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def candidate_listing(rows: list[dict], indent: str = " ") -> str:
|
|
169
|
+
"""The candidates, one pasteable `--connection-id` line each."""
|
|
170
|
+
return "\n".join(
|
|
171
|
+
f"{indent}{row.get('Name')} --connection-id {row.get('Id')}"
|
|
172
|
+
f" (folder: {row.get('Folder')})"
|
|
173
|
+
for row in rows
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
133
177
|
def _enabled_listing(rows: list[dict], connector_key: str) -> str:
|
|
134
178
|
"""The enabled connections for `connector_key`, one per line, pasteable."""
|
|
135
179
|
enabled = _matching(rows, connector_key, None)
|
|
@@ -25,6 +25,7 @@ import subprocess
|
|
|
25
25
|
import sys
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
|
+
"LookupExhausted",
|
|
28
29
|
"ResolutionError",
|
|
29
30
|
"parse_resolve_flag",
|
|
30
31
|
"resolve_one",
|
|
@@ -55,6 +56,36 @@ class ResolutionError(Exception):
|
|
|
55
56
|
"""A lookup that could not be resolved, with the reason a human needs."""
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
class LookupExhausted(ResolutionError):
|
|
60
|
+
"""The collection was read to the end on THIS connection and had no match.
|
|
61
|
+
|
|
62
|
+
Split out from its parent because the two failures want opposite responses
|
|
63
|
+
and the caller cannot tell them apart from a message. A transport or
|
|
64
|
+
permission failure says nothing about whether the value exists — retrying it
|
|
65
|
+
on another connection would hide a broken call behind a confident "no
|
|
66
|
+
connection has it". An exhaustive miss is positive evidence about this
|
|
67
|
+
connection specifically, and is the signal `prepare` uses to demote it and
|
|
68
|
+
try the next candidate (flow-builder-sdk#744).
|
|
69
|
+
|
|
70
|
+
`capped` flips that: a scan stopped at the record cap did NOT reach the end,
|
|
71
|
+
so it is not evidence of absence and must not demote anything.
|
|
72
|
+
|
|
73
|
+
The attributes carry the counts so a caller can report per-candidate results
|
|
74
|
+
without re-parsing the sentence.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, message: str, *, field: str, collection: str, by: str,
|
|
78
|
+
value: str, scanned: int, pages: int, capped: bool) -> None:
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
self.field = field
|
|
81
|
+
self.collection = collection
|
|
82
|
+
self.by = by
|
|
83
|
+
self.value = value
|
|
84
|
+
self.scanned = scanned
|
|
85
|
+
self.pages = pages
|
|
86
|
+
self.capped = capped
|
|
87
|
+
|
|
88
|
+
|
|
58
89
|
def parse_resolve_flag(raw: str) -> tuple[str, str, str]:
|
|
59
90
|
"""`channel:profile.email=dustin@example.com` -> (field, by, value).
|
|
60
91
|
|
|
@@ -261,13 +292,28 @@ def resolve_one(
|
|
|
261
292
|
query = f"{query}&nextPage={token}" if query else f"nextPage={token}"
|
|
262
293
|
|
|
263
294
|
capped = scanned >= MAX_SCANNED or pages > PAGE_GUARD
|
|
264
|
-
|
|
295
|
+
# This states the FACT and stops; `prepare` owns the remedy, because
|
|
296
|
+
# `prepare` is what knows the other candidate connections.
|
|
297
|
+
#
|
|
298
|
+
# It deliberately carries NO "check the value" and NO `uip is resources`
|
|
299
|
+
# pointer. The old text led with both: it named only the value as suspect
|
|
300
|
+
# when the connection is equally suspect, and then sent the reader to a
|
|
301
|
+
# tenant crawl the skill explicitly rules out ("Tenant discovery is not a
|
|
302
|
+
# phase of either loop", references/CLI-LOOP.md). An agent followed that
|
|
303
|
+
# advice into exactly what it had been told not to do — 50 of 94 Bash calls
|
|
304
|
+
# hand-crawling Integration Service, 41 of them paging this collection, and
|
|
305
|
+
# the run hit its ceiling without producing a flow (flow-builder-sdk#744).
|
|
306
|
+
#
|
|
307
|
+
# The listing command is still offered, once, by the caller's terminal
|
|
308
|
+
# report — where every connection has been tried and seeing what IS there
|
|
309
|
+
# is finally the next step rather than a detour.
|
|
310
|
+
raise LookupExhausted(
|
|
265
311
|
f"{field}: no record in {collection!r} has {by}={value!r} "
|
|
266
312
|
f"(scanned {scanned} record(s) across {pages + 1} page(s)"
|
|
267
313
|
+ (f"; STOPPED AT THE {MAX_SCANNED}-record cap, so the value may be "
|
|
268
|
-
f"further in" if capped
|
|
269
|
-
|
|
270
|
-
f"
|
|
271
|
-
|
|
272
|
-
|
|
314
|
+
f"further in" if capped
|
|
315
|
+
else "; collection exhausted")
|
|
316
|
+
+ f") on connection {connection_id}.",
|
|
317
|
+
field=field, collection=collection, by=by, value=value,
|
|
318
|
+
scanned=scanned, pages=pages + 1, capped=capped,
|
|
273
319
|
)
|
|
@@ -382,6 +382,12 @@ def main() -> None:
|
|
|
382
382
|
# step that removes 4-7 tool calls per task: listing connections, filtering
|
|
383
383
|
# them, and then hunting Orchestrator for a folder key that was on the
|
|
384
384
|
# connection record all along.
|
|
385
|
+
# Whether the CALLER chose the connection, recorded before discovery
|
|
386
|
+
# overwrites `connection_id` with its own answer. An explicit choice is never
|
|
387
|
+
# second-guessed: the retry in `_write_resolutions` is for a connection this
|
|
388
|
+
# command picked, not for one the author named (flow-builder-sdk#744).
|
|
389
|
+
args._connection_pinned = bool(args.connection_id or args.connection)
|
|
390
|
+
|
|
385
391
|
if not args.connection_id:
|
|
386
392
|
try:
|
|
387
393
|
found = _connections.discover_connection(args.key, args.connection)
|
|
@@ -1125,6 +1131,14 @@ def _write_resolutions(args, node_type: str) -> None:
|
|
|
1125
1131
|
args.key, specs[field], field, by, value,
|
|
1126
1132
|
args.connection_id, resolved_values,
|
|
1127
1133
|
)
|
|
1134
|
+
except _resolve.LookupExhausted as error:
|
|
1135
|
+
# The collection was read to the END on this connection and the
|
|
1136
|
+
# value is not in it. That is evidence about the CONNECTION, not
|
|
1137
|
+
# only about the value — so before failing, try the other enabled
|
|
1138
|
+
# connections for this connector. A transport failure is the
|
|
1139
|
+
# sibling exception below and is never retried: retrying it would
|
|
1140
|
+
# dress a broken call up as "no connection has it".
|
|
1141
|
+
_retry_or_exit(args, node_type, specs, ordered, error)
|
|
1128
1142
|
except _resolve.ResolutionError as error:
|
|
1129
1143
|
sys.exit(f"prepare-connector: {error}")
|
|
1130
1144
|
resolved_values[field] = got
|
|
@@ -1143,6 +1157,164 @@ def _write_resolutions(args, node_type: str) -> None:
|
|
|
1143
1157
|
print(f" wrote {len(existing)} resolution(s) to {path}")
|
|
1144
1158
|
|
|
1145
1159
|
|
|
1160
|
+
def _candidate_label(row: dict) -> str:
|
|
1161
|
+
"""`name (folder)` for a per-candidate report line."""
|
|
1162
|
+
return f"{row.get('Name')} ({row.get('Folder')})"
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _probe_candidate(args, specs, ordered, connection_id: str):
|
|
1166
|
+
"""Resolve every requested lookup against one connection.
|
|
1167
|
+
|
|
1168
|
+
Returns `None` when all of them resolve, or the first reason they did not:
|
|
1169
|
+
`("exhausted", <LookupExhausted>)` or `("error", <ResolutionError>)`. The
|
|
1170
|
+
two are kept apart all the way up, because only the first is evidence about
|
|
1171
|
+
the connection.
|
|
1172
|
+
"""
|
|
1173
|
+
resolved: dict[str, str] = {}
|
|
1174
|
+
for field, by, value in ordered:
|
|
1175
|
+
try:
|
|
1176
|
+
resolved[field] = _resolve.resolve_one(
|
|
1177
|
+
args.key, specs[field], field, by, value, connection_id, resolved,
|
|
1178
|
+
)
|
|
1179
|
+
except _resolve.LookupExhausted as error:
|
|
1180
|
+
return ("exhausted", error)
|
|
1181
|
+
except _resolve.ResolutionError as error:
|
|
1182
|
+
return ("error", error)
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _retry_or_exit(args, node_type: str, specs, ordered, first) -> None:
|
|
1187
|
+
"""Try the other candidate connections, or exit explaining what was tried.
|
|
1188
|
+
|
|
1189
|
+
NEVER RETURNS. Either it re-execs this command pinned to a connection that
|
|
1190
|
+
resolves everything, or it exits with a report.
|
|
1191
|
+
"""
|
|
1192
|
+
if args._connection_pinned:
|
|
1193
|
+
# An explicit --connection-id / --connection is the author's decision and
|
|
1194
|
+
# is not overridden. The alternatives are still named, because the whole
|
|
1195
|
+
# cost of this bug was not knowing they existed.
|
|
1196
|
+
others = [
|
|
1197
|
+
row for row in _connections.candidate_connections(args.key)
|
|
1198
|
+
if str(row.get("Id")) != str(args.connection_id)
|
|
1199
|
+
]
|
|
1200
|
+
extra = ""
|
|
1201
|
+
if others:
|
|
1202
|
+
extra = (
|
|
1203
|
+
f"\n{len(others)} other enabled {args.key} connection"
|
|
1204
|
+
f"{'s' if len(others) > 1 else ''} exist"
|
|
1205
|
+
f"{'' if len(others) > 1 else 's'}:\n"
|
|
1206
|
+
f"{_connections.candidate_listing(others)}\n"
|
|
1207
|
+
"Re-run against one of those, or check the value. This run kept the "
|
|
1208
|
+
"connection you named."
|
|
1209
|
+
)
|
|
1210
|
+
sys.exit(f"prepare-connector: {first}{extra}")
|
|
1211
|
+
|
|
1212
|
+
try:
|
|
1213
|
+
candidates = _connections.candidate_connections(args.key, args.connection)
|
|
1214
|
+
except _connections.ConnectionError_ as error:
|
|
1215
|
+
# The listing itself failed; report the original lookup failure, which is
|
|
1216
|
+
# the thing the author asked about.
|
|
1217
|
+
sys.exit(f"prepare-connector: {first}\n(could not list other connections: {error})")
|
|
1218
|
+
|
|
1219
|
+
others = [row for row in candidates
|
|
1220
|
+
if str(row.get("Id")) != str(args.connection_id)]
|
|
1221
|
+
if not others:
|
|
1222
|
+
sys.exit(
|
|
1223
|
+
f"prepare-connector: {first}\n"
|
|
1224
|
+
f"This is the only enabled {args.key} connection on the tenant, so the "
|
|
1225
|
+
"value is what to check."
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
print(f" {first.field}: not on this connection; trying "
|
|
1229
|
+
f"{len(others)} other {args.key} connection(s)")
|
|
1230
|
+
winners: list[dict] = []
|
|
1231
|
+
report: list[str] = []
|
|
1232
|
+
for row in others:
|
|
1233
|
+
outcome = _probe_candidate(args, specs, ordered, str(row.get("Id")))
|
|
1234
|
+
if outcome is None:
|
|
1235
|
+
winners.append(row)
|
|
1236
|
+
report.append(f" {_candidate_label(row)} resolves all "
|
|
1237
|
+
f"{len(ordered)} lookup(s)")
|
|
1238
|
+
continue
|
|
1239
|
+
kind, error = outcome
|
|
1240
|
+
if kind == "exhausted":
|
|
1241
|
+
report.append(
|
|
1242
|
+
f" {_candidate_label(row)} {error.field}: "
|
|
1243
|
+
f"{error.scanned} record(s) scanned, no match")
|
|
1244
|
+
else:
|
|
1245
|
+
report.append(f" {_candidate_label(row)} {error}")
|
|
1246
|
+
|
|
1247
|
+
if len(winners) == 1:
|
|
1248
|
+
chosen = winners[0]
|
|
1249
|
+
# Undo the bindings this run wrote for the connection that cannot serve
|
|
1250
|
+
# the lookups, then start over pinned to the one that can. Re-running
|
|
1251
|
+
# rather than switching in place is deliberate: the overlay and its
|
|
1252
|
+
# lookup specs are described THROUGH a connection
|
|
1253
|
+
# (`_lookup_specs_from_overlay`), so adopting a different connection
|
|
1254
|
+
# after the describe would leave the overlay describing one connection
|
|
1255
|
+
# while bindings.json names another.
|
|
1256
|
+
_restore_bindings(args)
|
|
1257
|
+
print(f" connection: {_candidate_label(chosen)} resolves "
|
|
1258
|
+
f"{first.field}; re-running against it "
|
|
1259
|
+
f"(--connection-id {chosen.get('Id')})")
|
|
1260
|
+
argv = [sys.executable, *sys.argv,
|
|
1261
|
+
"--connection-id", str(chosen.get("Id"))]
|
|
1262
|
+
# The ledger keys a resolution by [nodeType, field, by, value] and NOT by
|
|
1263
|
+
# connection, so an id recorded against the connection just abandoned
|
|
1264
|
+
# would be reused as "already resolved". Force the re-run to resolve
|
|
1265
|
+
# against the connection it actually pins.
|
|
1266
|
+
if not args.refresh_lookups:
|
|
1267
|
+
argv.append("--refresh-lookups")
|
|
1268
|
+
sys.stdout.flush()
|
|
1269
|
+
try:
|
|
1270
|
+
os.execv(sys.executable, argv)
|
|
1271
|
+
except OSError as error:
|
|
1272
|
+
# `execv` does not return on success, so reaching here means the
|
|
1273
|
+
# re-run could not start. Hand back the command rather than a
|
|
1274
|
+
# traceback: the author is one paste from the answer.
|
|
1275
|
+
pinned = " ".join(argv[2:])
|
|
1276
|
+
sys.exit(
|
|
1277
|
+
f"prepare-connector: {first}\n"
|
|
1278
|
+
f"{_candidate_label(chosen)} resolves it, but this command could "
|
|
1279
|
+
f"not re-run itself ({error}). Run:\n prepare {pinned}"
|
|
1280
|
+
)
|
|
1281
|
+
# Unreachable in production — execv replaced the process. Kept so the
|
|
1282
|
+
# function can never fall through to the report below, which would
|
|
1283
|
+
# contradict the line just printed.
|
|
1284
|
+
raise SystemExit(0)
|
|
1285
|
+
|
|
1286
|
+
if len(winners) > 1:
|
|
1287
|
+
sys.exit(
|
|
1288
|
+
f"prepare-connector: {len(winners)} {args.key} connections resolve "
|
|
1289
|
+
f"every requested lookup, and they are not interchangeable — the ids "
|
|
1290
|
+
f"they return differ per connection. Pick the one you mean:\n"
|
|
1291
|
+
f"{_connections.candidate_listing(winners)}"
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
# Nothing resolved anywhere. Report every candidate and what it held, the
|
|
1295
|
+
# one connection first, so the reader can see the value really is absent
|
|
1296
|
+
# rather than guess which connection was asked.
|
|
1297
|
+
original = next(
|
|
1298
|
+
(row for row in candidates
|
|
1299
|
+
if str(row.get("Id")) == str(args.connection_id)),
|
|
1300
|
+
{"Name": args.connection_id, "Folder": "?"},
|
|
1301
|
+
)
|
|
1302
|
+
lines = [f" {_candidate_label(original)} {first.field}: "
|
|
1303
|
+
f"{first.scanned} record(s) scanned, no match", *report]
|
|
1304
|
+
# HERE is where the collection listing finally belongs: every connection has
|
|
1305
|
+
# been tried, so seeing what the collection actually holds is the next step
|
|
1306
|
+
# rather than the detour it is while a retry is still available.
|
|
1307
|
+
sys.exit(
|
|
1308
|
+
f"prepare-connector: no {args.key} connection could resolve "
|
|
1309
|
+
f"{first.field}: {first.by}={first.value!r}\n"
|
|
1310
|
+
+ "\n".join(lines) + "\n"
|
|
1311
|
+
"Every candidate was read to the end, so the value is what to check now. "
|
|
1312
|
+
f"To see what {first.collection!r} holds:\n"
|
|
1313
|
+
f" uip is resources run list {args.key} {first.collection} "
|
|
1314
|
+
f"--connection-id {args.connection_id}"
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
|
|
1146
1318
|
def _lookup_specs_from_overlay(
|
|
1147
1319
|
out_dir: str, node_type: str, object_name: str | None = None,
|
|
1148
1320
|
) -> dict:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"@types/node": "^22.7.0",
|
|
88
88
|
"esbuild": "^0.28.1"
|
|
89
89
|
},
|
|
90
|
-
"gitref": "
|
|
90
|
+
"gitref": "21bfc3d02150e62bfca808d6dfdbf8b4c320870f"
|
|
91
91
|
}
|