@platforma-sdk/workflow-tengo 6.3.3 → 6.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.
Files changed (53) hide show
  1. package/dist/tengo/lib/exec.constants.lib.tengo +18 -1
  2. package/dist/tengo/lib/exec.formula.lib.tengo +327 -0
  3. package/dist/tengo/lib/exec.lib.tengo +248 -9
  4. package/dist/tengo/lib/feats.lib.tengo +6 -1
  5. package/dist/tengo/lib/file.internal.lib.tengo +35 -3
  6. package/dist/tengo/lib/file.lib.tengo +4 -0
  7. package/dist/tengo/lib/pt.lib.tengo +25 -0
  8. package/dist/tengo/tpl/annotations.compute.plj.gz +0 -0
  9. package/dist/tengo/tpl/anonymize.deanonymize.plj.gz +0 -0
  10. package/dist/tengo/tpl/anonymize.impl.plj.gz +0 -0
  11. package/dist/tengo/tpl/check_network.create_workdir_from_storage.plj.gz +0 -0
  12. package/dist/tengo/tpl/check_network.download_blob.plj.gz +0 -0
  13. package/dist/tengo/tpl/check_network.run_hello_world.plj.gz +0 -0
  14. package/dist/tengo/tpl/check_network.run_hello_world_py.plj.gz +0 -0
  15. package/dist/tengo/tpl/check_network.upload_blob.plj.gz +0 -0
  16. package/dist/tengo/tpl/check_network.upload_template.plj.gz +0 -0
  17. package/dist/tengo/tpl/check_network.workdir_type_name_extractor.plj.gz +0 -0
  18. package/dist/tengo/tpl/exec.conda.create-env.plj.gz +0 -0
  19. package/dist/tengo/tpl/exec.delayed-compute-allocation.impl.plj.gz +0 -0
  20. package/dist/tengo/tpl/exec.exec-impl.plj.gz +0 -0
  21. package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
  22. package/dist/tengo/tpl/exec.load-software.impl.plj.gz +0 -0
  23. package/dist/tengo/tpl/exec.monetization_info.plj.gz +0 -0
  24. package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
  25. package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
  26. package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
  27. package/dist/tengo/tpl/exec.python.venv-bin-path.plj.gz +0 -0
  28. package/dist/tengo/tpl/ll.get-field.plj.gz +0 -0
  29. package/dist/tengo/tpl/pframes.build-table.plj.gz +0 -0
  30. package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
  31. package/dist/tengo/tpl/pframes.export-parquet-pcolumn-chunk.plj.gz +0 -0
  32. package/dist/tengo/tpl/pframes.export-pframe-for-ui.plj.gz +0 -0
  33. package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
  34. package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
  35. package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
  36. package/dist/tengo/tpl/pframes.merge-resource-maps.plj.gz +0 -0
  37. package/dist/tengo/tpl/pframes.parse-to-json.plj.gz +0 -0
  38. package/dist/tengo/tpl/pframes.process-pcolumn-batch-split.plj.gz +0 -0
  39. package/dist/tengo/tpl/pframes.process-pcolumn-batch.plj.gz +0 -0
  40. package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
  41. package/dist/tengo/tpl/pframes.remove-nulls.plj.gz +0 -0
  42. package/dist/tengo/tpl/pframes.slice-data.plj.gz +0 -0
  43. package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
  44. package/dist/tengo/tpl/pframes.xsv-import-if-not-null.plj.gz +0 -0
  45. package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
  46. package/dist/tengo/tpl/pt.import-dir.plj.gz +0 -0
  47. package/dist/tengo/tpl/pt.workflow-run.plj.gz +0 -0
  48. package/dist/tengo/tpl/waiter.waiter.plj.gz +0 -0
  49. package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
  50. package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
  51. package/dist/tengo/tpl/workflow.query-anchored.plj.gz +0 -0
  52. package/dist/tengo/tpl/workflow.query-context-filter.plj.gz +0 -0
  53. package/package.json +2 -2
@@ -16,6 +16,19 @@ UI_TASKS_QUEUE := "ui-tasks"
16
16
  RUNNER_BATCH := "batch" // batch system execution (k8s jobs, etc.)
17
17
  RUNNER_EXECUTOR := "executor" // local binary execution
