@styx-api/core 0.3.0 → 0.5.0
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.
- package/dist/index.cjs +1027 -273
- package/dist/index.d.cts +226 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +226 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1020 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/index.ts +13 -1
- package/src/backend/nipype/emit.ts +270 -0
- package/src/backend/nipype/index.ts +3 -0
- package/src/backend/nipype/nipype.ts +70 -0
- package/src/backend/pydra/emit.ts +318 -0
- package/src/backend/pydra/index.ts +3 -0
- package/src/backend/pydra/pydra.ts +67 -0
- package/src/backend/python/index.ts +6 -1
- package/src/backend/python/outputs-emit.ts +1 -1
- package/src/backend/python/packaging.ts +33 -5
- package/src/backend/python/python.ts +28 -3
- package/src/backend/schema/jsonschema.ts +34 -4
- package/src/backend/styxdefs-compat.ts +6 -4
- package/src/backend/typed-spec.ts +258 -0
- package/src/frontend/mrtrix/parser.ts +51 -8
package/dist/index.cjs
CHANGED
|
@@ -15,7 +15,7 @@ function isObject$3(x) {
|
|
|
15
15
|
function isString$3(x) {
|
|
16
16
|
return typeof x === "string";
|
|
17
17
|
}
|
|
18
|
-
function isNumber$
|
|
18
|
+
function isNumber$2(x) {
|
|
19
19
|
return typeof x === "number";
|
|
20
20
|
}
|
|
21
21
|
function isArray$3(x) {
|
|
@@ -100,7 +100,7 @@ var ArgdumpParser = class {
|
|
|
100
100
|
const choices = action.choices;
|
|
101
101
|
if (isArray$3(choices) && choices.length > 0) {
|
|
102
102
|
const alts = [];
|
|
103
|
-
for (const choice of choices) if (isString$3(choice) || isNumber$
|
|
103
|
+
for (const choice of choices) if (isString$3(choice) || isNumber$2(choice)) alts.push({
|
|
104
104
|
kind: "literal",
|
|
105
105
|
attrs: { str: String(choice) }
|
|
106
106
|
});
|
|
@@ -233,7 +233,7 @@ var ArgdumpParser = class {
|
|
|
233
233
|
countMin: 1
|
|
234
234
|
}
|
|
235
235
|
};
|
|
236
|
-
if (isNumber$
|
|
236
|
+
if (isNumber$2(nargs) && Number.isInteger(nargs) && nargs >= 0) {
|
|
237
237
|
if (nargs === 1) return node;
|
|
238
238
|
return {
|
|
239
239
|
kind: "repeat",
|
|
@@ -253,7 +253,7 @@ var ArgdumpParser = class {
|
|
|
253
253
|
const name = this.preferredName(action) ?? (isString$3(dest) ? dest : void 0);
|
|
254
254
|
const hasName = name !== void 0;
|
|
255
255
|
const hasHelp = isString$3(help) && !isSuppressed(help);
|
|
256
|
-
const hasDefault = (isString$3(defaultVal) || isNumber$
|
|
256
|
+
const hasDefault = (isString$3(defaultVal) || isNumber$2(defaultVal) || typeof defaultVal === "boolean") && !isSuppressed(defaultVal);
|
|
257
257
|
if (!hasName && !hasHelp && !hasDefault) return void 0;
|
|
258
258
|
return {
|
|
259
259
|
...hasName && { name },
|
|
@@ -854,7 +854,7 @@ function isObject$2(x) {
|
|
|
854
854
|
function isString$2(x) {
|
|
855
855
|
return typeof x === "string";
|
|
856
856
|
}
|
|
857
|
-
function isNumber(x) {
|
|
857
|
+
function isNumber$1(x) {
|
|
858
858
|
return typeof x === "number";
|
|
859
859
|
}
|
|
860
860
|
function isArray$2(x) {
|
|
@@ -952,7 +952,7 @@ var BoutiquesParser = class {
|
|
|
952
952
|
const title = btInput.name;
|
|
953
953
|
const description = btInput.description;
|
|
954
954
|
const defaultValue = btInput["default-value"];
|
|
955
|
-
const hasDefault = isString$2(defaultValue) || isNumber(defaultValue) || typeof defaultValue === "boolean";
|
|
955
|
+
const hasDefault = isString$2(defaultValue) || isNumber$1(defaultValue) || typeof defaultValue === "boolean";
|
|
956
956
|
if (!isString$2(name) && !isString$2(title) && !isString$2(description) && !hasDefault) return;
|
|
957
957
|
return {
|
|
958
958
|
...isString$2(name) && { name },
|
|
@@ -1076,7 +1076,7 @@ var BoutiquesParser = class {
|
|
|
1076
1076
|
kind: "literal",
|
|
1077
1077
|
attrs: { str: choice }
|
|
1078
1078
|
});
|
|
1079
|
-
else if (isNumber(choice)) alts.push({
|
|
1079
|
+
else if (isNumber$1(choice)) alts.push({
|
|
1080
1080
|
kind: "literal",
|
|
1081
1081
|
attrs: { str: String(choice) }
|
|
1082
1082
|
});
|
|
@@ -1113,11 +1113,11 @@ var BoutiquesParser = class {
|
|
|
1113
1113
|
kind: "int",
|
|
1114
1114
|
attrs: {}
|
|
1115
1115
|
};
|
|
1116
|
-
if (isNumber(btInput.minimum)) {
|
|
1116
|
+
if (isNumber$1(btInput.minimum)) {
|
|
1117
1117
|
node.attrs.minValue = Math.floor(btInput.minimum);
|
|
1118
1118
|
if (btInput["exclusive-minimum"] === true) node.attrs.minValue += 1;
|
|
1119
1119
|
}
|
|
1120
|
-
if (isNumber(btInput.maximum)) {
|
|
1120
|
+
if (isNumber$1(btInput.maximum)) {
|
|
1121
1121
|
node.attrs.maxValue = Math.floor(btInput.maximum);
|
|
1122
1122
|
if (btInput["exclusive-maximum"] === true) node.attrs.maxValue -= 1;
|
|
1123
1123
|
}
|
|
@@ -1129,8 +1129,8 @@ var BoutiquesParser = class {
|
|
|
1129
1129
|
kind: "float",
|
|
1130
1130
|
attrs: {}
|
|
1131
1131
|
};
|
|
1132
|
-
if (isNumber(btInput.minimum)) node.attrs.minValue = btInput.minimum;
|
|
1133
|
-
if (isNumber(btInput.maximum)) node.attrs.maxValue = btInput.maximum;
|
|
1132
|
+
if (isNumber$1(btInput.minimum)) node.attrs.minValue = btInput.minimum;
|
|
1133
|
+
if (isNumber$1(btInput.maximum)) node.attrs.maxValue = btInput.maximum;
|
|
1134
1134
|
if (meta) node.meta = meta;
|
|
1135
1135
|
return node;
|
|
1136
1136
|
}
|
|
@@ -1235,8 +1235,8 @@ var BoutiquesParser = class {
|
|
|
1235
1235
|
attrs: {
|
|
1236
1236
|
node,
|
|
1237
1237
|
...isString$2(btInput["list-separator"]) && { join: btInput["list-separator"] },
|
|
1238
|
-
...isNumber(btInput["min-list-entries"]) && { countMin: btInput["min-list-entries"] },
|
|
1239
|
-
...isNumber(btInput["max-list-entries"]) && { countMax: btInput["max-list-entries"] }
|
|
1238
|
+
...isNumber$1(btInput["min-list-entries"]) && { countMin: btInput["min-list-entries"] },
|
|
1239
|
+
...isNumber$1(btInput["max-list-entries"]) && { countMax: btInput["max-list-entries"] }
|
|
1240
1240
|
}
|
|
1241
1241
|
};
|
|
1242
1242
|
}
|
|
@@ -1486,6 +1486,9 @@ function isObject$1(x) {
|
|
|
1486
1486
|
function isString$1(x) {
|
|
1487
1487
|
return typeof x === "string";
|
|
1488
1488
|
}
|
|
1489
|
+
function isNumber(x) {
|
|
1490
|
+
return typeof x === "number" && Number.isFinite(x);
|
|
1491
|
+
}
|
|
1489
1492
|
function isArray$1(x) {
|
|
1490
1493
|
return Array.isArray(x);
|
|
1491
1494
|
}
|
|
@@ -1532,9 +1535,10 @@ function emptyExpr$1() {
|
|
|
1532
1535
|
* - option, 1 arg -> opt(seq(lit(-switch), value)) (flat optional)
|
|
1533
1536
|
* - option, >1 arg / multi -> opt|rep(seq(lit(-switch), ...)) (sub-struct)
|
|
1534
1537
|
*
|
|
1535
|
-
* Type mapping mirrors the v1 `mrt2bt.js` converter's `set_type
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
+
* Type mapping mirrors the v1 `mrt2bt.js` converter's `set_type`, plus `choice`
|
|
1539
|
+
* values: when the dump carries `choices` it lowers to an enum (an alternative
|
|
1540
|
+
* of literals), else a plain string (older dumps that omit the values - as v1
|
|
1541
|
+
* always did). Per-command quirks v1 hand-coded that the flat dump cannot
|
|
1538
1542
|
* express (e.g. dwi2fod/mtnormalise paired in/out args) are intentionally NOT
|
|
1539
1543
|
* special-cased here - they are patched on the niwrap side post-dump, keeping
|
|
1540
1544
|
* this frontend format-general.
|
|
@@ -1617,6 +1621,19 @@ var MrtrixParser = class {
|
|
|
1617
1621
|
};
|
|
1618
1622
|
}
|
|
1619
1623
|
/**
|
|
1624
|
+
* Integer/float bounds, when the dump carries them. The C++ hook serializes
|
|
1625
|
+
* `Argument::limits.{i,f}.{min,max}` as `min`/`max`, omitting the unbounded
|
|
1626
|
+
* sentinels - so a present value is a real, tool-enforced bound.
|
|
1627
|
+
*/
|
|
1628
|
+
numericBounds(arg) {
|
|
1629
|
+
const lo = arg.min;
|
|
1630
|
+
const hi = arg.max;
|
|
1631
|
+
return {
|
|
1632
|
+
...isNumber(lo) && { minValue: lo },
|
|
1633
|
+
...isNumber(hi) && { maxValue: hi }
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1620
1637
|
* Lower one MRtrix argument to its terminal node (carrying name + doc) and,
|
|
1621
1638
|
* for output types, an accompanying `Output`.
|
|
1622
1639
|
*/
|
|
@@ -1628,11 +1645,31 @@ var MrtrixParser = class {
|
|
|
1628
1645
|
}
|
|
1629
1646
|
const name = meta.name;
|
|
1630
1647
|
switch (argType) {
|
|
1631
|
-
case "integer":
|
|
1632
|
-
|
|
1648
|
+
case "integer": {
|
|
1649
|
+
const node = int(meta);
|
|
1650
|
+
Object.assign(node.attrs, this.numericBounds(arg));
|
|
1651
|
+
return { node };
|
|
1652
|
+
}
|
|
1653
|
+
case "float": {
|
|
1654
|
+
const node = float(meta);
|
|
1655
|
+
Object.assign(node.attrs, this.numericBounds(arg));
|
|
1656
|
+
return { node };
|
|
1657
|
+
}
|
|
1633
1658
|
case "text":
|
|
1634
|
-
case "
|
|
1635
|
-
case "
|
|
1659
|
+
case "undefined":
|
|
1660
|
+
case "boolean": return { node: str(meta) };
|
|
1661
|
+
case "choice": {
|
|
1662
|
+
const choices = arg.choices;
|
|
1663
|
+
if (isArray$1(choices)) {
|
|
1664
|
+
const alts = choices.filter(isString$1).map((c) => lit(c));
|
|
1665
|
+
if (alts.length > 0) {
|
|
1666
|
+
const node = alt(...alts);
|
|
1667
|
+
node.meta = meta;
|
|
1668
|
+
return { node };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return { node: str(meta) };
|
|
1672
|
+
}
|
|
1636
1673
|
case "int seq": {
|
|
1637
1674
|
const node = repJoin(",", int());
|
|
1638
1675
|
node.meta = meta;
|
|
@@ -3351,196 +3388,6 @@ function resolveTypeName(namedTypes) {
|
|
|
3351
3388
|
};
|
|
3352
3389
|
}
|
|
3353
3390
|
|
|
3354
|
-
//#endregion
|
|
3355
|
-
//#region src/backend/styxdefs-compat.ts
|
|
3356
|
-
/**
|
|
3357
|
-
* Runtime version floors baked into generated dependency metadata.
|
|
3358
|
-
*
|
|
3359
|
-
* styx2-generated code calls `mutable_copy` / `mutableCopy` (introduced in the
|
|
3360
|
-
* styxdefs 0.7.0 / styxdefs-js 0.2.0 release), so emitted packages genuinely
|
|
3361
|
-
* require that runtime floor. This is the single source of truth: bump here and
|
|
3362
|
-
* both the Python and TypeScript backends pick it up.
|
|
3363
|
-
*/
|
|
3364
|
-
const STYXDEFS_COMPAT = {
|
|
3365
|
-
python: ">=0.7.0,<0.8.0",
|
|
3366
|
-
npm: "^0.2.0"
|
|
3367
|
-
};
|
|
3368
|
-
/**
|
|
3369
|
-
* Extra Python runtime packages the root distribution pulls in (container +
|
|
3370
|
-
* graph runners). Left unpinned - styxdefs's floor constrains them transitively
|
|
3371
|
-
* via their own inter-package pins.
|
|
3372
|
-
*/
|
|
3373
|
-
const PYTHON_RUNNER_DEPS = [
|
|
3374
|
-
"styxdocker",
|
|
3375
|
-
"styxsingularity",
|
|
3376
|
-
"styxgraph"
|
|
3377
|
-
];
|
|
3378
|
-
|
|
3379
|
-
//#endregion
|
|
3380
|
-
//#region src/backend/python/packaging.ts
|
|
3381
|
-
const REQUIRES_PYTHON = ">=3.10";
|
|
3382
|
-
const BUILD_SYSTEM = `[build-system]
|
|
3383
|
-
requires = ["setuptools>=61"]
|
|
3384
|
-
build-backend = "setuptools.build_meta"`;
|
|
3385
|
-
/** Escape a value for embedding in a TOML basic string. */
|
|
3386
|
-
function tomlStr(s) {
|
|
3387
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
3388
|
-
}
|
|
3389
|
-
/** Suite directory / importable package name; matches the CLI's `pkgDir` fallback. */
|
|
3390
|
-
function pkgDir(pkg) {
|
|
3391
|
-
return pkg.name ?? "package";
|
|
3392
|
-
}
|
|
3393
|
-
/** Distribution (PyPI) name for a package: `<project>_<package>`, or just `<package>`. */
|
|
3394
|
-
function pyDistName(proj, pkg) {
|
|
3395
|
-
const name = pkgDir(pkg);
|
|
3396
|
-
return proj.name ? `${proj.name}_${name}` : name;
|
|
3397
|
-
}
|
|
3398
|
-
const SUMMARY_MAX_LEN = 512;
|
|
3399
|
-
/**
|
|
3400
|
-
* Clamp a description to a <=512-char one-line summary. Prefer cutting at the
|
|
3401
|
-
* last complete sentence that fits (clean, no dangling fragment); if there is no
|
|
3402
|
-
* sentence boundary early enough, cut at a word boundary and mark the elision.
|
|
3403
|
-
*/
|
|
3404
|
-
function clampSummary(s) {
|
|
3405
|
-
if (s.length <= SUMMARY_MAX_LEN) return s;
|
|
3406
|
-
const window = s.slice(0, SUMMARY_MAX_LEN);
|
|
3407
|
-
const lastSentence = window.lastIndexOf(". ");
|
|
3408
|
-
if (lastSentence >= 0) return s.slice(0, lastSentence + 1);
|
|
3409
|
-
const ellipsis = "...";
|
|
3410
|
-
const body = window.slice(0, SUMMARY_MAX_LEN - 3);
|
|
3411
|
-
const lastSpace = body.lastIndexOf(" ");
|
|
3412
|
-
return (lastSpace > 0 ? body.slice(0, lastSpace) : body).replace(/[.,;:\s]+$/, "") + ellipsis;
|
|
3413
|
-
}
|
|
3414
|
-
function description$1(doc, fallbackName) {
|
|
3415
|
-
return clampSummary(doc?.description ?? `Styx generated wrappers for ${doc?.title ?? fallbackName ?? "tools"}.`);
|
|
3416
|
-
}
|
|
3417
|
-
function authorsField(doc) {
|
|
3418
|
-
return `[${(doc?.authors?.length ? doc.authors : ["unknown"]).map((a) => `{ name = "${tomlStr(a)}" }`).join(", ")}]`;
|
|
3419
|
-
}
|
|
3420
|
-
function licenseField(proj) {
|
|
3421
|
-
return `{ text = "${tomlStr(proj.license?.description ?? "unknown")}" }`;
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
|
-
* Per-suite `pyproject.toml`. The flat layout (`python/<pkg>/bet.py`) makes the
|
|
3425
|
-
* directory itself the importable package, so setuptools' `package-dir` maps the
|
|
3426
|
-
* import name (`<pkg>`) onto the distribution's root directory. The styxdefs
|
|
3427
|
-
* floor is the only runtime dependency.
|
|
3428
|
-
*
|
|
3429
|
-
* Precondition: `pkg.name` must be a valid Python identifier - the flat layout's
|
|
3430
|
-
* relative imports (`from .bet import *`) already require this, and the CLI uses
|
|
3431
|
-
* it verbatim as the directory name, so this stays consistent with that.
|
|
3432
|
-
*/
|
|
3433
|
-
function generateSubPyproject(proj, pkg) {
|
|
3434
|
-
const importName = pkgDir(pkg);
|
|
3435
|
-
const cb = new CodeBuilder(" ");
|
|
3436
|
-
cb.line("[project]");
|
|
3437
|
-
cb.line(`name = "${tomlStr(pyDistName(proj, pkg))}"`);
|
|
3438
|
-
cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
|
|
3439
|
-
cb.line(`description = "${tomlStr(description$1(pkg.doc, pkg.name))}"`);
|
|
3440
|
-
cb.line(`readme = "README.md"`);
|
|
3441
|
-
cb.line(`license = ${licenseField(proj)}`);
|
|
3442
|
-
cb.line(`authors = ${authorsField(pkg.doc ?? proj.doc)}`);
|
|
3443
|
-
cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
|
|
3444
|
-
cb.line("dependencies = [");
|
|
3445
|
-
cb.line(` "styxdefs${STYXDEFS_COMPAT.python}",`);
|
|
3446
|
-
cb.line("]");
|
|
3447
|
-
cb.blank();
|
|
3448
|
-
cb.line("[tool.setuptools]");
|
|
3449
|
-
cb.line(`packages = ["${importName}"]`);
|
|
3450
|
-
cb.line(`package-dir = { "${importName}" = "." }`);
|
|
3451
|
-
cb.blank();
|
|
3452
|
-
cb.line("[tool.setuptools.package-data]");
|
|
3453
|
-
cb.line(`"${importName}" = ["py.typed"]`);
|
|
3454
|
-
cb.blank();
|
|
3455
|
-
cb.line(BUILD_SYSTEM);
|
|
3456
|
-
return cb.toString() + "\n";
|
|
3457
|
-
}
|
|
3458
|
-
/**
|
|
3459
|
-
* Root `pyproject.toml`: a metapackage depending on each per-suite distribution
|
|
3460
|
-
* plus the container/graph runner packages. `packages = []` keeps setuptools
|
|
3461
|
-
* from sweeping the sibling suite directories into this distribution.
|
|
3462
|
-
*/
|
|
3463
|
-
function generateRootPyproject(proj, distNames) {
|
|
3464
|
-
const cb = new CodeBuilder(" ");
|
|
3465
|
-
cb.line("[project]");
|
|
3466
|
-
cb.line(`name = "${tomlStr(proj.name ?? "project")}"`);
|
|
3467
|
-
cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
|
|
3468
|
-
cb.line(`description = "${tomlStr(description$1(proj.doc, proj.name))}"`);
|
|
3469
|
-
cb.line(`readme = "README.md"`);
|
|
3470
|
-
cb.line(`license = ${licenseField(proj)}`);
|
|
3471
|
-
cb.line(`authors = ${authorsField(proj.doc)}`);
|
|
3472
|
-
cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
|
|
3473
|
-
cb.line("dependencies = [");
|
|
3474
|
-
for (const dep of PYTHON_RUNNER_DEPS) cb.line(` "${dep}",`);
|
|
3475
|
-
for (const dist of distNames) cb.line(` "${tomlStr(dist)}",`);
|
|
3476
|
-
cb.line("]");
|
|
3477
|
-
cb.blank();
|
|
3478
|
-
cb.line("[tool.setuptools]");
|
|
3479
|
-
cb.line("packages = []");
|
|
3480
|
-
cb.blank();
|
|
3481
|
-
cb.line(BUILD_SYSTEM);
|
|
3482
|
-
return cb.toString() + "\n";
|
|
3483
|
-
}
|
|
3484
|
-
/** Per-suite README crediting the upstream tool authors. */
|
|
3485
|
-
function generateSubReadme(proj, pkg) {
|
|
3486
|
-
const projectTitle = proj.doc?.title ?? proj.name ?? "Styx";
|
|
3487
|
-
const packageTitle = pkg.doc?.title ?? pkg.name ?? "package";
|
|
3488
|
-
const url = pkg.doc?.urls?.[0];
|
|
3489
|
-
const titleMd = url ? `[${packageTitle}](${url})` : packageTitle;
|
|
3490
|
-
const credits = pkg.doc?.authors?.length ? pkg.doc.authors.join(", ") : pkg.doc?.urls?.join(", ") ?? "unknown";
|
|
3491
|
-
return `# ${projectTitle} wrappers for ${titleMd}${pkg.doc?.description ? `\n\n${pkg.doc.description}` : ""}\n\n${packageTitle} is made by ${credits}.\n\nThis package contains wrappers only and has no affiliation with the original authors.\n`;
|
|
3492
|
-
}
|
|
3493
|
-
/** Root README listing the bundled per-suite distributions. */
|
|
3494
|
-
function generateRootReadme(proj, distNames) {
|
|
3495
|
-
return `# ${proj.doc?.title ?? proj.name ?? "Styx"}\n${proj.doc?.description ? `\n${proj.doc.description}\n` : ""}\nAuto-generated Styx wrappers. This project bundles the following packages:\n\n${distNames.map((d) => `- ${d}`).join("\n")}\n`;
|
|
3496
|
-
}
|
|
3497
|
-
/** Local-install manifest: each suite directory first, then the root metapackage. */
|
|
3498
|
-
function generateRequirementsTxt(pkgDirs) {
|
|
3499
|
-
return [...pkgDirs.map((d) => `./${d}`), "./"].join("\n") + "\n";
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
//#endregion
|
|
3503
|
-
//#region src/backend/sig-entries.ts
|
|
3504
|
-
/**
|
|
3505
|
-
* Build per-field signature entries for the kwarg wrapper and params factory.
|
|
3506
|
-
* Skips `@type` (the factory injects it as a constant). Required-no-default
|
|
3507
|
-
* entries are placed before defaulted ones so the resulting signature is
|
|
3508
|
-
* syntactically valid in both Python and TS.
|
|
3509
|
-
*
|
|
3510
|
-
* `registerLocal` is called once per field with the wire key; it must return
|
|
3511
|
-
* a scrubbed, unique host identifier (typically by combining a language-aware
|
|
3512
|
-
* scrub function with `Scope.add()`). The caller's scope should already have
|
|
3513
|
-
* the function's other locals (`params`, `runner`, ...) reserved so this
|
|
3514
|
-
* registration cannot collide with them.
|
|
3515
|
-
*/
|
|
3516
|
-
function buildSigEntries(rootType, fieldInfo, registerLocal, opts) {
|
|
3517
|
-
const entries = [];
|
|
3518
|
-
for (const [fieldName, fieldType] of Object.entries(rootType.fields)) {
|
|
3519
|
-
if (fieldType.kind === "literal") continue;
|
|
3520
|
-
const fi = fieldInfo.get(fieldName);
|
|
3521
|
-
const isOptional = fieldType.kind === "optional";
|
|
3522
|
-
const inner = isOptional ? fieldType.inner : fieldType;
|
|
3523
|
-
let sigType = opts.renderType(inner);
|
|
3524
|
-
if (isOptional) sigType += opts.nullableSuffix;
|
|
3525
|
-
let sigDefault;
|
|
3526
|
-
const hasExplicitDefault = fi?.defaultValue !== void 0;
|
|
3527
|
-
if (hasExplicitDefault) sigDefault = opts.renderDefault(fi.defaultValue);
|
|
3528
|
-
else if (isOptional) sigDefault = opts.nullableDefault;
|
|
3529
|
-
entries.push({
|
|
3530
|
-
name: registerLocal(fieldName),
|
|
3531
|
-
wireKey: fieldName,
|
|
3532
|
-
sigType,
|
|
3533
|
-
sigDefault,
|
|
3534
|
-
isOptional,
|
|
3535
|
-
hasExplicitDefault,
|
|
3536
|
-
doc: fi?.doc
|
|
3537
|
-
});
|
|
3538
|
-
}
|
|
3539
|
-
const required = entries.filter((e) => e.sigDefault === void 0);
|
|
3540
|
-
const defaulted = entries.filter((e) => e.sigDefault !== void 0);
|
|
3541
|
-
return [...required, ...defaulted];
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
3391
|
//#endregion
|
|
3545
3392
|
//#region src/backend/union-variants.ts
|
|
3546
3393
|
/**
|
|
@@ -4360,65 +4207,455 @@ function emitKwargWrapper$1(ctx, entries, funcName, paramsFnName, executeFnName,
|
|
|
4360
4207
|
}
|
|
4361
4208
|
|
|
4362
4209
|
//#endregion
|
|
4363
|
-
//#region src/backend/
|
|
4210
|
+
//#region src/backend/nipype/emit.ts
|
|
4211
|
+
function call(ctor, args) {
|
|
4212
|
+
return `${ctor}(${args.join(", ")})`;
|
|
4213
|
+
}
|
|
4364
4214
|
/**
|
|
4365
|
-
*
|
|
4366
|
-
*
|
|
4367
|
-
*
|
|
4368
|
-
*
|
|
4215
|
+
* Render a numeric literal, forcing a float form (e.g. `0.0`) for a float field.
|
|
4216
|
+
* `traits.Range` infers its numeric type from the bound literals, so integer
|
|
4217
|
+
* bounds on a float field (e.g. `low=0, high=1`) would build an *integer* range
|
|
4218
|
+
* that rejects `0.5`. Emitting `0.0`/`1.0` keeps it a float range.
|
|
4369
4219
|
*/
|
|
4370
|
-
function
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4220
|
+
function renderNum(value, asFloat) {
|
|
4221
|
+
if (asFloat && typeof value === "number" && Number.isInteger(value)) return `${value}.0`;
|
|
4222
|
+
return renderPyLiteral(value);
|
|
4223
|
+
}
|
|
4224
|
+
/** Human-readable `desc=` text: doc + degrade/media-type notes. */
|
|
4225
|
+
function descText(p) {
|
|
4226
|
+
const parts = [];
|
|
4227
|
+
if (p.doc) parts.push(p.doc);
|
|
4228
|
+
if (p.kind === "struct" || p.kind === "union") parts.push("(nested configuration; pass a dict)");
|
|
4229
|
+
if (p.mediaTypes && p.mediaTypes.length > 0) parts.push(`(media types: ${p.mediaTypes.join(", ")})`);
|
|
4230
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
4231
|
+
}
|
|
4232
|
+
/** Map a list element descriptor to a (bare) nipype inner trait. */
|
|
4233
|
+
function renderItemTrait(item) {
|
|
4234
|
+
if (!item) return "traits.Any()";
|
|
4235
|
+
switch (item.kind) {
|
|
4236
|
+
case "path": return "File(exists=True)";
|
|
4237
|
+
case "int":
|
|
4238
|
+
case "count": return "traits.Int()";
|
|
4239
|
+
case "float": return "traits.Float()";
|
|
4240
|
+
case "str": return "traits.Str()";
|
|
4241
|
+
case "bool": return "traits.Bool()";
|
|
4242
|
+
case "enum": return call("traits.Enum", (item.choices ?? []).map((c) => renderPyLiteral(c)));
|
|
4243
|
+
default: return "traits.Any()";
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
/** Map a parameter to its nipype input trait expression, carrying rich constraints. */
|
|
4247
|
+
function renderInputTrait(p) {
|
|
4248
|
+
const tail = [];
|
|
4249
|
+
if (p.mandatory) tail.push("mandatory=True");
|
|
4250
|
+
const desc = descText(p);
|
|
4251
|
+
if (desc) tail.push(`desc=${pyStr(desc)}`);
|
|
4252
|
+
const hasDef = p.hasDefault && p.default !== void 0;
|
|
4253
|
+
const def = p.default;
|
|
4254
|
+
switch (p.kind) {
|
|
4255
|
+
case "path": return call("File", ["exists=True", ...tail]);
|
|
4256
|
+
case "bool": return call("traits.Bool", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
|
|
4257
|
+
case "count": return call("traits.Int", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
|
|
4258
|
+
case "int":
|
|
4259
|
+
case "float": {
|
|
4260
|
+
const asFloat = p.kind === "float";
|
|
4261
|
+
if (p.range) {
|
|
4262
|
+
const a = [];
|
|
4263
|
+
if (hasDef) a.push(`value=${renderNum(def, asFloat)}`);
|
|
4264
|
+
if (p.range.min !== void 0) a.push(`low=${renderNum(p.range.min, asFloat)}`);
|
|
4265
|
+
if (p.range.max !== void 0) a.push(`high=${renderNum(p.range.max, asFloat)}`);
|
|
4266
|
+
if (hasDef) a.push("usedefault=True");
|
|
4267
|
+
return call("traits.Range", [...a, ...tail]);
|
|
4268
|
+
}
|
|
4269
|
+
return call(p.kind === "int" ? "traits.Int" : "traits.Float", [...hasDef ? [renderNum(def, asFloat), "usedefault=True"] : [], ...tail]);
|
|
4270
|
+
}
|
|
4271
|
+
case "str": return call("traits.Str", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
|
|
4272
|
+
case "enum": {
|
|
4273
|
+
const choices = p.choices ?? [];
|
|
4274
|
+
return call("traits.Enum", [
|
|
4275
|
+
...(hasDef ? [def, ...choices.filter((c) => c !== def)] : choices).map((c) => renderPyLiteral(c)),
|
|
4276
|
+
...hasDef ? ["usedefault=True"] : [],
|
|
4277
|
+
...tail
|
|
4278
|
+
]);
|
|
4279
|
+
}
|
|
4280
|
+
case "list": {
|
|
4281
|
+
const inner = renderItemTrait(p.itemType);
|
|
4282
|
+
const bounds = [];
|
|
4283
|
+
if (p.listBounds?.min !== void 0) bounds.push(`minlen=${p.listBounds.min}`);
|
|
4284
|
+
if (p.listBounds?.max !== void 0) bounds.push(`maxlen=${p.listBounds.max}`);
|
|
4285
|
+
return call("traits.List", [
|
|
4286
|
+
inner,
|
|
4287
|
+
...bounds,
|
|
4288
|
+
...tail
|
|
4289
|
+
]);
|
|
4377
4290
|
}
|
|
4291
|
+
case "struct":
|
|
4292
|
+
case "union": return call("traits.Any", tail);
|
|
4378
4293
|
}
|
|
4379
|
-
const fieldInfo = collectFieldInfo(ctx, structType);
|
|
4380
|
-
return Object.entries(structType.fields).map(([name, type]) => ({
|
|
4381
|
-
name,
|
|
4382
|
-
type,
|
|
4383
|
-
node: nodeByName.get(name),
|
|
4384
|
-
hasDefault: fieldInfo.get(name)?.defaultValue !== void 0
|
|
4385
|
-
}));
|
|
4386
4294
|
}
|
|
4387
4295
|
/**
|
|
4388
|
-
*
|
|
4389
|
-
*
|
|
4390
|
-
* (sequence/optional/repeat/alternative).
|
|
4296
|
+
* Map an output field to its nipype output trait expression. `isRoot` is the
|
|
4297
|
+
* synthetic output directory, typed as a `Directory` rather than a `File`.
|
|
4391
4298
|
*/
|
|
4392
|
-
function
|
|
4393
|
-
|
|
4394
|
-
if (
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
for (const child of node.attrs.nodes) {
|
|
4398
|
-
const r = findNode(child, pred);
|
|
4399
|
-
if (r) return r;
|
|
4400
|
-
}
|
|
4401
|
-
return;
|
|
4402
|
-
case "optional": return findNode(node.attrs.node, pred);
|
|
4403
|
-
case "repeat": return findNode(node.attrs.node, pred);
|
|
4404
|
-
case "alternative":
|
|
4405
|
-
for (const alt of node.attrs.alts) {
|
|
4406
|
-
const r = findNode(alt, pred);
|
|
4407
|
-
if (r) return r;
|
|
4408
|
-
}
|
|
4409
|
-
return;
|
|
4410
|
-
default: return;
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
/** Locate the int/float node carrying a scalar field's numeric range. */
|
|
4414
|
-
function findRangeNode(node) {
|
|
4415
|
-
return findNode(node, (n) => n.kind === "int" || n.kind === "float");
|
|
4416
|
-
}
|
|
4417
|
-
/** Locate the repeat node carrying a list field's length bounds and item. */
|
|
4418
|
-
function findRepeatNode(node) {
|
|
4419
|
-
return findNode(node, (n) => n.kind === "repeat");
|
|
4299
|
+
function renderOutputTrait(f, isRoot) {
|
|
4300
|
+
const tail = f.doc ? [`desc=${pyStr(f.doc)}`] : [];
|
|
4301
|
+
if (f.shape.kind === "list") return call("traits.List", ["File()", ...tail]);
|
|
4302
|
+
if (isRoot) return call("Directory", tail);
|
|
4303
|
+
return call("File", tail);
|
|
4420
4304
|
}
|
|
4421
|
-
/**
|
|
4305
|
+
/**
|
|
4306
|
+
* Emit the nipype interface module for one tool: a typed InputSpec/OutputSpec and
|
|
4307
|
+
* a BaseInterface that delegates execution to the co-emitted styx Python wrapper.
|
|
4308
|
+
*/
|
|
4309
|
+
function emitNipypeInterface(ctx, spec, names) {
|
|
4310
|
+
const cb = new CodeBuilder(" ");
|
|
4311
|
+
cb.comment("This file was auto generated by Styx.", "# ");
|
|
4312
|
+
cb.comment("Do not edit this file directly.", "# ");
|
|
4313
|
+
cb.blank();
|
|
4314
|
+
cb.line("from nipype.interfaces.base import (");
|
|
4315
|
+
cb.indent(() => {
|
|
4316
|
+
cb.line("BaseInterface,");
|
|
4317
|
+
cb.line("BaseInterfaceInputSpec,");
|
|
4318
|
+
cb.line("Directory,");
|
|
4319
|
+
cb.line("File,");
|
|
4320
|
+
cb.line("TraitedSpec,");
|
|
4321
|
+
cb.line("isdefined,");
|
|
4322
|
+
cb.line("traits,");
|
|
4323
|
+
});
|
|
4324
|
+
cb.line(")");
|
|
4325
|
+
cb.blank();
|
|
4326
|
+
cb.line(`from .${names.styxStem} import ${spec.delegation.wrapperFn}, ${spec.delegation.outputsClass}`);
|
|
4327
|
+
cb.blank();
|
|
4328
|
+
cb.blank();
|
|
4329
|
+
cb.line(`class ${names.inputSpec}(BaseInterfaceInputSpec):`);
|
|
4330
|
+
cb.indent(() => {
|
|
4331
|
+
if (!spec.rootIsStruct || spec.params.length === 0) {
|
|
4332
|
+
cb.line("pass");
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
for (const p of spec.params) cb.line(`${p.hostName} = ${renderInputTrait(p)}`);
|
|
4336
|
+
});
|
|
4337
|
+
cb.blank();
|
|
4338
|
+
cb.blank();
|
|
4339
|
+
cb.line(`class ${names.outputSpec}(TraitedSpec):`);
|
|
4340
|
+
cb.indent(() => {
|
|
4341
|
+
if (spec.outputs.length === 0 && spec.streams.length === 0) {
|
|
4342
|
+
cb.line("pass");
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
spec.outputs.forEach((f, i) => cb.line(`${f.id} = ${renderOutputTrait(f, i === 0)}`));
|
|
4346
|
+
for (const s of spec.streams) {
|
|
4347
|
+
const tail = s.doc ? `, desc=${pyStr(s.doc)}` : "";
|
|
4348
|
+
cb.line(`${s.id} = traits.List(traits.Str()${tail})`);
|
|
4349
|
+
}
|
|
4350
|
+
});
|
|
4351
|
+
cb.blank();
|
|
4352
|
+
cb.blank();
|
|
4353
|
+
cb.line(`class ${names.cls}(BaseInterface):`);
|
|
4354
|
+
cb.indent(() => {
|
|
4355
|
+
emitDocstring(cb, [ctx.app?.doc?.title, ctx.app?.doc?.description].filter(Boolean).join("\n\n") || void 0);
|
|
4356
|
+
cb.line(`input_spec = ${names.inputSpec}`);
|
|
4357
|
+
cb.line(`output_spec = ${names.outputSpec}`);
|
|
4358
|
+
cb.blank();
|
|
4359
|
+
cb.line("def _run_interface(self, runtime):");
|
|
4360
|
+
cb.indent(() => {
|
|
4361
|
+
if (!spec.rootIsStruct) {
|
|
4362
|
+
cb.line("raise NotImplementedError(");
|
|
4363
|
+
cb.indent(() => cb.line(pyStr("styx nipype backend: tools with a non-struct root are not supported.")));
|
|
4364
|
+
cb.line(")");
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
cb.line("kwargs = {}");
|
|
4368
|
+
for (const p of spec.params) if (p.mandatory) cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`);
|
|
4369
|
+
else {
|
|
4370
|
+
cb.line(`if isdefined(self.inputs.${p.hostName}):`);
|
|
4371
|
+
cb.indent(() => cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`));
|
|
4372
|
+
}
|
|
4373
|
+
cb.line(`self._result: ${spec.delegation.outputsClass} = ${spec.delegation.wrapperFn}(**kwargs)`);
|
|
4374
|
+
cb.line("return runtime");
|
|
4375
|
+
});
|
|
4376
|
+
cb.blank();
|
|
4377
|
+
cb.line("def _list_outputs(self):");
|
|
4378
|
+
cb.indent(() => {
|
|
4379
|
+
if (!spec.rootIsStruct) {
|
|
4380
|
+
cb.line("return self._outputs().get()");
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
cb.line("result = self._result");
|
|
4384
|
+
cb.line("outputs = self._outputs().get()");
|
|
4385
|
+
for (const f of spec.outputs) if (f.shape.kind === "single" && f.shape.optional) {
|
|
4386
|
+
cb.line(`if result.${f.id} is not None:`);
|
|
4387
|
+
cb.indent(() => cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`));
|
|
4388
|
+
} else cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`);
|
|
4389
|
+
for (const s of spec.streams) cb.line(`outputs[${pyStr(s.id)}] = result.${s.id}`);
|
|
4390
|
+
cb.line("return outputs");
|
|
4391
|
+
});
|
|
4392
|
+
});
|
|
4393
|
+
return cb.toString();
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
//#endregion
|
|
4397
|
+
//#region src/backend/styxdefs-compat.ts
|
|
4398
|
+
/**
|
|
4399
|
+
* Runtime version floors baked into generated dependency metadata.
|
|
4400
|
+
*
|
|
4401
|
+
* styx2-generated code calls `mutable_copy` / `mutableCopy` (introduced in the
|
|
4402
|
+
* styxdefs 0.7.0 / styxdefs-js 0.2.0 release), so emitted packages genuinely
|
|
4403
|
+
* require that runtime floor. This is the single source of truth: bump here and
|
|
4404
|
+
* both the Python and TypeScript backends pick it up.
|
|
4405
|
+
*/
|
|
4406
|
+
const STYXDEFS_COMPAT = {
|
|
4407
|
+
python: ">=0.7.0,<0.8.0",
|
|
4408
|
+
npm: "^0.2.0"
|
|
4409
|
+
};
|
|
4410
|
+
/**
|
|
4411
|
+
* Extra Python runtime packages the root metapackage pulls in. `styxkit[all]`
|
|
4412
|
+
* provides the cross-backend runner-selection helpers (`use_docker`, `use_auto`,
|
|
4413
|
+
* ...) that the metapackage's `__init__` re-exports, and transitively installs
|
|
4414
|
+
* every container/graph runner backend. Left unpinned - styxkit's own styxdefs
|
|
4415
|
+
* floor constrains the stack.
|
|
4416
|
+
*/
|
|
4417
|
+
const PYTHON_RUNNER_DEPS = ["styxkit[all]"];
|
|
4418
|
+
|
|
4419
|
+
//#endregion
|
|
4420
|
+
//#region src/backend/python/packaging.ts
|
|
4421
|
+
const REQUIRES_PYTHON = ">=3.10";
|
|
4422
|
+
const BUILD_SYSTEM = `[build-system]
|
|
4423
|
+
requires = ["setuptools>=61"]
|
|
4424
|
+
build-backend = "setuptools.build_meta"`;
|
|
4425
|
+
/** Escape a value for embedding in a TOML basic string. */
|
|
4426
|
+
function tomlStr(s) {
|
|
4427
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
4428
|
+
}
|
|
4429
|
+
/** Suite directory / importable package name; matches the CLI's `pkgDir` fallback. */
|
|
4430
|
+
function pkgDir(pkg) {
|
|
4431
|
+
return pkg.name ?? "package";
|
|
4432
|
+
}
|
|
4433
|
+
/** Distribution (PyPI) name for a package: `<project>_<package>`, or just `<package>`. */
|
|
4434
|
+
function pyDistName(proj, pkg) {
|
|
4435
|
+
const name = pkgDir(pkg);
|
|
4436
|
+
return proj.name ? `${proj.name}_${name}` : name;
|
|
4437
|
+
}
|
|
4438
|
+
const SUMMARY_MAX_LEN = 512;
|
|
4439
|
+
/**
|
|
4440
|
+
* Clamp a description to a <=512-char one-line summary. Prefer cutting at the
|
|
4441
|
+
* last complete sentence that fits (clean, no dangling fragment); if there is no
|
|
4442
|
+
* sentence boundary early enough, cut at a word boundary and mark the elision.
|
|
4443
|
+
*/
|
|
4444
|
+
function clampSummary(s) {
|
|
4445
|
+
if (s.length <= SUMMARY_MAX_LEN) return s;
|
|
4446
|
+
const window = s.slice(0, SUMMARY_MAX_LEN);
|
|
4447
|
+
const lastSentence = window.lastIndexOf(". ");
|
|
4448
|
+
if (lastSentence >= 0) return s.slice(0, lastSentence + 1);
|
|
4449
|
+
const ellipsis = "...";
|
|
4450
|
+
const body = window.slice(0, SUMMARY_MAX_LEN - 3);
|
|
4451
|
+
const lastSpace = body.lastIndexOf(" ");
|
|
4452
|
+
return (lastSpace > 0 ? body.slice(0, lastSpace) : body).replace(/[.,;:\s]+$/, "") + ellipsis;
|
|
4453
|
+
}
|
|
4454
|
+
function description$1(doc, fallbackName) {
|
|
4455
|
+
return clampSummary(doc?.description ?? `Styx generated wrappers for ${doc?.title ?? fallbackName ?? "tools"}.`);
|
|
4456
|
+
}
|
|
4457
|
+
function authorsField(doc) {
|
|
4458
|
+
return `[${(doc?.authors?.length ? doc.authors : ["unknown"]).map((a) => `{ name = "${tomlStr(a)}" }`).join(", ")}]`;
|
|
4459
|
+
}
|
|
4460
|
+
function licenseField(proj) {
|
|
4461
|
+
return `{ text = "${tomlStr(proj.license?.description ?? "unknown")}" }`;
|
|
4462
|
+
}
|
|
4463
|
+
/**
|
|
4464
|
+
* Per-suite `pyproject.toml`. The flat layout (`python/<pkg>/bet.py`) makes the
|
|
4465
|
+
* directory itself the importable package, so setuptools' `package-dir` maps the
|
|
4466
|
+
* import name (`<pkg>`) onto the distribution's root directory. The styxdefs
|
|
4467
|
+
* floor is the only runtime dependency.
|
|
4468
|
+
*
|
|
4469
|
+
* Precondition: `pkg.name` must be a valid Python identifier - the flat layout's
|
|
4470
|
+
* relative imports (`from .bet import *`) already require this, and the CLI uses
|
|
4471
|
+
* it verbatim as the directory name, so this stays consistent with that.
|
|
4472
|
+
*/
|
|
4473
|
+
function generateSubPyproject(proj, pkg) {
|
|
4474
|
+
const importName = pkgDir(pkg);
|
|
4475
|
+
const cb = new CodeBuilder(" ");
|
|
4476
|
+
cb.line("[project]");
|
|
4477
|
+
cb.line(`name = "${tomlStr(pyDistName(proj, pkg))}"`);
|
|
4478
|
+
cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
|
|
4479
|
+
cb.line(`description = "${tomlStr(description$1(pkg.doc, pkg.name))}"`);
|
|
4480
|
+
cb.line(`readme = "README.md"`);
|
|
4481
|
+
cb.line(`license = ${licenseField(proj)}`);
|
|
4482
|
+
cb.line(`authors = ${authorsField(pkg.doc ?? proj.doc)}`);
|
|
4483
|
+
cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
|
|
4484
|
+
cb.line("dependencies = [");
|
|
4485
|
+
cb.line(` "styxdefs${STYXDEFS_COMPAT.python}",`);
|
|
4486
|
+
cb.line("]");
|
|
4487
|
+
cb.blank();
|
|
4488
|
+
cb.line("[tool.setuptools]");
|
|
4489
|
+
cb.line(`packages = ["${importName}"]`);
|
|
4490
|
+
cb.line(`package-dir = { "${importName}" = "." }`);
|
|
4491
|
+
cb.blank();
|
|
4492
|
+
cb.line("[tool.setuptools.package-data]");
|
|
4493
|
+
cb.line(`"${importName}" = ["py.typed"]`);
|
|
4494
|
+
cb.blank();
|
|
4495
|
+
cb.line(BUILD_SYSTEM);
|
|
4496
|
+
return cb.toString() + "\n";
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* The metapackage's `__init__.py`. A thin re-export of styxkit so the runner
|
|
4500
|
+
* configuration helpers (`use_docker`, `use_local`, `set_global_runner`, ...)
|
|
4501
|
+
* are reachable as `<project>.<name>` - the v1 ergonomic that the v2 split into
|
|
4502
|
+
* per-suite distributions otherwise dropped. The logic lives in styxkit (pulled
|
|
4503
|
+
* in via `styxkit[all]`), so this stays a wildcard re-export and never re-emits
|
|
4504
|
+
* the runner-config code itself.
|
|
4505
|
+
*/
|
|
4506
|
+
function generateRootInitPy() {
|
|
4507
|
+
return "# This file was auto generated by Styx.\n# Do not edit this file directly.\n\n# Re-export styxkit's runner-configuration helpers (use_docker, use_local,\n# use_auto, set_global_runner, get_global_runner, ...) so they are available\n# directly on this package, e.g. `import niwrap; niwrap.use_docker()`.\nfrom styxkit import * # noqa: F401,F403\n";
|
|
4508
|
+
}
|
|
4509
|
+
/**
|
|
4510
|
+
* Root `pyproject.toml`: a metapackage depending on `styxkit[all]` (the runner
|
|
4511
|
+
* stack it re-exports) plus each per-suite distribution. `packages` lists only
|
|
4512
|
+
* the metapackage's own module so setuptools ships the styxkit re-export without
|
|
4513
|
+
* sweeping the sibling suite directories into this distribution.
|
|
4514
|
+
*/
|
|
4515
|
+
function generateRootPyproject(proj, distNames, moduleName) {
|
|
4516
|
+
const cb = new CodeBuilder(" ");
|
|
4517
|
+
cb.line("[project]");
|
|
4518
|
+
cb.line(`name = "${tomlStr(proj.name ?? "project")}"`);
|
|
4519
|
+
cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
|
|
4520
|
+
cb.line(`description = "${tomlStr(description$1(proj.doc, proj.name))}"`);
|
|
4521
|
+
cb.line(`readme = "README.md"`);
|
|
4522
|
+
cb.line(`license = ${licenseField(proj)}`);
|
|
4523
|
+
cb.line(`authors = ${authorsField(proj.doc)}`);
|
|
4524
|
+
cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
|
|
4525
|
+
cb.line("dependencies = [");
|
|
4526
|
+
for (const dep of PYTHON_RUNNER_DEPS) cb.line(` "${dep}",`);
|
|
4527
|
+
for (const dist of distNames) cb.line(` "${tomlStr(dist)}",`);
|
|
4528
|
+
cb.line("]");
|
|
4529
|
+
cb.blank();
|
|
4530
|
+
cb.line("[tool.setuptools]");
|
|
4531
|
+
cb.line(`packages = ["${moduleName}"]`);
|
|
4532
|
+
cb.blank();
|
|
4533
|
+
cb.line("[tool.setuptools.package-data]");
|
|
4534
|
+
cb.line(`"${moduleName}" = ["py.typed"]`);
|
|
4535
|
+
cb.blank();
|
|
4536
|
+
cb.line(BUILD_SYSTEM);
|
|
4537
|
+
return cb.toString() + "\n";
|
|
4538
|
+
}
|
|
4539
|
+
/** Per-suite README crediting the upstream tool authors. */
|
|
4540
|
+
function generateSubReadme(proj, pkg) {
|
|
4541
|
+
const projectTitle = proj.doc?.title ?? proj.name ?? "Styx";
|
|
4542
|
+
const packageTitle = pkg.doc?.title ?? pkg.name ?? "package";
|
|
4543
|
+
const url = pkg.doc?.urls?.[0];
|
|
4544
|
+
const titleMd = url ? `[${packageTitle}](${url})` : packageTitle;
|
|
4545
|
+
const credits = pkg.doc?.authors?.length ? pkg.doc.authors.join(", ") : pkg.doc?.urls?.join(", ") ?? "unknown";
|
|
4546
|
+
return `# ${projectTitle} wrappers for ${titleMd}${pkg.doc?.description ? `\n\n${pkg.doc.description}` : ""}\n\n${packageTitle} is made by ${credits}.\n\nThis package contains wrappers only and has no affiliation with the original authors.\n`;
|
|
4547
|
+
}
|
|
4548
|
+
/** Root README listing the bundled per-suite distributions. */
|
|
4549
|
+
function generateRootReadme(proj, distNames) {
|
|
4550
|
+
return `# ${proj.doc?.title ?? proj.name ?? "Styx"}\n${proj.doc?.description ? `\n${proj.doc.description}\n` : ""}\nAuto-generated Styx wrappers. This project bundles the following packages:\n\n${distNames.map((d) => `- ${d}`).join("\n")}\n`;
|
|
4551
|
+
}
|
|
4552
|
+
/** Local-install manifest: each suite directory first, then the root metapackage. */
|
|
4553
|
+
function generateRequirementsTxt(pkgDirs) {
|
|
4554
|
+
return [...pkgDirs.map((d) => `./${d}`), "./"].join("\n") + "\n";
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
//#endregion
|
|
4558
|
+
//#region src/backend/sig-entries.ts
|
|
4559
|
+
/**
|
|
4560
|
+
* Build per-field signature entries for the kwarg wrapper and params factory.
|
|
4561
|
+
* Skips `@type` (the factory injects it as a constant). Required-no-default
|
|
4562
|
+
* entries are placed before defaulted ones so the resulting signature is
|
|
4563
|
+
* syntactically valid in both Python and TS.
|
|
4564
|
+
*
|
|
4565
|
+
* `registerLocal` is called once per field with the wire key; it must return
|
|
4566
|
+
* a scrubbed, unique host identifier (typically by combining a language-aware
|
|
4567
|
+
* scrub function with `Scope.add()`). The caller's scope should already have
|
|
4568
|
+
* the function's other locals (`params`, `runner`, ...) reserved so this
|
|
4569
|
+
* registration cannot collide with them.
|
|
4570
|
+
*/
|
|
4571
|
+
function buildSigEntries(rootType, fieldInfo, registerLocal, opts) {
|
|
4572
|
+
const entries = [];
|
|
4573
|
+
for (const [fieldName, fieldType] of Object.entries(rootType.fields)) {
|
|
4574
|
+
if (fieldType.kind === "literal") continue;
|
|
4575
|
+
const fi = fieldInfo.get(fieldName);
|
|
4576
|
+
const isOptional = fieldType.kind === "optional";
|
|
4577
|
+
const inner = isOptional ? fieldType.inner : fieldType;
|
|
4578
|
+
let sigType = opts.renderType(inner);
|
|
4579
|
+
if (isOptional) sigType += opts.nullableSuffix;
|
|
4580
|
+
let sigDefault;
|
|
4581
|
+
const hasExplicitDefault = fi?.defaultValue !== void 0;
|
|
4582
|
+
if (hasExplicitDefault) sigDefault = opts.renderDefault(fi.defaultValue);
|
|
4583
|
+
else if (isOptional) sigDefault = opts.nullableDefault;
|
|
4584
|
+
entries.push({
|
|
4585
|
+
name: registerLocal(fieldName),
|
|
4586
|
+
wireKey: fieldName,
|
|
4587
|
+
sigType,
|
|
4588
|
+
sigDefault,
|
|
4589
|
+
isOptional,
|
|
4590
|
+
hasExplicitDefault,
|
|
4591
|
+
doc: fi?.doc
|
|
4592
|
+
});
|
|
4593
|
+
}
|
|
4594
|
+
const required = entries.filter((e) => e.sigDefault === void 0);
|
|
4595
|
+
const defaulted = entries.filter((e) => e.sigDefault !== void 0);
|
|
4596
|
+
return [...required, ...defaulted];
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
//#endregion
|
|
4600
|
+
//#region src/backend/validate-walk.ts
|
|
4601
|
+
/**
|
|
4602
|
+
* Enumerate a struct type's fields in declaration order, each paired with the
|
|
4603
|
+
* IR node its binding resolved from. `searchRoot` is the IR subtree known to
|
|
4604
|
+
* contain the struct (the root expr for the top-level struct, or a field /
|
|
4605
|
+
* union-arm node for nested ones).
|
|
4606
|
+
*/
|
|
4607
|
+
function structFields(ctx, structType, searchRoot) {
|
|
4608
|
+
const nodeByName = /* @__PURE__ */ new Map();
|
|
4609
|
+
if (searchRoot) {
|
|
4610
|
+
const structNode = findStructNode(searchRoot, ctx, structType);
|
|
4611
|
+
if (structNode) for (const child of structNode.attrs.nodes) {
|
|
4612
|
+
const match = resolveFieldBinding(child, ctx, structType);
|
|
4613
|
+
if (match) nodeByName.set(match.binding.name, match.binding.node);
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
const fieldInfo = collectFieldInfo(ctx, structType);
|
|
4617
|
+
return Object.entries(structType.fields).map(([name, type]) => ({
|
|
4618
|
+
name,
|
|
4619
|
+
type,
|
|
4620
|
+
node: nodeByName.get(name),
|
|
4621
|
+
hasDefault: fieldInfo.get(name)?.defaultValue !== void 0
|
|
4622
|
+
}));
|
|
4623
|
+
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Depth-first search for the first node satisfying `pred`, descending through
|
|
4626
|
+
* the transparent structural wrappers the solver may bury a binding under
|
|
4627
|
+
* (sequence/optional/repeat/alternative).
|
|
4628
|
+
*/
|
|
4629
|
+
function findNode(node, pred) {
|
|
4630
|
+
if (!node) return void 0;
|
|
4631
|
+
if (pred(node)) return node;
|
|
4632
|
+
switch (node.kind) {
|
|
4633
|
+
case "sequence":
|
|
4634
|
+
for (const child of node.attrs.nodes) {
|
|
4635
|
+
const r = findNode(child, pred);
|
|
4636
|
+
if (r) return r;
|
|
4637
|
+
}
|
|
4638
|
+
return;
|
|
4639
|
+
case "optional": return findNode(node.attrs.node, pred);
|
|
4640
|
+
case "repeat": return findNode(node.attrs.node, pred);
|
|
4641
|
+
case "alternative":
|
|
4642
|
+
for (const alt of node.attrs.alts) {
|
|
4643
|
+
const r = findNode(alt, pred);
|
|
4644
|
+
if (r) return r;
|
|
4645
|
+
}
|
|
4646
|
+
return;
|
|
4647
|
+
default: return;
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
/** Locate the int/float node carrying a scalar field's numeric range. */
|
|
4651
|
+
function findRangeNode(node) {
|
|
4652
|
+
return findNode(node, (n) => n.kind === "int" || n.kind === "float");
|
|
4653
|
+
}
|
|
4654
|
+
/** Locate the repeat node carrying a list field's length bounds and item. */
|
|
4655
|
+
function findRepeatNode(node) {
|
|
4656
|
+
return findNode(node, (n) => n.kind === "repeat");
|
|
4657
|
+
}
|
|
4658
|
+
/** Locate the alternative node backing a union field, to map arms to variants. */
|
|
4422
4659
|
function findAlternativeNode(node) {
|
|
4423
4660
|
return findNode(node, (n) => n.kind === "alternative");
|
|
4424
4661
|
}
|
|
@@ -5120,7 +5357,7 @@ function computePublicNames$1(appId) {
|
|
|
5120
5357
|
* (the `reg` registrations and the `sigScope` child), so passing the same scope
|
|
5121
5358
|
* the emitter continues with keeps later local registrations consistent.
|
|
5122
5359
|
*/
|
|
5123
|
-
function buildEmitModel
|
|
5360
|
+
function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
|
|
5124
5361
|
const appId = ctx.app?.id;
|
|
5125
5362
|
const pkg = ctx.package?.name;
|
|
5126
5363
|
const publicNames = computePublicNames$1(appId);
|
|
@@ -5163,7 +5400,7 @@ function buildEmitModel$1(ctx, scope = new Scope(PY_RESERVED)) {
|
|
|
5163
5400
|
function generatePython(ctx, packageScope) {
|
|
5164
5401
|
const cb = new CodeBuilder(" ");
|
|
5165
5402
|
const scope = packageScope ?? new Scope(PY_RESERVED);
|
|
5166
|
-
const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel
|
|
5403
|
+
const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
|
|
5167
5404
|
cb.comment("This file was auto generated by Styx.", "# ");
|
|
5168
5405
|
cb.comment("Do not edit this file directly.", "# ");
|
|
5169
5406
|
cb.blank();
|
|
@@ -5310,6 +5547,7 @@ var PythonBackend = class {
|
|
|
5310
5547
|
const files = /* @__PURE__ */ new Map();
|
|
5311
5548
|
const distNames = [];
|
|
5312
5549
|
const pkgDirs = [];
|
|
5550
|
+
const warnings = [];
|
|
5313
5551
|
for (const p of packages) {
|
|
5314
5552
|
const pkg = p.meta ?? {};
|
|
5315
5553
|
const dir = pkg.name ?? "package";
|
|
@@ -5318,13 +5556,21 @@ var PythonBackend = class {
|
|
|
5318
5556
|
files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg));
|
|
5319
5557
|
files.set(`${dir}/README.md`, generateSubReadme(proj, pkg));
|
|
5320
5558
|
}
|
|
5321
|
-
|
|
5559
|
+
let rootMod = pyScrubIdent(proj.name && proj.name.trim() ? proj.name : "project", PY_RESERVED);
|
|
5560
|
+
if (pkgDirs.includes(rootMod)) {
|
|
5561
|
+
const collided = rootMod;
|
|
5562
|
+
while (pkgDirs.includes(rootMod)) rootMod += "_";
|
|
5563
|
+
warnings.push({ message: `metapackage module "${collided}" collides with a suite directory; emitting as "${rootMod}" instead` });
|
|
5564
|
+
}
|
|
5565
|
+
files.set(`${rootMod}/__init__.py`, generateRootInitPy());
|
|
5566
|
+
files.set(`${rootMod}/py.typed`, "");
|
|
5567
|
+
files.set("pyproject.toml", generateRootPyproject(proj, distNames, rootMod));
|
|
5322
5568
|
files.set("README.md", generateRootReadme(proj, distNames));
|
|
5323
5569
|
files.set("requirements.txt", generateRequirementsTxt(pkgDirs));
|
|
5324
5570
|
return {
|
|
5325
5571
|
files,
|
|
5326
5572
|
errors: [],
|
|
5327
|
-
warnings
|
|
5573
|
+
warnings
|
|
5328
5574
|
};
|
|
5329
5575
|
}
|
|
5330
5576
|
};
|
|
@@ -5470,7 +5716,7 @@ const pyDialect = {
|
|
|
5470
5716
|
* @param opts - Import and package-root options.
|
|
5471
5717
|
*/
|
|
5472
5718
|
function renderPythonCall(ctx, config, opts = {}) {
|
|
5473
|
-
const model = buildEmitModel
|
|
5719
|
+
const model = buildEmitModel(ctx);
|
|
5474
5720
|
const pkg = ctx.package?.name;
|
|
5475
5721
|
const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
|
|
5476
5722
|
const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config) : `${callee}(${renderValue(config, model.rootType, "", pyDialect)})`;
|
|
@@ -5493,6 +5739,495 @@ function renderKwargCall(callee, sigEntries, rootType, config) {
|
|
|
5493
5739
|
return `${callee}(\n${lines.join("\n")}\n)`;
|
|
5494
5740
|
}
|
|
5495
5741
|
|
|
5742
|
+
//#endregion
|
|
5743
|
+
//#region src/backend/typed-spec.ts
|
|
5744
|
+
/** Project a tool's solved tree into the flat typed spec for delegation backends. */
|
|
5745
|
+
function buildTypedSpec(ctx) {
|
|
5746
|
+
const model = buildEmitModel(ctx);
|
|
5747
|
+
const delegation = {
|
|
5748
|
+
moduleName: appModuleName$1(ctx.app),
|
|
5749
|
+
wrapperFn: model.names.wrapper,
|
|
5750
|
+
outputsClass: model.names.outputs
|
|
5751
|
+
};
|
|
5752
|
+
const outputs = collectOutputFields(ctx, pyId);
|
|
5753
|
+
const streams = streamFields(ctx, pyId);
|
|
5754
|
+
if (!model.rootIsStruct || model.rootType.kind !== "struct") return {
|
|
5755
|
+
rootIsStruct: false,
|
|
5756
|
+
params: [],
|
|
5757
|
+
outputs,
|
|
5758
|
+
streams,
|
|
5759
|
+
delegation
|
|
5760
|
+
};
|
|
5761
|
+
const rootType = model.rootType;
|
|
5762
|
+
const fieldByName = new Map(structFields(ctx, rootType, ctx.expr).map((f) => [f.name, f]));
|
|
5763
|
+
const fieldInfo = collectFieldInfo(ctx, rootType);
|
|
5764
|
+
return {
|
|
5765
|
+
rootIsStruct: true,
|
|
5766
|
+
params: model.sigEntries.map((e) => {
|
|
5767
|
+
const fe = fieldByName.get(e.wireKey);
|
|
5768
|
+
const fi = fieldInfo.get(e.wireKey);
|
|
5769
|
+
return describeParam(e, fe?.type, fe?.node, fi?.defaultValue, fi?.doc);
|
|
5770
|
+
}),
|
|
5771
|
+
outputs,
|
|
5772
|
+
streams,
|
|
5773
|
+
delegation
|
|
5774
|
+
};
|
|
5775
|
+
}
|
|
5776
|
+
function describeParam(e, type, node, defaultValue, fallbackDoc) {
|
|
5777
|
+
const optional = e.isOptional;
|
|
5778
|
+
const hasDefault = e.hasExplicitDefault;
|
|
5779
|
+
const base = {
|
|
5780
|
+
hostName: e.name,
|
|
5781
|
+
wireKey: e.wireKey,
|
|
5782
|
+
optional,
|
|
5783
|
+
hasDefault,
|
|
5784
|
+
mandatory: !optional && !hasDefault,
|
|
5785
|
+
default: defaultValue,
|
|
5786
|
+
doc: e.doc ?? fallbackDoc
|
|
5787
|
+
};
|
|
5788
|
+
const inner = type && type.kind === "optional" ? type.inner : type;
|
|
5789
|
+
const choices = inner ? literalChoices(inner, node) : void 0;
|
|
5790
|
+
if (choices) return {
|
|
5791
|
+
...base,
|
|
5792
|
+
kind: "enum",
|
|
5793
|
+
choices
|
|
5794
|
+
};
|
|
5795
|
+
switch (inner?.kind) {
|
|
5796
|
+
case "scalar":
|
|
5797
|
+
if (inner.scalar === "path") return {
|
|
5798
|
+
...base,
|
|
5799
|
+
kind: "path",
|
|
5800
|
+
scalarKind: "path",
|
|
5801
|
+
mediaTypes: pathMediaTypes(node)
|
|
5802
|
+
};
|
|
5803
|
+
if (inner.scalar === "int" || inner.scalar === "float") return {
|
|
5804
|
+
...base,
|
|
5805
|
+
kind: inner.scalar,
|
|
5806
|
+
scalarKind: inner.scalar,
|
|
5807
|
+
range: numericRange(node)
|
|
5808
|
+
};
|
|
5809
|
+
return {
|
|
5810
|
+
...base,
|
|
5811
|
+
kind: "str",
|
|
5812
|
+
scalarKind: "str"
|
|
5813
|
+
};
|
|
5814
|
+
case "bool": return {
|
|
5815
|
+
...base,
|
|
5816
|
+
kind: "bool"
|
|
5817
|
+
};
|
|
5818
|
+
case "count": return {
|
|
5819
|
+
...base,
|
|
5820
|
+
kind: "count"
|
|
5821
|
+
};
|
|
5822
|
+
case "list": return {
|
|
5823
|
+
...base,
|
|
5824
|
+
kind: "list",
|
|
5825
|
+
listBounds: listBounds(node),
|
|
5826
|
+
itemType: describeItem(inner.item, node)
|
|
5827
|
+
};
|
|
5828
|
+
case "struct": return {
|
|
5829
|
+
...base,
|
|
5830
|
+
kind: "struct"
|
|
5831
|
+
};
|
|
5832
|
+
case "union": return {
|
|
5833
|
+
...base,
|
|
5834
|
+
kind: "union"
|
|
5835
|
+
};
|
|
5836
|
+
default: return {
|
|
5837
|
+
...base,
|
|
5838
|
+
kind: "str"
|
|
5839
|
+
};
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
function describeItem(item, node) {
|
|
5843
|
+
const inner = item.kind === "optional" ? item.inner : item;
|
|
5844
|
+
const choices = literalChoices(inner, node);
|
|
5845
|
+
if (choices) return {
|
|
5846
|
+
kind: "enum",
|
|
5847
|
+
choices
|
|
5848
|
+
};
|
|
5849
|
+
switch (inner.kind) {
|
|
5850
|
+
case "scalar":
|
|
5851
|
+
if (inner.scalar === "path") return {
|
|
5852
|
+
kind: "path",
|
|
5853
|
+
scalarKind: "path",
|
|
5854
|
+
mediaTypes: pathMediaTypes(node)
|
|
5855
|
+
};
|
|
5856
|
+
return {
|
|
5857
|
+
kind: inner.scalar,
|
|
5858
|
+
scalarKind: inner.scalar
|
|
5859
|
+
};
|
|
5860
|
+
case "bool": return { kind: "bool" };
|
|
5861
|
+
case "count": return { kind: "count" };
|
|
5862
|
+
case "struct": return { kind: "struct" };
|
|
5863
|
+
case "union": return { kind: "union" };
|
|
5864
|
+
default: return { kind: "str" };
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
/** Allowed values when `inner` is a literal or a union of literals (else undefined). */
|
|
5868
|
+
function literalChoices(inner, node) {
|
|
5869
|
+
if (inner.kind === "literal") return [inner.value];
|
|
5870
|
+
if (inner.kind === "union" && inner.variants.length > 0 && inner.variants.every((v) => v.type.kind === "literal")) return inner.variants.map((v) => v.type.value);
|
|
5871
|
+
const altNode = node ? findNode(node, (n) => n.kind === "alternative") : void 0;
|
|
5872
|
+
if (altNode && altNode.kind === "alternative") {
|
|
5873
|
+
const alts = altNode.attrs.alts;
|
|
5874
|
+
if (alts.length > 0 && alts.every((a) => a.kind === "literal")) return alts.map((a) => a.attrs.str);
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
function numericRange(node) {
|
|
5878
|
+
const r = findRangeNode(node);
|
|
5879
|
+
if (!r) return void 0;
|
|
5880
|
+
const { minValue, maxValue } = r.attrs;
|
|
5881
|
+
if (minValue === void 0 && maxValue === void 0) return void 0;
|
|
5882
|
+
return {
|
|
5883
|
+
min: minValue,
|
|
5884
|
+
max: maxValue
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
function listBounds(node) {
|
|
5888
|
+
const r = findRepeatNode(node);
|
|
5889
|
+
if (!r) return void 0;
|
|
5890
|
+
const { countMin, countMax } = r.attrs;
|
|
5891
|
+
if (countMin === void 0 && countMax === void 0) return void 0;
|
|
5892
|
+
return {
|
|
5893
|
+
min: countMin,
|
|
5894
|
+
max: countMax
|
|
5895
|
+
};
|
|
5896
|
+
}
|
|
5897
|
+
function pathMediaTypes(node) {
|
|
5898
|
+
const p = node ? findNode(node, (n) => n.kind === "path") : void 0;
|
|
5899
|
+
if (p && p.kind === "path") {
|
|
5900
|
+
const mt = p.attrs.mediaTypes;
|
|
5901
|
+
if (mt && mt.length > 0) return mt;
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
//#endregion
|
|
5906
|
+
//#region src/backend/nipype/nipype.ts
|
|
5907
|
+
/** Derive the per-tool nipype module/class names. */
|
|
5908
|
+
function nipypeNames(ctx) {
|
|
5909
|
+
const mod = appModuleName$1(ctx.app);
|
|
5910
|
+
const rawId = ctx.app?.id ?? "tool";
|
|
5911
|
+
const cls = pascalCase(/^[0-9]/.test(rawId) ? "v_" + rawId : rawId);
|
|
5912
|
+
return {
|
|
5913
|
+
styxStem: `_${mod}`,
|
|
5914
|
+
ifaceStem: mod,
|
|
5915
|
+
cls,
|
|
5916
|
+
inputSpec: `${cls}InputSpec`,
|
|
5917
|
+
outputSpec: `${cls}OutputSpec`
|
|
5918
|
+
};
|
|
5919
|
+
}
|
|
5920
|
+
/** Generate the nipype interface module source for one tool. */
|
|
5921
|
+
function generateNipype(ctx) {
|
|
5922
|
+
return emitNipypeInterface(ctx, buildTypedSpec(ctx), nipypeNames(ctx));
|
|
5923
|
+
}
|
|
5924
|
+
/**
|
|
5925
|
+
* Emits nipype `Interface` definitions whose typed InputSpec/OutputSpec carry
|
|
5926
|
+
* rich constraints (numeric ranges, list bounds, enum choices, file types) and
|
|
5927
|
+
* which delegate execution to the styx Python wrapper (Option B): no command-line
|
|
5928
|
+
* arg-building or output-path resolution is re-implemented here.
|
|
5929
|
+
*
|
|
5930
|
+
* Per tool, two co-located files are emitted so the output is a self-contained,
|
|
5931
|
+
* importable Python package: `_<tool>.py` (the styx Python module) and
|
|
5932
|
+
* `<tool>.py` (the interface, importing the wrapper via a relative import).
|
|
5933
|
+
*/
|
|
5934
|
+
var NipypeBackend = class {
|
|
5935
|
+
name = "nipype";
|
|
5936
|
+
target = "nipype";
|
|
5937
|
+
emitApp(ctx, _scope) {
|
|
5938
|
+
const names = nipypeNames(ctx);
|
|
5939
|
+
const spec = buildTypedSpec(ctx);
|
|
5940
|
+
const warnings = [];
|
|
5941
|
+
if (!spec.rootIsStruct) warnings.push({ message: `nipype: '${ctx.app?.id ?? "?"}' has a non-struct root; emitted interface has no typed inputs and raises on run.` });
|
|
5942
|
+
const styxCode = generatePython(ctx);
|
|
5943
|
+
const ifaceCode = emitNipypeInterface(ctx, spec, names);
|
|
5944
|
+
return {
|
|
5945
|
+
meta: ctx.app,
|
|
5946
|
+
files: new Map([[`${names.styxStem}.py`, styxCode], [`${names.ifaceStem}.py`, ifaceCode]]),
|
|
5947
|
+
errors: [],
|
|
5948
|
+
warnings
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
};
|
|
5952
|
+
|
|
5953
|
+
//#endregion
|
|
5954
|
+
//#region src/backend/pydra/emit.ts
|
|
5955
|
+
/** A scalar enum's Python type: int when every choice is numeric, else str. */
|
|
5956
|
+
function enumScalar(choices) {
|
|
5957
|
+
return (choices ?? []).every((c) => typeof c === "number") ? "int" : "str";
|
|
5958
|
+
}
|
|
5959
|
+
/** True when a parameter is a file path (a path scalar or a list of paths). */
|
|
5960
|
+
function isPathParam(p) {
|
|
5961
|
+
return p.kind === "path" || p.kind === "list" && p.itemType?.kind === "path";
|
|
5962
|
+
}
|
|
5963
|
+
function itemTypeStr(item, imp) {
|
|
5964
|
+
if (!item) {
|
|
5965
|
+
imp.typing = true;
|
|
5966
|
+
return "ty.Any";
|
|
5967
|
+
}
|
|
5968
|
+
switch (item.kind) {
|
|
5969
|
+
case "path":
|
|
5970
|
+
imp.file = true;
|
|
5971
|
+
return "File";
|
|
5972
|
+
case "int":
|
|
5973
|
+
case "count": return "int";
|
|
5974
|
+
case "float": return "float";
|
|
5975
|
+
case "str": return "str";
|
|
5976
|
+
case "bool": return "bool";
|
|
5977
|
+
case "enum": return enumScalar(item.choices);
|
|
5978
|
+
default:
|
|
5979
|
+
imp.typing = true;
|
|
5980
|
+
return "ty.Any";
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
/** Base (non-optional) Python type expression for a parameter. */
|
|
5984
|
+
function baseType(p, imp) {
|
|
5985
|
+
switch (p.kind) {
|
|
5986
|
+
case "path":
|
|
5987
|
+
imp.file = true;
|
|
5988
|
+
return "File";
|
|
5989
|
+
case "int":
|
|
5990
|
+
case "count": return "int";
|
|
5991
|
+
case "float": return "float";
|
|
5992
|
+
case "str": return "str";
|
|
5993
|
+
case "bool": return "bool";
|
|
5994
|
+
case "enum": return enumScalar(p.choices);
|
|
5995
|
+
case "list": return `list[${itemTypeStr(p.itemType, imp)}]`;
|
|
5996
|
+
case "struct":
|
|
5997
|
+
case "union":
|
|
5998
|
+
imp.typing = true;
|
|
5999
|
+
return "ty.Any";
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
/** Full input type, wrapping in `ty.Optional[...]` for an omittable-no-default field. */
|
|
6003
|
+
function inputType(p, imp) {
|
|
6004
|
+
const base = baseType(p, imp);
|
|
6005
|
+
if (p.optional && !p.hasDefault) {
|
|
6006
|
+
imp.typing = true;
|
|
6007
|
+
return `ty.Optional[${base}]`;
|
|
6008
|
+
}
|
|
6009
|
+
return base;
|
|
6010
|
+
}
|
|
6011
|
+
/** An attrs validator enforcing numeric range / list-length bounds, or undefined. */
|
|
6012
|
+
function validatorExpr(p, imp) {
|
|
6013
|
+
const checks = [];
|
|
6014
|
+
if (p.range) {
|
|
6015
|
+
if (p.range.min !== void 0) checks.push(`attrs.validators.ge(${renderPyLiteral(p.range.min)})`);
|
|
6016
|
+
if (p.range.max !== void 0) checks.push(`attrs.validators.le(${renderPyLiteral(p.range.max)})`);
|
|
6017
|
+
}
|
|
6018
|
+
if (p.listBounds) {
|
|
6019
|
+
if (p.listBounds.min !== void 0) checks.push(`attrs.validators.min_len(${p.listBounds.min})`);
|
|
6020
|
+
if (p.listBounds.max !== void 0) checks.push(`attrs.validators.max_len(${p.listBounds.max})`);
|
|
6021
|
+
}
|
|
6022
|
+
if (checks.length === 0) return void 0;
|
|
6023
|
+
imp.attrs = true;
|
|
6024
|
+
return `_styx_optional(${checks.length === 1 ? checks[0] : `attrs.validators.and_(${checks.join(", ")})`})`;
|
|
6025
|
+
}
|
|
6026
|
+
/** Help text: doc + degrade / media-type notes. */
|
|
6027
|
+
function helpText(p) {
|
|
6028
|
+
const parts = [];
|
|
6029
|
+
if (p.doc) parts.push(p.doc);
|
|
6030
|
+
if (p.kind === "struct" || p.kind === "union") parts.push("(nested configuration; pass a dict)");
|
|
6031
|
+
if (p.mediaTypes && p.mediaTypes.length > 0) parts.push(`(media types: ${p.mediaTypes.join(", ")})`);
|
|
6032
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
6033
|
+
}
|
|
6034
|
+
/** Render a `python.arg(...)` field for one parameter. */
|
|
6035
|
+
function renderInputArg(p, imp) {
|
|
6036
|
+
const args = [`type=${inputType(p, imp)}`];
|
|
6037
|
+
if (p.mandatory) {} else if (p.optional && !p.hasDefault) args.push("default=None");
|
|
6038
|
+
else args.push(`default=${renderPyLiteral(p.default)}`);
|
|
6039
|
+
if (p.kind === "enum") args.push(`allowed_values=[${(p.choices ?? []).map((c) => renderPyLiteral(c)).join(", ")}]`);
|
|
6040
|
+
const v = validatorExpr(p, imp);
|
|
6041
|
+
if (v) args.push(`validator=${v}`);
|
|
6042
|
+
const h = helpText(p);
|
|
6043
|
+
if (h) args.push(`help=${pyStr(h)}`);
|
|
6044
|
+
return `python.arg(${args.join(", ")})`;
|
|
6045
|
+
}
|
|
6046
|
+
/** Python type for an output field. `isRoot` is the synthetic output directory. */
|
|
6047
|
+
function outputType(f, isRoot, imp) {
|
|
6048
|
+
if (f.shape.kind === "list") {
|
|
6049
|
+
imp.file = true;
|
|
6050
|
+
return "list[File]";
|
|
6051
|
+
}
|
|
6052
|
+
if (isRoot) {
|
|
6053
|
+
imp.directory = true;
|
|
6054
|
+
return "Directory";
|
|
6055
|
+
}
|
|
6056
|
+
imp.file = true;
|
|
6057
|
+
if (f.shape.optional) {
|
|
6058
|
+
imp.typing = true;
|
|
6059
|
+
return "ty.Optional[File]";
|
|
6060
|
+
}
|
|
6061
|
+
return "File";
|
|
6062
|
+
}
|
|
6063
|
+
function renderOutputArg(f, isRoot, imp) {
|
|
6064
|
+
const args = [`type=${outputType(f, isRoot, imp)}`];
|
|
6065
|
+
if (f.doc) args.push(`help=${pyStr(f.doc)}`);
|
|
6066
|
+
return `python.out(${args.join(", ")})`;
|
|
6067
|
+
}
|
|
6068
|
+
/**
|
|
6069
|
+
* Emit the pydra task module for one tool: a `@python.define` task whose typed
|
|
6070
|
+
* inputs/outputs carry rich constraints and whose body delegates execution to the
|
|
6071
|
+
* co-emitted styx Python wrapper (Option B). Targets the post-rewrite
|
|
6072
|
+
* `pydra.compose` API (pydra >= 1.0a, Python >= 3.11).
|
|
6073
|
+
*/
|
|
6074
|
+
function emitPydraTask(ctx, spec, names) {
|
|
6075
|
+
const imp = {
|
|
6076
|
+
typing: false,
|
|
6077
|
+
attrs: false,
|
|
6078
|
+
file: false,
|
|
6079
|
+
directory: false
|
|
6080
|
+
};
|
|
6081
|
+
const inputEntries = spec.params.map((p) => ({
|
|
6082
|
+
key: p.hostName,
|
|
6083
|
+
expr: renderInputArg(p, imp)
|
|
6084
|
+
}));
|
|
6085
|
+
const outputEntries = spec.outputs.map((f, i) => ({
|
|
6086
|
+
key: f.id,
|
|
6087
|
+
expr: renderOutputArg(f, i === 0, imp)
|
|
6088
|
+
}));
|
|
6089
|
+
for (const s of spec.streams) outputEntries.push({
|
|
6090
|
+
key: s.id,
|
|
6091
|
+
expr: `python.out(type=list[str]${s.doc ? `, help=${pyStr(s.doc)}` : ""})`
|
|
6092
|
+
});
|
|
6093
|
+
const needsPath = spec.params.some(isPathParam);
|
|
6094
|
+
const cb = new CodeBuilder(" ");
|
|
6095
|
+
cb.comment("This file was auto generated by Styx.", "# ");
|
|
6096
|
+
cb.comment("Do not edit this file directly.", "# ");
|
|
6097
|
+
cb.comment("Targets the pydra.compose API (pydra >= 1.0a, Python >= 3.11).", "# ");
|
|
6098
|
+
cb.blank();
|
|
6099
|
+
if (needsPath) cb.line("import os");
|
|
6100
|
+
if (imp.typing) cb.line("import typing as ty");
|
|
6101
|
+
if (imp.attrs) cb.line("import attrs.validators");
|
|
6102
|
+
cb.line("from pydra.compose import python");
|
|
6103
|
+
const ff = [];
|
|
6104
|
+
if (imp.directory) ff.push("Directory");
|
|
6105
|
+
if (imp.file) ff.push("File");
|
|
6106
|
+
if (ff.length > 0) cb.line(`from fileformats.generic import ${ff.join(", ")}`);
|
|
6107
|
+
cb.blank();
|
|
6108
|
+
cb.line(`from .${names.styxStem} import ${spec.delegation.wrapperFn}`);
|
|
6109
|
+
cb.blank();
|
|
6110
|
+
cb.blank();
|
|
6111
|
+
if (imp.attrs) {
|
|
6112
|
+
cb.line("def _styx_optional(validator):");
|
|
6113
|
+
cb.indent(() => {
|
|
6114
|
+
cb.line("\"\"\"Apply an attrs validator only to a set (non-NOTHING), non-None value.\"\"\"");
|
|
6115
|
+
cb.line("def _check(instance, attribute, value):");
|
|
6116
|
+
cb.indent(() => {
|
|
6117
|
+
cb.line("if value is attrs.NOTHING or value is None:");
|
|
6118
|
+
cb.indent(() => cb.line("return"));
|
|
6119
|
+
cb.line("validator(instance, attribute, value)");
|
|
6120
|
+
});
|
|
6121
|
+
cb.line("return _check");
|
|
6122
|
+
});
|
|
6123
|
+
cb.blank();
|
|
6124
|
+
cb.blank();
|
|
6125
|
+
}
|
|
6126
|
+
if (needsPath) {
|
|
6127
|
+
cb.line("def _styx_path(value):");
|
|
6128
|
+
cb.indent(() => {
|
|
6129
|
+
cb.line("\"\"\"Convert fileformats / Path inputs into a styxdefs-accepted path.\"\"\"");
|
|
6130
|
+
cb.line("if value is None:");
|
|
6131
|
+
cb.indent(() => cb.line("return None"));
|
|
6132
|
+
cb.line("if isinstance(value, (list, tuple)):");
|
|
6133
|
+
cb.indent(() => cb.line("return [os.fspath(v) for v in value]"));
|
|
6134
|
+
cb.line("return os.fspath(value)");
|
|
6135
|
+
});
|
|
6136
|
+
cb.blank();
|
|
6137
|
+
cb.blank();
|
|
6138
|
+
}
|
|
6139
|
+
cb.line("@python.define(");
|
|
6140
|
+
cb.indent(() => {
|
|
6141
|
+
if (inputEntries.length === 0) cb.line("inputs={},");
|
|
6142
|
+
else {
|
|
6143
|
+
cb.line("inputs={");
|
|
6144
|
+
cb.indent(() => {
|
|
6145
|
+
for (const e of inputEntries) cb.line(`${pyStr(e.key)}: ${e.expr},`);
|
|
6146
|
+
});
|
|
6147
|
+
cb.line("},");
|
|
6148
|
+
}
|
|
6149
|
+
cb.line("outputs={");
|
|
6150
|
+
cb.indent(() => {
|
|
6151
|
+
for (const e of outputEntries) cb.line(`${pyStr(e.key)}: ${e.expr},`);
|
|
6152
|
+
});
|
|
6153
|
+
cb.line("},");
|
|
6154
|
+
});
|
|
6155
|
+
cb.line(")");
|
|
6156
|
+
const paramList = spec.params.map((p) => p.hostName);
|
|
6157
|
+
cb.line(`def ${names.cls}(${paramList.join(", ")}):`);
|
|
6158
|
+
cb.indent(() => {
|
|
6159
|
+
emitDocstring(cb, [ctx.app?.doc?.title, ctx.app?.doc?.description].filter(Boolean).join("\n\n") || void 0);
|
|
6160
|
+
if (!spec.rootIsStruct) {
|
|
6161
|
+
cb.line("raise NotImplementedError(");
|
|
6162
|
+
cb.indent(() => cb.line(pyStr("styx pydra backend: tools with a non-struct root are not supported.")));
|
|
6163
|
+
cb.line(")");
|
|
6164
|
+
return;
|
|
6165
|
+
}
|
|
6166
|
+
if (spec.params.length === 0) cb.line(`result = ${spec.delegation.wrapperFn}()`);
|
|
6167
|
+
else {
|
|
6168
|
+
cb.line(`result = ${spec.delegation.wrapperFn}(`);
|
|
6169
|
+
cb.indent(() => {
|
|
6170
|
+
for (const p of spec.params) {
|
|
6171
|
+
const expr = isPathParam(p) ? `_styx_path(${p.hostName})` : p.hostName;
|
|
6172
|
+
cb.line(`${p.hostName}=${expr},`);
|
|
6173
|
+
}
|
|
6174
|
+
});
|
|
6175
|
+
cb.line(")");
|
|
6176
|
+
}
|
|
6177
|
+
const returns = [...spec.outputs.map((f) => `result.${f.id}`), ...spec.streams.map((s) => `result.${s.id}`)];
|
|
6178
|
+
if (returns.length === 1) cb.line(`return ${returns[0]}`);
|
|
6179
|
+
else cb.line(`return ${returns.join(", ")}`);
|
|
6180
|
+
});
|
|
6181
|
+
return cb.toString();
|
|
6182
|
+
}
|
|
6183
|
+
|
|
6184
|
+
//#endregion
|
|
6185
|
+
//#region src/backend/pydra/pydra.ts
|
|
6186
|
+
/** Derive the per-tool pydra module/class names. */
|
|
6187
|
+
function pydraNames(ctx) {
|
|
6188
|
+
const mod = appModuleName$1(ctx.app);
|
|
6189
|
+
const rawId = ctx.app?.id ?? "tool";
|
|
6190
|
+
const safeId = /^[0-9]/.test(rawId) ? "v_" + rawId : rawId;
|
|
6191
|
+
return {
|
|
6192
|
+
styxStem: `_${mod}`,
|
|
6193
|
+
ifaceStem: mod,
|
|
6194
|
+
cls: pascalCase(safeId)
|
|
6195
|
+
};
|
|
6196
|
+
}
|
|
6197
|
+
/** Generate the pydra task module source for one tool. */
|
|
6198
|
+
function generatePydra(ctx) {
|
|
6199
|
+
return emitPydraTask(ctx, buildTypedSpec(ctx), pydraNames(ctx));
|
|
6200
|
+
}
|
|
6201
|
+
/**
|
|
6202
|
+
* Emits pydra tasks (`@python.define`, the post-rewrite pydra.compose API) whose
|
|
6203
|
+
* typed inputs/outputs carry rich constraints (numeric ranges and list bounds via
|
|
6204
|
+
* attrs validators, enum choices, file types, defaults) and which delegate
|
|
6205
|
+
* execution to the styx Python wrapper (Option B): no command-line arg-building
|
|
6206
|
+
* or output-path resolution is re-implemented here.
|
|
6207
|
+
*
|
|
6208
|
+
* Per tool, two co-located files are emitted so the output is a self-contained,
|
|
6209
|
+
* importable Python package: `_<tool>.py` (the styx Python module) and
|
|
6210
|
+
* `<tool>.py` (the task, importing the wrapper via a relative import).
|
|
6211
|
+
*/
|
|
6212
|
+
var PydraBackend = class {
|
|
6213
|
+
name = "pydra";
|
|
6214
|
+
target = "pydra";
|
|
6215
|
+
emitApp(ctx, _scope) {
|
|
6216
|
+
const names = pydraNames(ctx);
|
|
6217
|
+
const spec = buildTypedSpec(ctx);
|
|
6218
|
+
const warnings = [];
|
|
6219
|
+
if (!spec.rootIsStruct) warnings.push({ message: `pydra: '${ctx.app?.id ?? "?"}' has a non-struct root; emitted task has no typed inputs and raises on run.` });
|
|
6220
|
+
const styxCode = generatePython(ctx);
|
|
6221
|
+
const taskCode = emitPydraTask(ctx, spec, names);
|
|
6222
|
+
return {
|
|
6223
|
+
meta: ctx.app,
|
|
6224
|
+
files: new Map([[`${names.styxStem}.py`, styxCode], [`${names.ifaceStem}.py`, taskCode]]),
|
|
6225
|
+
errors: [],
|
|
6226
|
+
warnings
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
};
|
|
6230
|
+
|
|
5496
6231
|
//#endregion
|
|
5497
6232
|
//#region src/backend/schema/jsonschema.ts
|
|
5498
6233
|
var SchemaBuilder = class {
|
|
@@ -5538,7 +6273,7 @@ var SchemaBuilder = class {
|
|
|
5538
6273
|
case "literal": return { const: type.value };
|
|
5539
6274
|
case "optional": return this.fromType(type.inner, node?.kind === "optional" ? node.attrs.node : void 0);
|
|
5540
6275
|
case "list": {
|
|
5541
|
-
const repeat = node
|
|
6276
|
+
const repeat = findRepeatNode(node);
|
|
5542
6277
|
const schema = {
|
|
5543
6278
|
type: "array",
|
|
5544
6279
|
items: this.fromType(type.item, repeat?.attrs.node)
|
|
@@ -5548,7 +6283,7 @@ var SchemaBuilder = class {
|
|
|
5548
6283
|
return schema;
|
|
5549
6284
|
}
|
|
5550
6285
|
case "struct": return this.structSchema(type, node);
|
|
5551
|
-
case "union": return this.unionSchema(type);
|
|
6286
|
+
case "union": return this.unionSchema(type, node);
|
|
5552
6287
|
}
|
|
5553
6288
|
}
|
|
5554
6289
|
findTerminal(node) {
|
|
@@ -5609,9 +6344,20 @@ var SchemaBuilder = class {
|
|
|
5609
6344
|
if (required.length > 0) schema.required = required;
|
|
5610
6345
|
return schema;
|
|
5611
6346
|
}
|
|
5612
|
-
unionSchema(type) {
|
|
6347
|
+
unionSchema(type, node) {
|
|
5613
6348
|
if (type.variants.every((v) => v.type.kind === "literal")) return { enum: type.variants.map((v) => v.type.kind === "literal" ? v.type.value : "") };
|
|
5614
|
-
|
|
6349
|
+
const altNode = findAlternativeNode(node);
|
|
6350
|
+
return { oneOf: type.variants.map((v, i) => {
|
|
6351
|
+
const schema = this.fromType(v.type, altNode?.attrs.alts[i]);
|
|
6352
|
+
if (v.type.kind === "struct" && "@type" in v.type.fields && schema.properties && !("@type" in schema.properties)) {
|
|
6353
|
+
schema.properties = {
|
|
6354
|
+
"@type": this.fromType(v.type.fields["@type"]),
|
|
6355
|
+
...schema.properties
|
|
6356
|
+
};
|
|
6357
|
+
schema.required = ["@type", ...schema.required ?? []];
|
|
6358
|
+
}
|
|
6359
|
+
return schema;
|
|
6360
|
+
}) };
|
|
5615
6361
|
}
|
|
5616
6362
|
};
|
|
5617
6363
|
function generateSchema(ctx) {
|
|
@@ -6863,7 +7609,7 @@ function computePublicNames(appId) {
|
|
|
6863
7609
|
* (the `reg` registrations and the `sigScope` child), so passing the same scope
|
|
6864
7610
|
* the emitter continues with keeps later local registrations consistent.
|
|
6865
7611
|
*/
|
|
6866
|
-
function buildEmitModel(ctx, scope = new Scope(TS_RESERVED)) {
|
|
7612
|
+
function buildEmitModel$1(ctx, scope = new Scope(TS_RESERVED)) {
|
|
6867
7613
|
const appId = ctx.app?.id;
|
|
6868
7614
|
const pkg = ctx.package?.name ?? "unknown";
|
|
6869
7615
|
const publicNames = computePublicNames(appId);
|
|
@@ -6906,7 +7652,7 @@ function buildEmitModel(ctx, scope = new Scope(TS_RESERVED)) {
|
|
|
6906
7652
|
function generateTypeScript(ctx, packageScope) {
|
|
6907
7653
|
const cb = new CodeBuilder(" ");
|
|
6908
7654
|
const scope = packageScope ?? new Scope(TS_RESERVED);
|
|
6909
|
-
const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
|
|
7655
|
+
const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel$1(ctx, scope);
|
|
6910
7656
|
cb.comment("This file was auto generated by Styx.");
|
|
6911
7657
|
cb.comment("Do not edit this file directly.");
|
|
6912
7658
|
cb.blank();
|
|
@@ -7089,7 +7835,7 @@ const tsDialect = {
|
|
|
7089
7835
|
* @param opts - Import and package-root options.
|
|
7090
7836
|
*/
|
|
7091
7837
|
function renderTypeScriptCall(ctx, config, opts = {}) {
|
|
7092
|
-
const model = buildEmitModel(ctx);
|
|
7838
|
+
const model = buildEmitModel$1(ctx);
|
|
7093
7839
|
const pkg = ctx.package?.name;
|
|
7094
7840
|
const fnName = model.rootIsStruct ? model.names.execute : model.names.wrapper;
|
|
7095
7841
|
const call = `${pkg ? `${pkg}.${fnName}` : fnName}(${model.rootIsStruct && model.rootType.kind === "struct" ? renderStructLiteral(config, model.rootType, "", tsDialect, model.rootTypeTag) : renderValue(config, model.rootType, "", tsDialect)})`;
|
|
@@ -8254,8 +9000,10 @@ function compile(source, filenameOrOptions) {
|
|
|
8254
9000
|
exports.BoutiquesBackend = BoutiquesBackend;
|
|
8255
9001
|
exports.CodeBuilder = CodeBuilder;
|
|
8256
9002
|
exports.JsonSchemaBackend = JsonSchemaBackend;
|
|
9003
|
+
exports.NipypeBackend = NipypeBackend;
|
|
8257
9004
|
exports.PYTHON_RUNNER_DEPS = PYTHON_RUNNER_DEPS;
|
|
8258
9005
|
exports.PassStatus = PassStatus;
|
|
9006
|
+
exports.PydraBackend = PydraBackend;
|
|
8259
9007
|
exports.PythonBackend = PythonBackend;
|
|
8260
9008
|
exports.STYXDEFS_COMPAT = STYXDEFS_COMPAT;
|
|
8261
9009
|
exports.Scope = Scope;
|
|
@@ -8263,7 +9011,9 @@ exports.TypeScriptBackend = TypeScriptBackend;
|
|
|
8263
9011
|
exports.alt = alt;
|
|
8264
9012
|
exports.appEntrypoint = appEntrypoint;
|
|
8265
9013
|
exports.atomKey = atomKey;
|
|
9014
|
+
exports.buildEmitModel = buildEmitModel;
|
|
8266
9015
|
exports.buildSigEntries = buildSigEntries;
|
|
9016
|
+
exports.buildTypedSpec = buildTypedSpec;
|
|
8267
9017
|
exports.camelCase = camelCase;
|
|
8268
9018
|
exports.canonicalize = canonicalize;
|
|
8269
9019
|
exports.collectFieldInfo = collectFieldInfo;
|
|
@@ -8286,7 +9036,9 @@ exports.float = float;
|
|
|
8286
9036
|
exports.format = format;
|
|
8287
9037
|
exports.formatSolveResult = formatSolveResult;
|
|
8288
9038
|
exports.generateBoutiques = generateBoutiques;
|
|
9039
|
+
exports.generateNipype = generateNipype;
|
|
8289
9040
|
exports.generateOutputsSchema = generateOutputsSchema;
|
|
9041
|
+
exports.generatePydra = generatePydra;
|
|
8290
9042
|
exports.generatePython = generatePython;
|
|
8291
9043
|
exports.generateSchema = generateSchema;
|
|
8292
9044
|
exports.generateTypeScript = generateTypeScript;
|
|
@@ -8296,6 +9048,7 @@ exports.isIterated = isIterated;
|
|
|
8296
9048
|
exports.isStructural = isStructural;
|
|
8297
9049
|
exports.isTerminal = isTerminal;
|
|
8298
9050
|
exports.lit = lit;
|
|
9051
|
+
exports.nipypeNames = nipypeNames;
|
|
8299
9052
|
exports.nodeRef = nodeRef;
|
|
8300
9053
|
exports.opt = opt;
|
|
8301
9054
|
exports.outputGate = outputGate;
|
|
@@ -8303,6 +9056,7 @@ exports.pascalCase = pascalCase;
|
|
|
8303
9056
|
exports.path = path;
|
|
8304
9057
|
exports.planOutput = planOutput;
|
|
8305
9058
|
exports.planScope = planScope;
|
|
9059
|
+
exports.pydraNames = pydraNames;
|
|
8306
9060
|
exports.removeEmpty = removeEmpty;
|
|
8307
9061
|
exports.renderPythonCall = renderPythonCall;
|
|
8308
9062
|
exports.renderStructLiteral = renderStructLiteral;
|