@platforma-sdk/workflow-tengo 5.3.3 → 5.3.4

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 (52) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/tengo/lib/exec.lib.tengo +4 -0
  5. package/dist/tengo/lib/exec.monetization_internal.lib.tengo +8 -0
  6. package/dist/tengo/lib/pframes.spec.lib.tengo +6 -0
  7. package/dist/tengo/lib/pframes.util.lib.tengo +404 -1
  8. package/dist/tengo/lib/pframes.xsv.lib.tengo +117 -65
  9. package/dist/tengo/lib/pt.expression.lib.tengo +828 -268
  10. package/dist/tengo/lib/pt.lib.tengo +567 -216
  11. package/dist/tengo/lib/pt.util.lib.tengo +231 -15
  12. package/dist/tengo/lib/slices.lib.tengo +20 -1
  13. package/dist/tengo/lib/smart.lib.tengo +0 -1
  14. package/dist/tengo/tpl/anonymize.deanonymize.plj.gz +0 -0
  15. package/dist/tengo/tpl/anonymize.impl.plj.gz +0 -0
  16. package/dist/tengo/tpl/check_network.create_workdir_from_storage.plj.gz +0 -0
  17. package/dist/tengo/tpl/check_network.download_blob.plj.gz +0 -0
  18. package/dist/tengo/tpl/check_network.run_hello_world.plj.gz +0 -0
  19. package/dist/tengo/tpl/check_network.run_hello_world_py.plj.gz +0 -0
  20. package/dist/tengo/tpl/check_network.upload_blob.plj.gz +0 -0
  21. package/dist/tengo/tpl/check_network.upload_template.plj.gz +0 -0
  22. package/dist/tengo/tpl/check_network.workdir_type_name_extractor.plj.gz +0 -0
  23. package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
  24. package/dist/tengo/tpl/exec.delayed-compute-allocation.impl.plj.gz +0 -0
  25. package/dist/tengo/tpl/exec.exec-impl.plj.gz +0 -0
  26. package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
  27. package/dist/tengo/tpl/exec.load-software.impl.plj.gz +0 -0
  28. package/dist/tengo/tpl/exec.monetization_info.plj.gz +0 -0
  29. package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
  30. package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
  31. package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
  32. package/dist/tengo/tpl/exec.python.venv-bin-path.plj.gz +0 -0
  33. package/dist/tengo/tpl/ll.get-field.plj.gz +0 -0
  34. package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
  35. package/dist/tengo/tpl/pframes.export-pframe-for-ui.plj.gz +0 -0
  36. package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
  37. package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
  38. package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
  39. package/dist/tengo/tpl/pframes.parse-to-json.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-import-if-not-null.plj.gz +0 -0
  44. package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
  45. package/dist/tengo/tpl/pt.import-dir.plj.gz +0 -0
  46. package/dist/tengo/tpl/pt.workflow-run.plj.gz +0 -0
  47. package/dist/tengo/tpl/waiter.waiter.plj.gz +0 -0
  48. package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
  49. package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
  50. package/dist/tengo/tpl/workflow.query-anchored.plj.gz +0 -0
  51. package/package.json +3 -3
  52. package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
@@ -7,14 +7,16 @@ exec := import("@platforma-sdk/workflow-tengo:exec")
7
7
  json := import("json")
8
8
  text := import("text")
9
9
  slices := import("@platforma-sdk/workflow-tengo:slices")
10
- validation := import("@platforma-sdk/workflow-tengo:validation")
11
10
  execConstants := import("@platforma-sdk/workflow-tengo:exec.constants")
12
- oop := import("@platforma-sdk/workflow-tengo:oop")
13
- pframes := import("@platforma-sdk/workflow-tengo:pframes")
11
+ pframesBuilder := import("@platforma-sdk/workflow-tengo:pframes.builder")
12
+ pframesConstants := import("@platforma-sdk/workflow-tengo:pframes.constants")
13
+ pframesUtil := import("@platforma-sdk/workflow-tengo:pframes.util")
14
+ pframesSpec := import("@platforma-sdk/workflow-tengo:pframes.spec")
15
+ render := import("@platforma-sdk/workflow-tengo:render")
16
+ maps := import("@platforma-sdk/workflow-tengo:maps")
17
+ canonical := import("@platforma-sdk/workflow-tengo:canonical")
14
18
 
15
19
 
16
- importDirTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pt.import-dir")
17
-
18
20
 
19
21
 
20
22
 
@@ -57,6 +59,186 @@ _newDataFrameGroupBy := undefined
57
59
  workflowCounter := 0
58
60
 
59
61
 