18
18
 
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+ META_INPUT_MEM_FORMULA := "__memFormula__"
29
+ META_INPUT_CPU_FORMULA := "__cpuFormula__"
30
+ REGULAR_INPUT_LINE_COUNT := "__metric_lineCount__"
31
+
19
32
  export ll.toStrict({
20
33
  ARG_TYPE_STRING: ARG_TYPE_STRING,
21
34
  ARG_TYPE_EXPRESSION: ARG_TYPE_EXPRESSION,
@@ -30,5 +43,9 @@ export ll.toStrict({
30
43
  HEAVY_QUEUE: HEAVY_QUEUE,
31
44
 
32
45
  RUNNER_BATCH: RUNNER_BATCH,
33
- RUNNER_EXECUTOR: RUNNER_EXECUTOR
46
+ RUNNER_EXECUTOR: RUNNER_EXECUTOR,
47
+
48
+ META_INPUT_MEM_FORMULA: META_INPUT_MEM_FORMULA,
49
+ META_INPUT_CPU_FORMULA: META_INPUT_CPU_FORMULA,
50
+ REGULAR_INPUT_LINE_COUNT: REGULAR_INPUT_LINE_COUNT
34
51
  })
@@ -0,0 +1,327 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+ ll := import("@platforma-sdk/workflow-tengo:ll")
33
+ text := import("text")
34
+ slices := import("@platforma-sdk/workflow-tengo:slices")
35
+ maps := import("@platforma-sdk/workflow-tengo:maps")
36
+ sets := import("@platforma-sdk/workflow-tengo:sets")
37
+ units := import("@platforma-sdk/workflow-tengo:units")
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+ _KNOWN_OPS := sets.fromSlice([
55
+ "const", "metric", "metricSum",
56
+ "add", "sub", "mul", "div", "divCeil",
57
+ "max", "min", "clamp",
58
+ "gt", "gte", "lt", "lte", "and", "or", "not",
59
+ "if"
60
+ ])
61
+
62
+
63
+
64
+ _BOOL_OPS := sets.fromSlice(["gt", "gte", "lt", "lte", "and", "or", "not"])
65
+
66
+ _wrap := func(x) {
67
+ if is_map(x) && !is_undefined(x.op) {
68
+ ll.assert(sets.hasElement(_KNOWN_OPS, x.op), "exec.formula: not a valid expression node (unknown op %v)", x.op)
69
+ return x
70
+ }
71
+ ll.assert(is_int(x), "exec.formula: expected an expression node or an integer, got %v", x)
72
+ return { op: "const", value: x }
73
+ }
74
+
75
+ _bin := func(op, a, b) { return { op: op, a: _wrap(a), b: _wrap(b) } }
76
+
77
+
78
+ _metric := func(metricName, args) {
79
+ node := { op: "metric", metric: metricName }
80
+ for arg in args {
81
+ if is_string(arg) {
82
+ ll.assert(is_undefined(node.tag), "exec.formula.%s: tag specified more than once", metricName)
83
+ node.tag = arg
84
+ } else if is_map(arg) && !is_undefined(arg.default) {
85
+ ll.assert(is_undefined(node.default), "exec.formula.%s: default specified more than once", metricName)
86
+ ll.assert(is_int(arg.default), "exec.formula.%s: default must be an integer", metricName)
87
+ node.default = arg.default
88
+ } else {
89
+ ll.panic("exec.formula.%s: unexpected argument %v", metricName, arg)
90
+ }
91
+ }
92
+ return node
93
+ }
94
+
95
+ evaluate := undefined
96
+ evaluate = func(node, resolver) {
97
+ op := node.op
98
+ if op == "const" { return node.value }
99
+ if op == "metricSum" {
100
+ fn := resolver[node.metric]
101
+ ll.assert(!is_undefined(fn), "exec.formula.evaluate: no resolver for metric %q", node.metric)
102
+ return fn(node.files)
103
+ }
104
+
105
+ if op == "add" { return evaluate(node.a, resolver) + evaluate(node.b, resolver) }
106
+ if op == "sub" { return evaluate(node.a, resolver) - evaluate(node.b, resolver) }
107
+ if op == "mul" { return evaluate(node.a, resolver) * evaluate(node.b, resolver) }
108
+ if op == "div" {
109
+ b := evaluate(node.b, resolver)
110
+ ll.assert(b != 0, "exec.formula.evaluate: division by zero")
111
+ return evaluate(node.a, resolver) / b
112
+ }
113
+ if op == "divCeil" {
114
+ b := evaluate(node.b, resolver)
115
+ ll.assert(b != 0, "exec.formula.evaluate: division by zero")
116
+ a := evaluate(node.a, resolver)
117
+
118
+ return (a + b - 1) / b
119
+ }
120
+
121
+ if op == "max" { a := evaluate(node.a, resolver); b := evaluate(node.b, resolver); return a > b ? a : b }
122
+ if op == "min" { a := evaluate(node.a, resolver); b := evaluate(node.b, resolver); return a < b ? a : b }
123
+ if op == "clamp" {
124
+ v := evaluate(node.value, resolver)
125
+ lo := evaluate(node.lo, resolver)
126
+ hi := evaluate(node.hi, resolver)
127
+ ll.assert(lo <= hi, "exec.formula.clamp: lo (%v) must be <= hi (%v) — inverted bounds collapse every input to hi", lo, hi)
128
+ if v < lo { v = lo }
129
+ if v > hi { v = hi }
130
+ return v
131
+ }
132
+
133
+ if op == "gt" { return evaluate(node.a, resolver) > evaluate(node.b, resolver) }
134
+ if op == "gte" { return evaluate(node.a, resolver) >= evaluate(node.b, resolver) }
135
+ if op == "lt" { return evaluate(node.a, resolver) < evaluate(node.b, resolver) }
136
+ if op == "lte" { return evaluate(node.a, resolver) <= evaluate(node.b, resolver) }
137
+ if op == "and" { return evaluate(node.a, resolver) && evaluate(node.b, resolver) }
138
+ if op == "or" { return evaluate(node.a, resolver) || evaluate(node.b, resolver) }
139
+ if op == "not" { return !evaluate(node.a, resolver) }
140
+
141
+ if op == "if" { return evaluate(node.cond, resolver) ? evaluate(node.then, resolver) : evaluate(node["else"], resolver) }
142
+
143
+ ll.panic("exec.formula.evaluate: unknown op %q", op)
144
+ }
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+ evaluateResource := func(node, resolver, label) {
156
+ ll.assert(!sets.hasElement(_BOOL_OPS, node.op),
157
+ "exec resource formula (%s) must compute a number, but its top-level op is %q (a comparison/logic op). Use comparisons and logic only inside f.if(...).",
158
+ label, node.op)
159
+ v := evaluate(node, resolver)
160
+ ll.assert(is_int(v) && v > 0,
161
+ "exec resource formula (%s) must evaluate to a positive integer, got %v. If a metric can be zero or a subtraction can underflow, floor it with f.max(...) or f.clamp(...).",
162
+ label, v)
163
+ return v
164
+ }
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+ _LINECOUNT_OK := sets.fromSlice(["csv", "tsv", "txt", "fastq", "fq", "fasta", "fa", "tab"])
173
+ _COMPRESSION_EXT := sets.fromSlice(["gz", "bz2", "zst"])
174
+
175
+ _assertMetricCompatible := func(metric, fileName) {
176
+ if metric == "lineCount" {
177
+
178
+
179
+
180
+
181
+
182
+ parts := text.split(fileName, ".")
183
+ if len(parts) >= 2 && sets.hasElement(_COMPRESSION_EXT, text.to_lower(parts[len(parts)-1])) {
184
+ parts = parts[0:len(parts)-1]
185
+ }
186
+ ext := len(parts) >= 2 ? parts[len(parts)-1] : ""
187
+ ll.assert(sets.hasElement(_LINECOUNT_OK, text.to_lower(ext)),
188
+ "exec.formula: lineCount is not supported for file %q (extension %q is not a line-oriented text format)", fileName, ext)
189
+ }
190
+
191
+ }
192
+
193
+ resolveAst := undefined
194
+ resolveAst = func(node, tagMap, allFiles) {
195
+ op := node.op
196
+ if op == "metric" {
197
+ files := is_undefined(node.tag) ? allFiles : (is_undefined(tagMap[node.tag]) ? [] : tagMap[node.tag])
198
+ if len(files) == 0 {
199
+ if is_undefined(node.tag) {
200
+ ll.assert(!is_undefined(node.default),
201
+ "exec.formula: f.%s() references all input files but the exec has none, and no { default: ... } was given", node.metric)
202
+ } else {
203
+ ll.assert(!is_undefined(node.default),
204
+ "exec.formula: tag %q has no files and no { default: ... } was given", node.tag)
205
+ }
206
+ return { op: "const", value: node.default }
207
+ }
208
+ for fn in files { _assertMetricCompatible(node.metric, fn) }
209
+ files = slices.quickSort(files)
210
+
211
+
212
+
213
+
214
+ return { op: "metricSum", metric: node.metric, files: files }
215
+ }
216
+
217
+
218
+ return maps.mapValues(node, func(v) {
219
+ return is_map(v) && !is_undefined(v.op) ? resolveAst(v, tagMap, allFiles) : v
220
+ })
221
+ }
222
+
223
+ referencedMetricFiles := func(resolvedNode) {
224
+ acc := {}
225
+ walk := undefined
226
+ walk = func(n) {
227
+ if n.op == "metricSum" {
228
+ if n.metric != "size" {
229
+ existing := is_undefined(acc[n.metric]) ? [] : acc[n.metric]
230
+ for fn in n.files { existing = append(existing, fn) }
231
+
232
+
233
+
234
+ acc[n.metric] = slices.unique(existing)
235
+ }
236
+ return
237
+ }
238
+ for k, v in n {
239
+ if is_map(v) && !is_undefined(v.op) { walk(v) }
240
+ }
241
+ }
242
+ walk(resolvedNode)
243
+ return acc
244
+ }
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+ applyFormulaFallback := func(dims) {
259
+ res := { ram: dims.ram.static, cpu: dims.cpu.static, err: undefined }
260
+ if !is_undefined(dims.ram.ast) && is_undefined(res.ram) {
261
+ if is_undefined(dims.ram.fallback) {
262
+ res.err = "exec.builder: memFormula needs a static fallback on backends that can't evaluate resource formulas at run time. Provide { fallback: ... } on memFormula, set a static .mem(), or upgrade the backend."
263
+ return res
264
+ }
265
+ res.ram = dims.ram.fallback
266
+ }
267
+ if !is_undefined(dims.cpu.ast) && is_undefined(res.cpu) {
268
+ if is_undefined(dims.cpu.fallback) {
269
+ res.err = "exec.builder: cpuFormula needs a static fallback on backends that can't evaluate resource formulas at run time. Provide { fallback: ... } on cpuFormula, set a static .cpu(), or upgrade the backend."
270
+ return res
271
+ }
272
+ res.cpu = dims.cpu.fallback
273
+ }
274
+ return res
275
+ }
276
+
277
+ export {
278
+
279
+
280
+
281
+
282
+
283
+ size: func(...args) { return _metric("size", args) },
284
+ lineCount: func(...args) { return _metric("lineCount", args) },
285
+
286
+ gib: func(n) { ll.assert(is_int(n), "exec.formula.gib: n must be an integer"); return { op: "const", value: n * units.GiB } },
287
+ mib: func(n) { ll.assert(is_int(n), "exec.formula.mib: n must be an integer"); return { op: "const", value: n * units.MiB } },
288
+ kib: func(n) { ll.assert(is_int(n), "exec.formula.kib: n must be an integer"); return { op: "const", value: n * units.KiB } },
289
+ gb: func(n) { ll.assert(is_int(n), "exec.formula.gb: n must be an integer"); return { op: "const", value: n * units.GB } },
290
+ mb: func(n) { ll.assert(is_int(n), "exec.formula.mb: n must be an integer"); return { op: "const", value: n * units.MB } },
291
+ kb: func(n) { ll.assert(is_int(n), "exec.formula.kb: n must be an integer"); return { op: "const", value: n * units.KB } },
292
+
293
+ add: func(a, b) { return _bin("add", a, b) },
294
+ sub: func(a, b) { return _bin("sub", a, b) },
295
+ mul: func(a, b) { return _bin("mul", a, b) },
296
+ div: func(a, b) { return _bin("div", a, b) },
297
+
298
+
299
+
300
+ divCeil: func(a, b) { return _bin("divCeil", a, b) },
301
+
302
+ max: func(a, b) { return _bin("max", a, b) },
303
+ min: func(a, b) { return _bin("min", a, b) },
304
+ clamp: func(value, lo, hi) { return { op: "clamp", value: _wrap(value), lo: _wrap(lo), hi: _wrap(hi) } },
305
+
306
+ gt: func(a, b) { return _bin("gt", a, b) },
307
+ gte: func(a, b) { return _bin("gte", a, b) },
308
+ lt: func(a, b) { return _bin("lt", a, b) },
309
+ lte: func(a, b) { return _bin("lte", a, b) },
310
+ and: func(a, b) { return _bin("and", a, b) },
311
+ or: func(a, b) { return _bin("or", a, b) },
312
+ not: func(a) { return { op: "not", a: _wrap(a) } },
313
+
314
+ "if": func(cond, then, els) { return { op: "if", cond: _wrap(cond), then: _wrap(then), "else": _wrap(els) } },
315
+
316
+ evaluateResource: evaluateResource,
317
+ resolveAst: resolveAst,
318
+ referencedMetricFiles: referencedMetricFiles,
319
+ applyFormulaFallback: applyFormulaFallback,
320
+
321
+
322
+
323
+
324
+
325
+
326
+ _internal: { evaluate: evaluate }
327
+ }
@@ -19,6 +19,7 @@ constants := import("@platforma-sdk/workflow-tengo:constants")
19
19
  canonical := import("@platforma-sdk/workflow-tengo:canonical")
20
20
  monetization := import("@platforma-sdk/workflow-tengo:exec.monetization_internal")
21
21
  execConstants := import("@platforma-sdk/workflow-tengo:exec.constants")
22
+ execFormula := import("@platforma-sdk/workflow-tengo:exec.formula")
22
23
  units := import("@platforma-sdk/workflow-tengo:units")
23
24
  feats := import("@platforma-sdk/workflow-tengo:feats")
24
25
 
@@ -27,6 +28,34 @@ feats := import("@platforma-sdk/workflow-tengo:feats")
27
28
 
28
29
 
29
30
 
31
+ lineCounterSoft := assets.importSoftware("@platforma-open/milaboratories.software-small-binaries:line-counter")
32
+
33
+
34
+
35
+
36
+
37
+
38
+ _lineCountResource := undefined
39
+
40
+
41
+
42
+
43
+
44
+
45
+ _assertValidCpuRequest := func(amount, ctx) {
46
+ validation.assertType(amount, "number", ctx + ": amount of cores must be a number")
47
+ ll.assert(amount > 0, ctx + ": amount of cores should be greater than 0")
48
+ }
49
+ _assertValidMemRequest := func(amount, ctx) {
50
+ validation.assertType(amount, ["or", "number", "string"], ctx + ": RAM amount should be a number or string")
51
+ ll.assert(is_string(amount) || amount >= 16 * units.MiB, ctx + ": amount in bytes should be at least 16MiB")
52
+ }
53
+
54
+
55
+
56
+
57
+
58
+
30
59
  builder := func() {
31
60
  self := undefined
32
61
 
@@ -55,6 +84,13 @@ builder := func() {
55
84
  ramRequest := undefined
56
85
  gpuMemory := undefined
57
86
 
87
+
88
+ memFormulaAst := undefined
89
+ memFormulaFallback := undefined
90
+ cpuFormulaAst := undefined
91
+ cpuFormulaFallback := undefined
92
+ fileTags := {} // fileName -> tag (for f.size("tag") / f.lineCount("tag") aggregation)
93
+
58
94
  commandName := ""
59
95
  cache := 0
60
96
  inputCache := 0
@@ -405,8 +441,7 @@ builder := func() {
405
441
 
406
442
 
407
443
  cpu: func(amount) {
408
- validation.assertType(amount, "number", "exec.builder.cpu: amount of cores must be a number")
409
- ll.assert(amount > 0, "exec.builder.cpu: amount of cores should be greater than 0")
444
+ _assertValidCpuRequest(amount, "exec.builder.cpu")
410
445
  cpuRequest = amount
411
446
 
412
447
  return self
@@ -424,8 +459,7 @@ builder := func() {
424
459
 
425
460
 
426
461
  mem: func(amount) {
427
- validation.assertType(amount, ["or", "number", "string"], "exec.builder.mem: RAM amount should be a number or string")
428
- ll.assert(is_string(amount) || amount >= 16 * units.MiB, "exec.builder.mem: amount in bytes should be at least 16MiB")
462
+ _assertValidMemRequest(amount, "exec.builder.mem")
429
463
  ramRequest = amount
430
464
 
431
465
  return self
@@ -448,6 +482,89 @@ builder := func() {
448
482
 
449
483
 
450
484
 
485
+
486
+
487
+
488
+
489
+
490
+
491
+
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+ memFormula: func(ast, ...opts) {
507
+ ll.assert(is_map(ast) && !is_undefined(ast.op), "exec.builder.memFormula: expected a formula node from exec.formula")
508
+ memFormulaAst = ast
509
+ if len(opts) > 0 && ll.isMap(opts[0]) {
510
+ for k, _ in opts[0] {
511
+ ll.assert(k == "fallback", "exec.builder.memFormula: unknown option %q (only { fallback } is supported)", k)
512
+ }
513
+ if !is_undefined(opts[0].fallback) {
514
+ _assertValidMemRequest(opts[0].fallback, "exec.builder.memFormula fallback")
515
+ memFormulaFallback = opts[0].fallback
516
+ }
517
+ }
518
+ return self
519
+ },
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+
533
+
534
+
535
+
536
+
537
+
538
+
539
+
540
+
541
+
542
+
543
+
544
+
545
+
546
+
547
+ cpuFormula: func(ast, ...opts) {
548
+ ll.assert(is_map(ast) && !is_undefined(ast.op), "exec.builder.cpuFormula: expected a formula node from exec.formula")
549
+ cpuFormulaAst = ast
550
+ if len(opts) > 0 && ll.isMap(opts[0]) {
551
+ for k, _ in opts[0] {
552
+ ll.assert(k == "fallback", "exec.builder.cpuFormula: unknown option %q (only { fallback } is supported)", k)
553
+ }
554
+ if !is_undefined(opts[0].fallback) {
555
+ _assertValidCpuRequest(opts[0].fallback, "exec.builder.cpuFormula fallback")
556
+ cpuFormulaFallback = opts[0].fallback
557
+ }
558
+ }
559
+ return self
560
+ },
561
+
562
+
563
+
564
+
565
+
566
+
567
+
451
568
 
452
569
 
453
570
  gpuMemory: func(...args) {
@@ -786,8 +903,14 @@ builder := func() {
786
903
 
787
904
  fileName = path.canonize(fileName)
788
905
  filesToAdd[fileName] = file
789
- if len(opts) > 0 && ll.isMap(opts[0]) && opts[0].writable == true {
790
- sets.add(writableFiles, fileName)
906
+ if len(opts) > 0 && ll.isMap(opts[0]) {
907
+ if opts[0].writable == true {
908
+ sets.add(writableFiles, fileName)
909
+ }
910
+ if !is_undefined(opts[0].tag) {
911
+ ll.assert(is_string(opts[0].tag), "exec.builder().addFile: tag must be a string")
912
+ fileTags[fileName] = opts[0].tag
913
+ }
791
914
  }
792
915
  monetization.addOptionalArg(mnz, fileName, opts...)
793
916
  return self
@@ -1103,14 +1226,88 @@ builder := func() {
1103
1226
  if (is_int(ramRequest)) {
1104
1227
  ramRequest = string(ramRequest) + "b"
1105
1228
  }
1229
+
1230
+
1231
+
1232
+
1233
+
1234
+
1235
+
1236
+
1237
+
1238
+ hasFormula := !is_undefined(memFormulaAst) || !is_undefined(cpuFormulaAst)
1239
+ resolvedMem := undefined
1240
+ resolvedCpu := undefined
1241
+ lineCountMapResource := undefined
1242
+
1243
+ if hasFormula {
1244
+
1245
+
1246
+
1247
+
1248
+
1249
+
1250
+
1251
+
1252
+ canEvaluateFormulas := feats.hasBlobSize
1253
+ if !canEvaluateFormulas {
1254
+
1255
+
1256
+
1257
+
1258
+
1259
+ fb := execFormula.applyFormulaFallback({
1260
+ ram: { ast: memFormulaAst, fallback: memFormulaFallback, static: ramRequest },
1261
+ cpu: { ast: cpuFormulaAst, fallback: cpuFormulaFallback, static: cpuRequest }
1262
+ })
1263
+ ll.assert(is_undefined(fb.err), fb.err)
1264
+ ramRequest = fb.ram
1265
+ cpuRequest = fb.cpu
1266
+ if is_int(ramRequest) { ramRequest = string(ramRequest) + "b" }
1267
+ } else {
1268
+ tagMap := {}
1269
+ for fileName, tag in fileTags {
1270
+ existing := is_undefined(tagMap[tag]) ? [] : tagMap[tag]
1271
+ tagMap[tag] = append(existing, fileName)
1272
+ }
1273
+ allFiles := maps.getKeys(filesToAdd)
1274
+
1275
+ neededLineCount := {}
1276
+ if !is_undefined(memFormulaAst) {
1277
+ resolvedMem = execFormula.resolveAst(memFormulaAst, tagMap, allFiles)
1278
+ refs := execFormula.referencedMetricFiles(resolvedMem)
1279
+ if !is_undefined(refs.lineCount) { sets.add(neededLineCount, refs.lineCount...) }
1280
+ }
1281
+ if !is_undefined(cpuFormulaAst) {
1282
+ resolvedCpu = execFormula.resolveAst(cpuFormulaAst, tagMap, allFiles)
1283
+ refs := execFormula.referencedMetricFiles(resolvedCpu)
1284
+ if !is_undefined(refs.lineCount) { sets.add(neededLineCount, refs.lineCount...) }
1285
+ }
1286
+
1287
+ if len(neededLineCount) > 0 {
1288
+ perFile := {}
1289
+ for fn, _ in neededLineCount {
1290
+ perFile[fn] = _lineCountResource(fn, filesToAdd[fn])
1291
+ }
1292
+ lineCountMapResource = smart.createMapResource(perFile)
1293
+ }
1294
+ }
1295
+ }
1296
+
1106
1297
  quotaData := {
1107
1298
  queue: queue,
1108
1299
  cpu: cpuRequest,
1109
1300
  ram: ramRequest
1110
1301
  }
1111
1302
  if !is_undefined(gpuMemory) {
1112
- ll.assert(!is_undefined(cpuRequest), "exec.builder: cpu() is required when gpuMemory() is set. GPU nodes vary in CPU capacity and cost; please provide an explicit CPU request.")
1113
- ll.assert(!is_undefined(ramRequest), "exec.builder: mem() is required when gpuMemory() is set. GPU nodes vary in RAM capacity and cost; please provide an explicit RAM request.")
1303
+
1304
+
1305
+
1306
+
1307
+
1308
+
1309
+ ll.assert(!is_undefined(cpuRequest) || !is_undefined(cpuFormulaAst), "exec.builder: cpu() or cpuFormula() is required when gpuMemory() is set. GPU nodes vary in CPU capacity and cost; please provide an explicit CPU request.")
1310
+ ll.assert(!is_undefined(ramRequest) || !is_undefined(memFormulaAst), "exec.builder: mem() or memFormula() is required when gpuMemory() is set. GPU nodes vary in RAM capacity and cost; please provide an explicit RAM request.")
1114
1311
  quotaData.gpuMemory = gpuMemory
1115
1312
  }
1116
1313
  metaExecInputs := {
@@ -1120,6 +1317,26 @@ builder := func() {
1120
1317
 
1121
1318
 
1122
1319
 
1320
+
1321
+
1322
+
1323
+
1324
+
1325
+
1326
+
1327
+
1328
+
1329
+
1330
+
1331
+
1332
+
1333
+ if !is_undefined(resolvedMem) { metaExecInputs[execConstants.META_INPUT_MEM_FORMULA] = smart.createJsonResource(resolvedMem) }
1334
+ if !is_undefined(resolvedCpu) { metaExecInputs[execConstants.META_INPUT_CPU_FORMULA] = smart.createJsonResource(resolvedCpu) }
1335
+ if !is_undefined(lineCountMapResource) { pureExecInputs[execConstants.REGULAR_INPUT_LINE_COUNT] = lineCountMapResource }
1336
+
1337
+
1338
+
1339
+
1123
1340
  if monetization.shouldRun(mnz) {
1124
1341
  pureExecInputs.runOptions.monetization = monetization.toJson(mnz)
1125
1342
  }
@@ -1283,6 +1500,27 @@ builder := func() {
1283
1500
  return self
1284
1501
  }
1285
1502
 
1503
+
1504
+
1505
+
1506
+
1507
+
1508
+ _lineCountResource = func(fileName, fileRef) {
1509
+ countOut := "linecount.txt"
1510
+ res := builder().
1511
+ software(lineCounterSoft).
1512
+ addFile(fileName, fileRef).
1513
+ arg("--input").arg(fileName).
1514
+ arg("--output").arg(countOut).
1515
+ saveFileContent(countOut).
1516
+ inLightQueue().
1517
+ cacheInputsDays(30).
1518
+ mem("256MiB").
1519
+ cpu(1).
1520
+ run()
1521
+ return res.getFileContent(countOut)
1522
+ }
1523
+
1286
1524
  export ll.toStrict({
1287
1525
  builder: builder,
1288
1526
 
@@ -1290,5 +1528,6 @@ export ll.toStrict({
1290
1528
 
1291
1529
 
1292
1530
 
1293
- hasGpu: feats.hasGpu
1531
+ hasGpu: feats.hasGpu,
1532
+ formula: execFormula
1294
1533
  })
@@ -77,5 +77,10 @@ export ll.toStrict({
77
77
  secretEnvSupport: _isEnabled("secretEnvSupport"),
78
78
 
79
79
 
80
- contextDomainQuery: _isEnabled("contextDomainQuery")
80
+ contextDomainQuery: _isEnabled("contextDomainQuery"),
81
+
82
+
83
+
84
+
85
+ multiStepProgress: _isEnabled("multiStepProgress")
81
86
  })
@@ -12,6 +12,7 @@
12
12
  ll := import("@platforma-sdk/workflow-tengo:ll")
13
13
  smart := import("@platforma-sdk/workflow-tengo:smart")
14
14
  validation := import("@platforma-sdk/workflow-tengo:validation")
15
+ feats := import("@platforma-sdk/workflow-tengo:feats")
15
16
 
16
17
  json := import("json")
17
18
 
@@ -19,6 +20,22 @@ json := import("json")
19
20
 
20
21
 
21
22
 
23
+
24
+
25
+
26
+
27
+
28
+ _progressField := func(importer, handleField) {
29
+ if feats.multiStepProgress {
30
+ return importer.getField("progress")
31
+ }
32
+ return smart.mapBuilder().addRef("01", handleField).build()
33
+ }
34
+
35
+
36
+
37
+
38
+
22
39
  RTYPE_BLOB_IMPORT_EXTERNAL := { Name: "BlobImportExternal", Version: "1" }
23
40
  RTYPE_BLOB_IMPORT_INTERNAL := { Name: "BlobImportInternal", Version: "1" }
24
41
  RTYPE_BLOB_UPLOAD := { Name: "BlobUpload", Version: "1" }
@@ -30,6 +47,9 @@ RTYPE_BLOB_DOWNLOAD := { Name: "BlobDownload", Version: "2" }
30
47
 
31
48
 
32
49
 
50
+
51
+
52
+
33
53
  createBlobImportExternal := func(settings) {
34
54
  validation.assertType(settings, {
35
55
  "dataURL": "string",
@@ -38,10 +58,12 @@ createBlobImportExternal := func(settings) {
38
58
  })
39
59
 
40
60
  importer := smart.structBuilder(RTYPE_BLOB_IMPORT_EXTERNAL, json.encode(settings)).lockAndBuild()
61
+ handleField := importer.getField("handle")
41
62
 
42
63
  return ll.toStrict({
43
64
  file: importer.getField("blob"),
44
- handle: importer.getField("handle")
65
+ handle: handleField,
66
+ progress: _progressField(importer, handleField)
45
67
  })
46
68
  }
47
69
 
@@ -51,6 +73,9 @@ createBlobImportExternal := func(settings) {
51
73
 
52
74
 
53
75
 
76
+
77
+
78
+
54
79
  createBlobImportInternal := func(settings) {
55
80
  validation.assertType(settings, {
56
81
  "path": "string",
@@ -60,10 +85,12 @@ createBlobImportInternal := func(settings) {
60
85
  })
61
86
 
62
87
  importer := smart.structBuilder(RTYPE_BLOB_IMPORT_INTERNAL, json.encode(settings)).lockAndBuild()
88
+ handleField := importer.getField("handle")
63
89
 
64
90
  return ll.toStrict({
65
91
  file: importer.getField("blob"),
66
- handle: importer.getField("handle")
92
+ handle: handleField,
93
+ progress: _progressField(importer, handleField)
67
94
  })
68
95
  }
69
96
 
@@ -73,6 +100,9 @@ createBlobImportInternal := func(settings) {
73
100
 
74
101
 
75
102
 
103
+
104
+
105
+
76
106
  createBlobUpload := func(settings) {
77
107
  validation.assertType(settings, {
78
108
  "modificationTime": "string",
@@ -82,10 +112,12 @@ createBlobUpload := func(settings) {
82
112
  })
83
113
 
84
114
  uploader := smart.structBuilder(RTYPE_BLOB_UPLOAD, json.encode(settings)).lockAndBuild()
115
+ handleField := uploader.getField("handle")
85
116
 
86
117
  return ll.toStrict({
87
118
  file: uploader.getField("blob"),
88
- handle: uploader.getField("handle")
119
+ handle: handleField,
120
+ progress: _progressField(uploader, handleField)
89
121
  })
90
122
  }
91
123
 
@@ -31,6 +31,10 @@ json := import("json")
31
31
 
32
32
 
33
33
 
34
+
35
+
36
+
37
+
34
38
  importFile := func(importFileHandle) {
35
39
  validation.assertType(
36
40
  importFileHandle,
@@ -442,6 +442,14 @@ workflow := func() {
442
442
 
443
443
 
444
444
 
445
+ _testGetLastRawStep: func() {
446
+ ll.assert(len(steps) > 0, "_testGetLastRawStep: no steps registered")
447
+ return steps[len(steps)-1](undefined)
448
+ },
449
+
450
+
451
+
452
+
445
453
 
446
454
 
447
455
 
@@ -513,6 +521,7 @@ workflow := func() {
513
521
  nRowsOpt := undefined
514
522
  ignoreErrorsOpt := undefined
515
523
  commentPrefixOpt := undefined
524
+ delimiterOpt := undefined
516
525
 
517
526
  if is_map(frameInput) && !is_undefined(frameInput.file) {
518
527
  fileRef = frameInput.file
@@ -610,6 +619,15 @@ workflow := func() {
610
619
  commentPrefixOpt = opts.commentPrefix
611
620
  }
612
621
 
622
+ if !is_undefined(opts.delimiter) {
623
+ if !is_string(opts.delimiter) {
624
+ ll.panic("'delimiter' option must be a string. Got: %T", opts.delimiter)
625
+ }
626
+ ll.assert(finalFormat == "csv" || finalFormat == "tsv",
627
+ "'delimiter' option applies only to csv/tsv reads, not %q", finalFormat)
628
+ delimiterOpt = opts.delimiter
629
+ }
630
+
613
631
  if !is_undefined(fileRef) {
614
632
  ll.assert(
615
633
  smart.isReference(fileRef),
@@ -682,6 +700,13 @@ workflow := func() {
682
700
  if !is_undefined(commentPrefixOpt) {
683
701
  step.commentPrefix = commentPrefixOpt
684
702
  }
703
+
704
+
705
+
706
+
707
+ if !is_undefined(delimiterOpt) {
708
+ step.delimiter = delimiterOpt
709
+ }
685
710
  }
686
711
 
687
712
 
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/workflow-tengo",
3
- "version": "6.3.3",
3
+ "version": "6.5.0",
4
4
  "description": "Tengo SDK library",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "dependencies": {
19
19
  "@milaboratories/pframes-rs-wasip2": "1.1.44",
20
20
  "@milaboratories/software-pframes-conv": "2.2.9",
21
- "@platforma-open/milaboratories.software-small-binaries": "^2.0.3",
21
+ "@platforma-open/milaboratories.software-small-binaries": "^2.1.1",
22
22
  "@platforma-open/milaboratories.software-ptexter": "1.2.2",
23
23
  "@platforma-open/milaboratories.software-ptabler": "2.1.1"
24
24
  },