62
+ _validateColumn := func(name, columnStruct) {
63
+ ll.assert(is_string(name), "column name must be a string")
64
+ ll.assert(is_map(columnStruct), "columnStruct must be a map")
65
+ ll.assert(!is_undefined(columnStruct.spec), "columnStruct must have 'spec' field defined")
66
+ ll.assert(!is_undefined(columnStruct.data), "columnStruct must have 'data' field defined")
67
+ }
68
+
69
+ _newPEntry := func(columnsByName, joinTree) {
70
+ return ll.toStrict({
71
+
72
+ _getColumnsMap: func() {
73
+ return columnsByName
74
+ },
75
+
76
+ _getJoinTree: func() {
77
+ return joinTree
78
+ }
79
+ })
80
+ }
81
+
82
+ _isPEntry := func(entry) {
83
+ return ll.isMap(entry) && ll.methodExists(entry, "_getColumnsMap") && ll.methodExists(entry, "_getJoinTree");
84
+ }
85
+
86
+ slicedColumnCounter := 0
87
+
88
+ p := ll.toStrict({
89
+
90
+
91
+
92
+
93
+
94
+
95
+ column: func(name, columnStruct) {
96
+ _validateColumn(name, columnStruct)
97
+ columnsByName := {}
98
+ columnsByName[name] = columnStruct
99
+ return _newPEntry(columnsByName, {
100
+ type: "column",
101
+ columnId: name
102
+ })
103
+ },
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+ slicedColumn: func(name, columnMap, axisFilters) {
112
+ _validateColumn(name, columnMap)
113
+ util.validateAxisFilters(axisFilters)
114
+
115
+ slicedColumnCounter += 1
116
+ oldName := "p_sliced_" + slicedColumnCounter
117
+
118
+ columnsByName := {}
119
+ columnsByName[oldName] = columnMap
120
+ return _newPEntry(columnsByName, {
121
+ type: "slicedColumn",
122
+ columnId: oldName,
123
+ newId: name,
124
+ axisFilters: axisFilters
125
+ })
126
+ },
127
+
128
+
129
+
130
+
131
+
132
+ inner: func(...entries) {
133
+ ll.assert(len(entries) > 0, "inner join requires at least one entry")
134
+
135
+ mergedColMap := {}
136
+ joinTreeEntries := []
137
+
138
+ for entry in entries {
139
+ ll.assert(_isPEntry(entry), "all entries must be PEntry objects")
140
+
141
+ entryColMap := entry._getColumnsMap()
142
+ entryJoinTree := entry._getJoinTree()
143
+
144
+ for colName, colInfo in entryColMap {
145
+ if !is_undefined(mergedColMap[colName]) {
146
+ ll.panic("column name %s is already used, please pick a different name", colName)
147
+ }
148
+ mergedColMap[colName] = colInfo
149
+ }
150
+
151
+ joinTreeEntries = append(joinTreeEntries, entryJoinTree)
152
+ }
153
+
154
+ joinTree := {
155
+ type: "inner",
156
+ entries: joinTreeEntries
157
+ }
158
+
159
+ return _newPEntry(mergedColMap, joinTree)
160
+ },
161
+
162
+
163
+
164
+
165
+
166
+ full: func(...entries) {
167
+ ll.assert(len(entries) > 0, "full join requires at least one entry")
168
+
169
+ mergedColMap := {}
170
+ joinTreeEntries := []
171
+
172
+ for entry in entries {
173
+ ll.assert(_isPEntry(entry), "all entries must be PEntry objects")
174
+
175
+ entryColMap := entry._getColumnsMap()
176
+ entryJoinTree := entry._getJoinTree()
177
+
178
+ for colName, colInfo in entryColMap {
179
+ if !is_undefined(mergedColMap[colName]) {
180
+ ll.panic("column name %s is already used, please pick a different name", colName)
181
+ }
182
+ mergedColMap[colName] = colInfo
183
+ }
184
+
185
+ joinTreeEntries = append(joinTreeEntries, entryJoinTree)
186
+ }
187
+
188
+ joinTree := {
189
+ type: "full",
190
+ entries: joinTreeEntries
191
+ }
192
+
193
+ return _newPEntry(mergedColMap, joinTree)
194
+ },
195
+
196
+
197
+
198
+
199
+
200
+
201
+ outer: func(primary, ...secondary) {
202
+ ll.assert(_isPEntry(primary), "primary entry must be a PEntry object")
203
+ ll.assert(len(secondary) > 0, "outer join requires at least one secondary entry")
204
+
205
+ mergedColMap := {}
206
+
207
+ primaryColMap := primary._getColumnsMap()
208
+ primaryJoinTree := primary._getJoinTree()
209
+
210
+ for colName, colInfo in primaryColMap {
211
+ mergedColMap[colName] = colInfo
212
+ }
213
+
214
+ secondaryJoinTrees := []
215
+
216
+ for entry in secondary {
217
+ ll.assert(_isPEntry(entry), "all secondary entries must be PEntry objects")
218
+
219
+ entryColMap := entry._getColumnsMap()
220
+ entryJoinTree := entry._getJoinTree()
221
+
222
+ for colName, colInfo in entryColMap {
223
+ if !is_undefined(mergedColMap[colName]) {
224
+ ll.panic("column name %s is already used, please pick a different name", colName)
225
+ }
226
+ mergedColMap[colName] = colInfo
227
+ }
228
+
229
+ secondaryJoinTrees = append(secondaryJoinTrees, entryJoinTree)
230
+ }
231
+
232
+ joinTree := {
233
+ type: "outer",
234
+ primary: primaryJoinTree,
235
+ secondary: secondaryJoinTrees
236
+ }
237
+
238
+ return _newPEntry(mergedColMap, joinTree)
239
+ }
240
+ })
241
+
60
242
 
61
243
  _detectFormatFromFileName := func(fileName) {
62
244
  if text.has_suffix(fileName, ".csv") {
@@ -83,17 +265,25 @@ workflow := func() {
83
265
 
84
266
  inFiles := []
85
267
 
268
+ inFrames := {}
269
+
86
270
  outFiles := []
87
271
 
88
272
  outContentFiles := []
89
273
 
90
274
  outFramesSpecs := {}
91
275
 
276
+ outFramesRename := {}
277
+
92
278
  anonymousFilesCounter := 0
93
279
  anonymousDFCounter := 0
280
+ pcolumnCounter := 0
281
+ pframeCounter := 0
282
+ specDistillerSingleton := undefined
94
283
 
95
284
  cpu := undefined
96
285
  mem := undefined
286
+ inputCache := undefined
97
287
  queue := execConstants.MEDIUM_QUEUE
98
288
 
99
289
  self := undefined
@@ -111,10 +301,20 @@ workflow := func() {
111
301
 
112
302
 
113
303
  addRawStep: func(step) {
114
- steps = append(steps, step)
304
+ ll.assert(is_map(step), "step must be a map")
305
+ ll.assert(is_string(step.type), "step.type must be a string discriminator")
306
+ self._addStep(func(ctx) {
307
+ return step
308
+ })
115
309
  return self
116
310
  },
117
311
 
312
+ _addStep: func(step) {
313
+ ll.assert(is_callable(step), "step must be a function")
314
+ steps = append(steps, step)
315
+ },
316
+
317
+
118
318
 
119
319
 
120
320
 
@@ -163,6 +363,8 @@ workflow := func() {
163
363
 
164
364
 
165
365
  frame: func(frameInput, ...optionsRaw) {
366
+ ll.assert(!is_undefined(frameInput), "frameInput must be provided")
367
+
166
368
  opts := {}
167
369
  if len(optionsRaw) > 0 {
168
370
  if len(optionsRaw) == 1 && is_map(optionsRaw[0]) {
@@ -172,6 +374,7 @@ workflow := func() {
172
374
  }
173
375
  }
174
376
 
377
+ frameResource := undefined
175
378
  fileRef := undefined
176
379
  fileContent := undefined
177
380
  inputSchema := undefined
@@ -184,9 +387,7 @@ workflow := func() {
184
387
  nRowsOpt := undefined
185
388
  ignoreErrorsOpt := undefined
186
389
 
187
- isStructuralInput := is_map(frameInput) && !is_undefined(frameInput.file)
188
-
189
- if isStructuralInput {
390
+ if is_map(frameInput) && !is_undefined(frameInput.file) {
190
391
  fileRef = frameInput.file
191
392
  inputXsvType = frameInput.xsvType
192
393
 
@@ -198,13 +399,33 @@ workflow := func() {
198
399
  }
199
400
 
200
401
  finalFormat = inputXsvType
201
- } else {
202
- if smart.isReference(frameInput) {
203
- fileRef = frameInput
402
+ } else if smart.isReference(frameInput) {
403
+ frameInputResource := frameInput
404
+ if smart.isField(frameInputResource) {
405
+ frameInputResource = frameInputResource.getValue()
406
+ }
407
+ if frameInputResource.checkResourceType(pframesConstants.RTYPE_P_FRAME) {
408
+ frameResource = frameInputResource
409
+ finalFormat = "pframe"
204
410
  } else {
205
- validation.assertType(frameInput, ["or", "string", "bytes"])
206
- fileContent = frameInput
411
+ fileRef = frameInput
412
+ }
413
+ } else if is_string(frameInput) || is_bytes(frameInput) {
414
+ fileContent = frameInput
415
+ } else if pframesUtil.isFlatPfMap(frameInput) {
416
+ frameResource = frameInput
417
+ finalFormat = "pframe"
418
+ } else if pframesUtil.isStructuredPfMap(frameInput) {
419
+ pfBuilder := pframesBuilder.pFrameBuilder()
420
+ for key, value in frameInput {
421
+ pfBuilder.add(key, value.spec, value.data)
207
422
  }
423
+ frameResource = pfBuilder.build()
424
+ finalFormat = "pframe"
425
+ } else if _isPEntry(frameInput) {
426
+ finalFormat = "pentry"
427
+ } else {
428
+ ll.panic("unknown frame input type: %v", frameInput)
208
429
  }
209
430
 
210
431
 
@@ -277,22 +498,67 @@ workflow := func() {
277
498
  ignoreErrorsOpt = opts.ignoreErrors
278
499
  }
279
500
 
280
-
281
- if !is_undefined(fileRef) && !smart.isReference(fileRef) {
282
- ll.panic("frameInput.file (for structural input) or frameInput (for direct reference) must be a valid resource reference. Got: %v", fileRef)
283
- } else if !is_undefined(fileContent) && !is_string(fileContent) {
284
- ll.panic("frameInput (for direct content) must be a string. Got: %v", fileContent)
285
- }
286
-
287
501
  if !is_undefined(fileRef) {
502
+ ll.assert(
503
+ smart.isReference(fileRef),
504
+ "frameInput.file (for structural input) or frameInput (for direct reference) must be a valid resource reference, got: %v",
505
+ fileRef)
288
506
  inFiles = append(inFiles, { file: fileRef, name: finalFileName })
289
- } else {
507
+ } else if !is_undefined(fileContent) {
508
+ ll.assert(is_string(fileContent), "frameInput (for direct content) must be a string, got: %v", fileContent)
290
509
  inFiles = append(inFiles, { content: fileContent, name: finalFileName })
291
510
  }
292
511
 
293
512
 
294
- step := undefined
295
- if finalFormat == "parquet" {
513
+ step := undefined
514
+ if finalFormat == "pentry" || finalFormat == "pframe" {
515
+ if finalFormat == "pframe" {
516
+ columnsMap := pframesUtil.pFrameToColumnsMap(frameResource)
517
+ columnEntries := []
518
+ for frameUniqueColumnId, columnInfo in columnsMap {
519
+ columnEntry := p.column(frameUniqueColumnId, columnInfo)
520
+ columnEntries = append(columnEntries, columnEntry)
521
+ }
522
+ frameInput = p.full(columnEntries...)
523
+ }
524
+
525
+ columnsMap := frameInput._getColumnsMap()
526
+ src := frameInput._getJoinTree()
527
+
528
+
529
+ translation := {}
530
+ rename := {}
531
+ renamedColumnsMap := {}
532
+
533
+ for frameUniqueColumnId, columnInfo in columnsMap {
534
+ workflowUniqueColumnId := self._newUniqueColumnId()
535
+
536
+ translation[workflowUniqueColumnId] = frameUniqueColumnId
537
+ rename[frameUniqueColumnId] = workflowUniqueColumnId
538
+ renamedColumnsMap[workflowUniqueColumnId] = columnInfo
539
+ }
540
+
541
+
542
+ request := {
543
+ src: src,
544
+ filters: []
545
+ }
546
+ renamedRequest := util.renameJoinColumns(request, rename)
547
+
548
+
549
+ ll.assert(is_undefined(inFrames[finalDataFrameId]),
550
+ "frame with name '%s' already added", finalDataFrameId)
551
+ inFrames[finalDataFrameId] = {
552
+ columns: renamedColumnsMap
553
+ }
554
+
555
+ step = {
556
+ type: "read_frame",
557
+ name: finalDataFrameId,
558
+ request: renamedRequest,
559
+ translation: translation
560
+ }
561
+ } else if finalFormat == "parquet" {
296
562
  step = {
297
563
  type: "read_parquet",
298
564
  file: finalFileName,
@@ -360,66 +626,33 @@ workflow := func() {
360
626
  frameFromColumnBundle: func(bundle, options) {
361
627
  util.validateParamsForFrameFromColumnBundle(options)
362
628
 
363
- decodeColumn := func(columnId) {
364
- decoded := undefined
365
- if is_string(columnId) {
366
- decoded = json.decode(columnId)
367
- } else if is_map(columnId) {
368
- decoded = columnId
369
- } else {
370
- ll.panic("Invalid column ID type. Expected string or object.")
371
- }
372
- return decoded
373
- }
374
- isFilteredColumn := func(columnId) {
375
- decoded := decodeColumn(columnId)
376
- return is_map(decoded) && !is_undefined(decoded.source) && !is_undefined(decoded.axisFilters)
629
+ entries := []
630
+ aliases := []
631
+ for axis in options.axes {
632
+ aliases = append(aliases, exp.axis(axis.spec).alias(axis.column))
377
633
  }
378
-
379
- axes := options.axes
380
- columns := options.columns
381
- tsvBuilder := pframes.tsvFileBuilder()
382
- axesHeaders := []
383
-
384
- for idx, axis in axes {
385
- tsvBuilder.setAxisHeader(axis.spec, axis.column)
386
- axesHeaders = append(axesHeaders, axis.column)
387
- }
388
-
389
- filteredColumns := []
390
- for column in columns {
391
- if (isFilteredColumn(column)) {
392
- filteredColumns = append(filteredColumns, column)
634
+ for column in options.columns {
635
+ decoded := json.decode(column)
636
+ if (is_undefined(decoded.axisFilters)) {
637
+ aliases = append(aliases, exp.col(decoded.name))
638
+ entry := p.column(decoded.name, bundle.getColumn(decoded))
639
+ entries = append(entries, entry)
393
640
  } else {
394
- tsvBuilder.add(bundle.getColumn(column))
395
- }
396
- }
397
-
398
- mainDF := self.frame(tsvBuilder.buildForPT())
399
-
400
- for column in filteredColumns {
401
- decoded := decodeColumn(column)
402
- source := decoded.source
403
- axisFilters := decoded.axisFilters
404
-
405
- tsv := pframes.tsvFileBuilder()
406
- for axis in axes {
407
- tsv.setAxisHeader(axis.spec, axis.column)
408
- }
409
- tsv.add(bundle.getColumn(source), { header: column })
410
- df := self.frame(tsv.buildForPT())
411
- joinColumns := slices.map(axes, func(axis) { return axis.column })
412
- for axisFilter in axisFilters {
413
- id := axesHeaders[axisFilter[0]]
414
- value := axisFilter[1]
415
- df = df.filter(exp.col(id).eq(exp.lit(value))).withoutColumns(id)
416
- joinColumns = slices.filter(joinColumns, func(c) { return c != id } )
641
+ aliases = append(aliases, exp.col(decoded.source.name))
642
+ entry := p.slicedColumn(
643
+ decoded.source.name,
644
+ bundle.getColumn(decoded.source),
645
+ slices.map(decoded.axisFilters, func(axisFilter) {
646
+ return {
647
+ axisIndex: axisFilter[0],
648
+ constant: axisFilter[1]
649
+ }
650
+ })
651
+ )
652
+ entries = append(entries, entry)
417
653
  }
418
-
419
- mainDF = mainDF.join(df, { on: joinColumns })
420
654
  }
421
-
422
- return mainDF
655
+ return self.frame(p.full(entries...)).select(aliases...)
423
656
  },
424
657
 
425
658
  _newAnonymousDataFrameId: func() {
@@ -430,19 +663,63 @@ workflow := func() {
430
663
  anonymousFilesCounter += 1
431
664
  return "anonymous_" + string(anonymousFilesCounter) + "." + extension
432
665
  },
666
+ _newUniqueColumnId: func() {
667
+ pcolumnCounter += 1
668
+ return "pcolumn_" + string(pcolumnCounter)
669
+ },
670
+ _newUniquePFrameName: func() {
671
+ pframeCounter += 1
672
+ return "pframe_" + string(pframeCounter)
673
+ },
433
674
  _saveFile: func(name) {
434
675
  outFiles = append(outFiles, name)
435
676
  },
436
677
  _saveFileContent: func(name) {
437
678
  outContentFiles = append(outContentFiles, name)
438
679
  },
439
- _saveFrameSpecs: func(name, specs) {
680
+ _saveFrameSpecs: func(name, rename, specs) {
440
681
  ll.assert(
441
682
  outFramesSpecs[name] == undefined,
442
683
  "frame with name '%s' already saved, please pick a different name",
443
684
  name
444
685
  )
445
686
  outFramesSpecs[name] = specs
687
+ outFramesRename[name] = rename
688
+ },
689
+ _getSpecDistiller: func() {
690
+ if is_undefined(specDistillerSingleton) {
691
+ rawColumnSpecs := []
692
+ for _, columns in inFrames {
693
+ for columnId, columnInfo in columns {
694
+ rawColumnSpecs = append(rawColumnSpecs, columnInfo.spec)
695
+ }
696
+ }
697
+ specDistillerSingleton = pframesSpec.createSpecDistiller(rawColumnSpecs)
698
+ }
699
+ return specDistillerSingleton
700
+ },
701
+ _getDistilledAxesSpecs: func() {
702
+ specDistiller := self._getSpecDistiller()
703
+ canonicalMap := {}
704
+ distilledAxesSpecs := {}
705
+ for _, frame in inFrames {
706
+ for _, columnInfo in frame.columns {
707
+ for axis in columnInfo.spec.axesSpec {
708
+ if is_undefined(canonicalMap[axis.name]) {
709
+ canonicalMap[axis.name] = {}
710
+ }
711
+ canonicalizedAxisSpec := canonical.encode(axis)
712
+ if is_undefined(canonicalMap[axis.name][canonicalizedAxisSpec]) {
713
+ canonicalMap[axis.name][canonicalizedAxisSpec] = true
714
+ if is_undefined(distilledAxesSpecs[axis.name]) {
715
+ distilledAxesSpecs[axis.name] = []
716
+ }
717
+ distilledAxesSpecs[axis.name] = append(distilledAxesSpecs[axis.name], axis)
718
+ }
719
+ }
720
+ }
721
+ }
722
+ return distilledAxesSpecs
446
723
  },
447
724
 
448
725
 
@@ -509,60 +786,112 @@ workflow := func() {
509
786
 
510
787
 
511
788
 
789
+
790
+
791
+
792
+ cacheInputs: func(time) {
793
+ ll.assert(
794
+ is_int(time),
795
+ "input cache time must be an integer. " +
796
+ "Did you forget to import a standard tengo library 'times'?")
797
+ inputCache = time
798
+ return self
799
+ },
800
+
801
+
802
+
803
+
804
+
805
+
806
+
512
807
  run: func() {
513
- ptablerSw := assets.importSoftware("@platforma-open/milaboratories.software-ptabler:main")
514
- ptablerCmdBuilder := exec.builder().
515
- setQueue(queue).
516
- printErrStreamToStdout().
517
- dontSaveStdoutOrStderr().
518
- software(ptablerSw).
519
- envExpr("POLARS_MAX_THREADS", "{system.cpu}").
520
- arg("workflow_sc.json").
521
- writeFile("workflow_sc.json", json.encode({
522
- workflow: steps
523
- }));
808
+ workflowRunTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pt.workflow-run")
524
809
 
525
- if !is_undefined(cpu) {
526
- ptablerCmdBuilder.cpu(cpu)
810
+ specDistiller := self._getSpecDistiller()
811
+ distilledAxesSpecs := self._getDistilledAxesSpecs()
812
+ ctx := {
813
+ specDistiller: specDistiller,
814
+ distilledAxesSpecs: distilledAxesSpecs
527
815
  }
528
-
529
- if !is_undefined(mem) {
530
- ptablerCmdBuilder.mem(mem)
816
+ workflowJsonStruct := {
817
+ workflow: slices.map(steps, func(step) {
818
+ return step(ctx)
819
+ })
531
820
  }
532
821
 
822
+ inFilesMap := {}
823
+ inFilesContentMap := {}
533
824
  for inFile in inFiles {
534
825
  if !is_undefined(inFile.file) {
535
- ptablerCmdBuilder.addFile(inFile.name, inFile.file)
826
+ inFilesMap[inFile.name] = inFile.file
827
+ } else if !is_undefined(inFile.content) {
828
+ inFilesContentMap[inFile.name] = inFile.content
536
829
  } else {
537
- ptablerCmdBuilder.writeFile(inFile.name, inFile.content)
830
+ ll.panic("inFile must have either file or content, got: %v", inFile)
538
831
  }
539
832
  }
540
-
541
- for outFile in outFiles {
542
- ptablerCmdBuilder.saveFile(outFile)
543
- }
544
-
545
- for outContentFile in outContentFiles {
546
- ptablerCmdBuilder.saveFileContent(outContentFile)
547
- }
548
-
549
- tplArgs := {}
833
+
834
+ outFrames := {}
550
835
  for frameName, specsById in outFramesSpecs {
551
836
  columnIds := []
552
837
  for columnId, _ in specsById {
553
838
  columnIds = append(columnIds, columnId)
554
839
  }
555
- tplArgs[frameName] = columnIds;
556
- }
557
- workdirProcessorName := "import_dir";
558
- if len(tplArgs) > 0 {
559
- ptablerCmdBuilder.processWorkdir(workdirProcessorName, importDirTpl, tplArgs)
840
+ outFrames[outFramesRename[frameName]] = columnIds;
560
841
  }
561
842
 
562
- execResult := ptablerCmdBuilder.run()
843
+ inFramesDistilled := maps.mapValues(inFrames, func(frame) {
844
+ return {
845
+ columns: maps.mapValues(frame.columns, func(columnInfo) {
846
+ return {
847
+ spec: specDistiller.distill(columnInfo.spec),
848
+ data: columnInfo.data
849
+ }
850
+ })
851
+ }
852
+ })
853
+
854
+ runInputs := {
855
+ workflowJsonStruct: workflowJsonStruct,
856
+ inFiles: inFilesMap,
857
+ inFilesContent: inFilesContentMap,
858
+ inFrames: inFramesDistilled,
859
+ outFiles: outFiles,
860
+ outFilesContent: outContentFiles,
861
+ outFrames: outFrames
862
+ }
863
+ runMetaInputs := {
864
+ queue: queue,
865
+ cpu: cpu,
866
+ mem: mem,
867
+ inputCache: inputCache
868
+ }
869
+ ptResult := render.create(workflowRunTpl, runInputs, {
870
+ metaInputs: runMetaInputs
871
+ });
563
872
 
564
873
  self := undefined
565
- self = ll.toStrict(oop.inherit(execResult, {
874
+ self = ll.toStrict({
875
+
876
+
877
+
878
+
879
+
880
+
881
+ getFile: func(fileName) {
882
+ return ptResult.resolveOutput(["outFiles", fileName])
883
+ },
884
+
885
+
886
+
887
+
888
+
889
+
890
+
891
+ getFileContent: func(fileName) {
892
+ return ptResult.resolveOutput(["outFilesContent", fileName])
893
+ },
894
+
566
895
 
567
896
 
568
897
 
@@ -572,17 +901,15 @@ workflow := func() {
572
901
  getFrameDirect: func(frameName) {
573
902
  specs := outFramesSpecs[frameName]
574
903
  ll.assert(!is_undefined(specs), "saveFrameDirect was not called for frame '%s'", frameName)
575
- savedFrames := execResult.getProcessorResult(workdirProcessorName)
576
904
 
577
- pf := pframes.pFrameBuilder()
578
- for pColumnId, pColumnSpec in specs {
579
- pColumnRef := util.makeFrameColumnRef(frameName, pColumnId)
580
- pColumnData := savedFrames.getFutureInputField(pColumnRef)
581
- pf.add(pColumnId, pColumnSpec, pColumnData)
905
+ pf := pframesBuilder.pFrameBuilder()
906
+ for columnId, columnSpec in specs {
907
+ columnData := ptResult.resolveOutput(["outFrames", outFramesRename[frameName], columnId])
908
+ pf.add(columnId, columnSpec, columnData)
582
909
  }
583
910
  return pf.build()
584
911
  }
585
- }))
912
+ })
586
913
  return self
587
914
  }
588
915
  }
@@ -612,20 +939,22 @@ _newDataFrameGroupBy = func(parentWorkflow, dfName, groupByExpressions) {
612
939
  agg: func(...aggExpressions) {
613
940
  ll.assert(len(aggExpressions) > 0, "agg method requires at least one aggregation expression.")
614
941
 
615
- aggregations := slices.map(aggExpressions, func(aggExpr) {
616
- if !exp._isAggregation(aggExpr) {
617
- ll.panic("Invalid argument to agg: Expected an aggregation expression object, got %v", aggExpr)
618
- }
619
- return aggExpr.getAggregation()
620
- })
621
-
622
942
  outputDfName := parentWorkflow._newAnonymousDataFrameId()
623
- parentWorkflow.addRawStep({
624
- type: "aggregate",
625
- inputTable: dfName,
626
- outputTable: outputDfName,
627
- groupBy: exp._mapToExpressionStructList(groupByExpressions, "col"),
628
- aggregations: aggregations
943
+ parentWorkflow._addStep(func(ctx) {
944
+ aggregations := slices.map(aggExpressions, func(aggExpr) {
945
+ if !exp._isAggregation(aggExpr) {
946
+ ll.panic("Invalid argument to agg: Expected an aggregation expression object, got %v", aggExpr)
947
+ }
948
+ return aggExpr._getAggregation(ctx)
949
+ })
950
+
951
+ return {
952
+ type: "aggregate",
953
+ inputTable: dfName,
954
+ outputTable: outputDfName,
955
+ groupBy: exp._mapToExpressionStructList(groupByExpressions, "col", ctx),
956
+ aggregations: aggregations
957
+ }
629
958
  })
630
959
  return _newDataFrame(parentWorkflow, outputDfName)
631
960
  }
@@ -634,37 +963,28 @@ _newDataFrameGroupBy = func(parentWorkflow, dfName, groupByExpressions) {
634
963
  }
635
964
 
636
965
  _newDataFrame = func(parentWorkflow, dfName) {
637
- _mapExprsToStepCols := func(exprs, methodName) {
966
+ _mapExprsToStepCols := func(exprs, methodName, ctx) {
638
967
  ll.assert(len(exprs) > 0, methodName + " requires at least one expression argument.")
639
968
  cols := []
640
969
  for expr in exprs {
641
970
  if !exp._isExpression(expr) {
642
971
  ll.panic("Invalid argument to " + methodName + ": Expected an expression object, got %v", expr)
643
972
  }
644
- cols = append(cols, {
645
- name: expr.getAlias(),
646
- expression: expr.getExpression()
647
- })
973
+ cols = append(cols, expr._getExpression(ctx))
648
974
  }
649
975
  return cols
650
976
  }
651
977
 
652
- _mapExprsToStepColsAllowingStrings := func(exprs, methodName) {
978
+ _mapExprsToStepColsAllowingStrings := func(exprs, methodName, ctx) {
653
979
  ll.assert(len(exprs) > 0, methodName + " requires at least one expression argument.")
654
980
  cols := []
655
981
  for expr in exprs {
656
982
  if is_string(expr) {
657
983
 
658
984
  colExpr := exp.col(expr)
659
- cols = append(cols, {
660
- name: colExpr.getAlias(),
661
- expression: colExpr.getExpression()
662
- })
985
+ cols = append(cols, colExpr._getExpression(ctx))
663
986
  } else if exp._isExpression(expr) {
664
- cols = append(cols, {
665
- name: expr.getAlias(),
666
- expression: expr.getExpression()
667
- })
987
+ cols = append(cols, expr._getExpression(ctx))
668
988
  } else {
669
989
  ll.panic("Invalid argument to " + methodName + ": Expected an expression object or string column name, got %v", expr)
670
990
  }
@@ -763,13 +1083,15 @@ _newDataFrame = func(parentWorkflow, dfName) {
763
1083
 
764
1084
 
765
1085
  withColumns: func(...expressions) {
766
- stepCols := _mapExprsToStepCols(expressions, "withColumns")
767
1086
  outputDfName := parentWorkflow._newAnonymousDataFrameId()
768
- parentWorkflow.addRawStep({
769
- type: "with_columns",
770
- inputTable: dfName,
771
- outputTable: outputDfName,
772
- columns: stepCols
1087
+ parentWorkflow._addStep(func(ctx) {
1088
+ stepCols := _mapExprsToStepCols(expressions, "withColumns", ctx)
1089
+ return {
1090
+ type: "with_columns",
1091
+ inputTable: dfName,
1092
+ outputTable: outputDfName,
1093
+ columns: stepCols
1094
+ }
773
1095
  })
774
1096
  return _newDataFrame(parentWorkflow, outputDfName)
775
1097
  },
@@ -789,13 +1111,15 @@ _newDataFrame = func(parentWorkflow, dfName) {
789
1111
 
790
1112
 
791
1113
  select: func(...expressions) {
792
- stepCols := _mapExprsToStepColsAllowingStrings(expressions, "select")
793
1114
  outputDfName := parentWorkflow._newAnonymousDataFrameId()
794
- parentWorkflow.addRawStep({
795
- type: "select",
796
- inputTable: dfName,
797
- outputTable: outputDfName,
798
- columns: stepCols
1115
+ parentWorkflow._addStep(func(ctx) {
1116
+ stepCols := _mapExprsToStepColsAllowingStrings(expressions, "select", ctx)
1117
+ return {
1118
+ type: "select",
1119
+ inputTable: dfName,
1120
+ outputTable: outputDfName,
1121
+ columns: stepCols
1122
+ }
799
1123
  })
800
1124
  return _newDataFrame(parentWorkflow, outputDfName)
801
1125
  },
@@ -814,11 +1138,13 @@ _newDataFrame = func(parentWorkflow, dfName) {
814
1138
 
815
1139
 
816
1140
  addColumns: func(...expressions) {
817
- stepCols := _mapExprsToStepCols(expressions, "addColumns")
818
- parentWorkflow.addRawStep({
819
- type: "add_columns",
820
- table: dfName,
821
- columns: stepCols
1141
+ parentWorkflow._addStep(func(ctx) {
1142
+ stepCols := _mapExprsToStepCols(expressions, "addColumns", ctx)
1143
+ return {
1144
+ type: "add_columns",
1145
+ table: dfName,
1146
+ columns: stepCols
1147
+ }
822
1148
  })
823
1149
  return self
824
1150
  },
@@ -907,9 +1233,10 @@ _newDataFrame = func(parentWorkflow, dfName) {
907
1233
  util.validateParamsForSaveFrameDirect(params)
908
1234
 
909
1235
  specs := util.makeFrameSpecs(params)
910
- parentWorkflow._saveFrameSpecs(frameName, specs)
1236
+ sequentialFrameName := parentWorkflow._newUniquePFrameName()
1237
+ parentWorkflow._saveFrameSpecs(frameName, sequentialFrameName, specs)
911
1238
 
912
- step := util.makeWriteFrameStep(frameName, dfName, params)
1239
+ step := util.makeWriteFrameStep(sequentialFrameName, dfName, params)
913
1240
  parentWorkflow.addRawStep(step)
914
1241
  },
915
1242
 
@@ -965,12 +1292,35 @@ _newDataFrame = func(parentWorkflow, dfName) {
965
1292
  condition = exp.and(allConditions...)
966
1293
  }
967
1294
 
1295
+ outputDfName := parentWorkflow._newAnonymousDataFrameId()
1296
+ parentWorkflow._addStep(func(ctx) {
1297
+ return {
1298
+ type: "filter",
1299
+ inputTable: dfName,
1300
+ outputTable: outputDfName,
1301
+ condition: condition._getExpression(ctx)
1302
+ }
1303
+ })
1304
+ return _newDataFrame(parentWorkflow, outputDfName)
1305
+ },
1306
+
1307
+
1308
+
1309
+
1310
+
1311
+
1312
+
1313
+
1314
+
1315
+ limit: func(nRows) {
1316
+ ll.assert(is_int(nRows) && nRows > 0, "limit argument must be a positive number.")
1317
+
968
1318
  outputDfName := parentWorkflow._newAnonymousDataFrameId()
969
1319
  parentWorkflow.addRawStep({
970
- type: "filter",
1320
+ type: "limit",
971
1321
  inputTable: dfName,
972
1322
  outputTable: outputDfName,
973
- condition: condition.getExpression()
1323
+ n: nRows
974
1324
  })
975
1325
  return _newDataFrame(parentWorkflow, outputDfName)
976
1326
  },
@@ -1029,60 +1379,58 @@ _newDataFrame = func(parentWorkflow, dfName) {
1029
1379
  ll.panic("sort 'nulls_last' array length (%d) must match the number of sort keys (%d).", len(nullsLastOpt), len(by))
1030
1380
  }
1031
1381
 
1382
+ outputDfName := parentWorkflow._newAnonymousDataFrameId()
1032
1383
 
1384
+ parentWorkflow._addStep(func(ctx) {
1385
+ sortDirectives := []
1386
+ for i, item in by {
1387
+ directive := { value: undefined }
1033
1388
 
1034
- sortDirectives := []
1035
- for i, item in by {
1036
- directive := { value: undefined }
1389
+ if is_string(item) {
1390
+ directive.value = exp.col(item)._getExpression(ctx)
1391
+ } else if exp._isExpression(item) {
1392
+ directive.value = item._getExpression(ctx)
1393
+ } else {
1394
+ ll.panic("Invalid sort key at index %d in array: Expected a column name (string) or an expression object, got %T", i, item)
1395
+ }
1037
1396
 
1038
- if is_string(item) {
1039
- directive.value = exp.col(item).getExpression()
1040
- } else if exp._isExpression(item) {
1041
- directive.value = item.getExpression()
1042
- } else {
1043
- ll.panic("Invalid sort key at index %d in array: Expected a column name (string) or an expression object, got %T", i, item)
1044
- }
1045
1397
 
1398
+ itemDesc := false // Polars default
1399
+ if is_array(descendingOpt) {
1400
+ val := descendingOpt[i]
1401
+ ll.assert(is_bool(val), "Elements of 'descending' array (at index %d) must be booleans. Got: %T", i, val)
1402
+ itemDesc = val
1403
+ } else if is_bool(descendingOpt) {
1404
+ itemDesc = descendingOpt
1405
+ }
1406
+ if itemDesc { // Only include if true, as PTabler 'descending' defaults to false
1407
+ directive.descending = true
1408
+ }
1046
1409
 
1047
- itemDesc := false // Polars default
1048
- if is_array(descendingOpt) {
1049
- val := descendingOpt[i]
1050
- ll.assert(is_bool(val), "Elements of 'descending' array (at index %d) must be booleans. Got: %T", i, val)
1051
- itemDesc = val
1052
- } else if is_bool(descendingOpt) {
1053
- itemDesc = descendingOpt
1054
- }
1055
- if itemDesc { // Only include if true, as PTabler 'descending' defaults to false
1056
- directive.descending = true
1057
- }
1058
1410
 
1411
+ itemNullsLast := undefined // Let PTabler use its default if not specified
1412
+ if is_array(nullsLastOpt) {
1413
+ val := nullsLastOpt[i]
1414
+ ll.assert(is_bool(val), "Elements of 'nulls_last' array (at index %d) must be booleans. Got: %T", i, val)
1415
+ itemNullsLast = val
1416
+ } else if is_bool(nullsLastOpt) {
1417
+ itemNullsLast = nullsLastOpt
1418
+ }
1059
1419
 
1060
- itemNullsLast := undefined // Let PTabler use its default if not specified
1061
- if is_array(nullsLastOpt) {
1062
- val := nullsLastOpt[i]
1063
- ll.assert(is_bool(val), "Elements of 'nulls_last' array (at index %d) must be booleans. Got: %T", i, val)
1064
- itemNullsLast = val
1065
- } else if is_bool(nullsLastOpt) {
1066
- itemNullsLast = nullsLastOpt
1067
- }
1420
+ if !is_undefined(itemNullsLast) { // Only include if explicitly set
1421
+ directive.nullsLast = itemNullsLast
1422
+ }
1068
1423
 
1069
- if !is_undefined(itemNullsLast) { // Only include if explicitly set
1070
- directive.nullsLast = itemNullsLast
1424
+ sortDirectives = append(sortDirectives, directive)
1071
1425
  }
1072
1426
 
1073
- sortDirectives = append(sortDirectives, directive)
1074
- }
1075
-
1076
- outputDfName := parentWorkflow._newAnonymousDataFrameId()
1077
-
1078
- sortStep := {
1079
- type: "sort",
1080
- inputTable: dfName,
1081
- outputTable: outputDfName,
1082
- by: sortDirectives
1083
- }
1084
-
1085
- parentWorkflow.addRawStep(sortStep)
1427
+ return {
1428
+ type: "sort",
1429
+ inputTable: dfName,
1430
+ outputTable: outputDfName,
1431
+ by: sortDirectives
1432
+ }
1433
+ })
1086
1434
  return _newDataFrame(parentWorkflow, outputDfName)
1087
1435
  },
1088
1436
 
@@ -1438,10 +1786,12 @@ treeJoin := func(dataframes, opts) {
1438
1786
  }
1439
1787
 
1440
1788
  export ll.toStrict({
1789
+ p: p,
1441
1790
  workflow: workflow,
1442
1791
  concat: concat,
1443
1792
  treeJoin: treeJoin,
1444
1793
 
1794
+ axis: exp.axis,
1445
1795
  col: exp.col,
1446
1796
  lit: exp.lit,
1447
1797
  concatStr: exp.concatStr,
@@ -1453,5 +1803,6 @@ export ll.toStrict({
1453
1803
  or: exp.or,
1454
1804
  rank: exp.rank,
1455
1805
  when: exp.when,
1456
- rawExp: exp.rawExp
1806
+ rawExp: exp.rawExp,
1807
+ sc: exp.sc
1457
1808
  })