ddeutil-workflow 0.0.77__py3-none-any.whl → 0.0.79__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ddeutil/workflow/utils.py CHANGED
@@ -10,16 +10,24 @@ system for ID generation, datetime handling, string processing, template
10
10
  operations, and other common tasks.
11
11
 
12
12
  Functions:
13
- gen_id: Generate unique identifiers for workflow components
14
- make_exec: Create executable strings for shell commands
15
- filter_func: Filter functions based on criteria
16
- dump_all: Serialize data to various formats
17
- delay: Create delays in execution
18
- to_train: Convert strings to train-case format
13
+ to_train: Convert camel case strings to train case format
14
+ prepare_newline: Format messages with multiple newlines
15
+ replace_sec: Replace seconds and microseconds in datetime objects
16
+ clear_tz: Clear timezone info from datetime objects
19
17
  get_dt_now: Get current datetime with timezone
20
18
  get_d_now: Get current date
19
+ get_diff_sec: Calculate time difference in seconds
20
+ reach_next_minute: Check if datetime reaches next minute
21
+ wait_until_next_minute: Wait until next minute
22
+ delay: Add random delay to execution
23
+ gen_id: Generate unique identifiers for workflow components
24
+ default_gen_id: Generate default running ID
25
+ make_exec: Make files executable
26
+ filter_func: Filter function objects from data structures
21
27
  cross_product: Generate cross product of matrix values
22
- replace_sec: Replace template variables in strings
28
+ cut_id: Cut running ID to specified length
29
+ dump_all: Serialize nested BaseModel objects to dictionaries
30
+ obj_name: Get object name or class name
23
31
 
24
32
  Example:
25
33
  ```python
@@ -116,10 +124,11 @@ def clear_tz(dt: datetime) -> datetime:
116
124
  def get_dt_now(offset: float = 0.0) -> datetime:
117
125
  """Return the current datetime object.
118
126
 
119
- :param offset: An offset second value.
127
+ Args:
128
+ offset: An offset second value to subtract from current time.
120
129
 
121
- :rtype: datetime
122
- :return: The current datetime object that use an input timezone or UTC.
130
+ Returns:
131
+ datetime: The current datetime object with UTC timezone.
123
132
  """
124
133
  return datetime.now().replace(tzinfo=UTC) - timedelta(seconds=offset)
125
134
 
@@ -127,10 +136,11 @@ def get_dt_now(offset: float = 0.0) -> datetime:
127
136
  def get_d_now(offset: float = 0.0) -> date: # pragma: no cov
128
137
  """Return the current date object.
129
138
 
130
- :param offset: An offset second value.
139
+ Args:
140
+ offset: An offset second value to subtract from current time.
131
141
 
132
- :rtype: date
133
- :return: The current date object that use an input timezone or UTC.
142
+ Returns:
143
+ date: The current date object.
134
144
  """
135
145
  return (
136
146
  datetime.now().replace(tzinfo=UTC) - timedelta(seconds=offset)
@@ -138,13 +148,14 @@ def get_d_now(offset: float = 0.0) -> date: # pragma: no cov
138
148
 
139
149
 
140
150
  def get_diff_sec(dt: datetime, offset: float = 0.0) -> int:
141
- """Return second value that come from diff of an input datetime and the
142
- current datetime with specific timezone.
151
+ """Return second value from difference between input datetime and current datetime.
143
152
 
144
- :param dt: (datetime) A datetime object that want to get different second value.
145
- :param offset: (float) An offset second value.
153
+ Args:
154
+ dt: A datetime object to calculate difference from.
155
+ offset: An offset second value to add to the difference.
146
156
 
147
- :rtype: int
157
+ Returns:
158
+ int: The difference in seconds between the input datetime and current time.
148
159
  """
149
160
  return round(
150
161
  (
@@ -154,11 +165,17 @@ def get_diff_sec(dt: datetime, offset: float = 0.0) -> int:
154
165
 
155
166
 
156
167
  def reach_next_minute(dt: datetime, offset: float = 0.0) -> bool:
157
- """Check this datetime object is not in range of minute level on the current
158
- datetime.
168
+ """Check if datetime object reaches the next minute level.
159
169
 
160
- :param dt: (datetime) A datetime object that want to check.
161
- :param offset: (float) An offset second value.
170
+ Args:
171
+ dt: A datetime object to check.
172
+ offset: An offset second value.
173
+
174
+ Returns:
175
+ bool: True if datetime reaches next minute, False otherwise.
176
+
177
+ Raises:
178
+ ValueError: If the input datetime is less than current date.
162
179
  """
163
180
  diff: float = (
164
181
  replace_sec(clear_tz(dt)) - replace_sec(get_dt_now(offset=offset))
@@ -176,16 +193,21 @@ def reach_next_minute(dt: datetime, offset: float = 0.0) -> bool:
176
193
  def wait_until_next_minute(
177
194
  dt: datetime, second: float = 0
178
195
  ) -> None: # pragma: no cov
179
- """Wait with sleep to the next minute with an offset second value."""
196
+ """Wait with sleep to the next minute with an offset second value.
197
+
198
+ Args:
199
+ dt: The datetime to wait until next minute from.
200
+ second: Additional seconds to wait after reaching next minute.
201
+ """
180
202
  future: datetime = replace_sec(dt) + timedelta(minutes=1)
181
203
  time.sleep((future - dt).total_seconds() + second)
182
204
 
183
205
 
184
206
  def delay(second: float = 0) -> None: # pragma: no cov
185
- """Delay time that use time.sleep with random second value between
186
- 0.00 - 0.99 seconds.
207
+ """Delay execution with time.sleep and random second value between 0.00-0.99 seconds.
187
208
 
188
- :param second: (float) A second number that want to adds-on random value.
209
+ Args:
210
+ second: Additional seconds to add to the random delay.
189
211
  """
190
212
  global _DELAY_INDEX
191
213
  cached_random = _CACHED_DELAYS[_DELAY_INDEX % len(_CACHED_DELAYS)]
@@ -201,26 +223,24 @@ def gen_id(
201
223
  simple_mode: Optional[bool] = None,
202
224
  extras: DictData | None = None,
203
225
  ) -> str:
204
- """Generate running ID for able to tracking. This generates process use
205
- ``md5`` algorithm function if ``WORKFLOW_CORE_WORKFLOW_ID_SIMPLE_MODE`` set
206
- to false. But it will cut this hashing value length to 10 it the setting
207
- value set to true.
208
-
209
- Simple Mode:
210
-
211
- ... 0000 00 00 00 00 00 000000 T 0000000000
212
- ... year month day hour minute second microsecond sep simple-id
213
-
214
- :param value: A value that want to add to prefix before hashing with md5.
215
- :param sensitive: (bool) A flag that enable to convert the value to lower
216
- case before hashing that value before generate ID.
217
- :param unique: (bool) A flag that add timestamp at microsecond level to
218
- value before hashing.
219
- :param simple_mode: (bool | None) A flag for generate ID by simple mode.
220
- :param extras: (DictData) An extra parameter that use for override config
221
- value.
222
-
223
- :rtype: str
226
+ """Generate running ID for tracking purposes.
227
+
228
+ This function uses MD5 algorithm if simple mode is disabled, or cuts the
229
+ hashing value length to 10 if simple mode is enabled.
230
+
231
+ Simple Mode Format:
232
+ YYYYMMDDHHMMSSffffffTxxxxxxxxxx
233
+ year month day hour minute second microsecond sep simple-id
234
+
235
+ Args:
236
+ value: A value to add as prefix before hashing with MD5.
237
+ sensitive: Flag to convert value to lowercase before hashing.
238
+ unique: Flag to add timestamp at microsecond level before hashing.
239
+ simple_mode: Flag to generate ID using simple mode.
240
+ extras: Extra parameters to override config values.
241
+
242
+ Returns:
243
+ str: Generated unique identifier.
224
244
  """
225
245
  from .conf import dynamic
226
246
 
@@ -242,31 +262,37 @@ def gen_id(
242
262
 
243
263
 
244
264
  def default_gen_id() -> str:
245
- """Return running ID which use for making default ID for the Result model if
246
- a run_id field initializes at the first time.
265
+ """Return running ID for making default ID for the Result model.
266
+
267
+ This function is used when a run_id field is initialized for the first time.
247
268
 
248
- :rtype: str
269
+ Returns:
270
+ str: Generated default running ID.
249
271
  """
250
- return gen_id("manual", unique=True)
272
+ return gen_id("MOCK", unique=True)
251
273
 
252
274
 
253
275
  def make_exec(path: Union[Path, str]) -> None:
254
- """Change mode of file to be executable file.
276
+ """Change file mode to be executable.
255
277
 
256
- :param path: (Path | str) A file path that want to make executable
257
- permission.
278
+ Args:
279
+ path: A file path to make executable.
258
280
  """
259
281
  f: Path = Path(path) if isinstance(path, str) else path
260
282
  f.chmod(f.stat().st_mode | stat.S_IEXEC)
261
283
 
262
284
 
263
285
  def filter_func(value: T) -> T:
264
- """Filter out an own created function of any value of mapping context by
265
- replacing it to its function name. If it is built-in function, it does not
266
- have any changing.
286
+ """Filter out custom functions from mapping context by replacing with function names.
287
+
288
+ This function replaces custom functions with their function names in data
289
+ structures. Built-in functions remain unchanged.
267
290
 
268
- :param value: A value context data that want to filter out function value.
269
- :type: The same type of input ``value``.
291
+ Args:
292
+ value: A value or data structure to filter function values from.
293
+
294
+ Returns:
295
+ T: The filtered value with functions replaced by their names.
270
296
  """
271
297
  if isinstance(value, dict):
272
298
  return {k: filter_func(value[k]) for k in value}
@@ -287,11 +313,13 @@ def filter_func(value: T) -> T:
287
313
 
288
314
 
289
315
  def cross_product(matrix: Matrix) -> Iterator[DictData]:
290
- """Iterator of products value from matrix.
316
+ """Generate iterator of product values from matrix.
291
317
 
292
- :param matrix: (Matrix)
318
+ Args:
319
+ matrix: A matrix to generate cross products from.
293
320
 
294
- :rtype: Iterator[DictData]
321
+ Returns:
322
+ Iterator[DictData]: Iterator of dictionary combinations.
295
323
  """
296
324
  yield from (
297
325
  {_k: _v for e in mapped for _k, _v in e.items()}
@@ -302,16 +330,18 @@ def cross_product(matrix: Matrix) -> Iterator[DictData]:
302
330
 
303
331
 
304
332
  def cut_id(run_id: str, *, num: int = 6) -> str:
305
- """Cutting running ID with length.
333
+ """Cut running ID to specified length.
306
334
 
307
335
  Example:
308
336
  >>> cut_id(run_id='20240101081330000000T1354680202')
309
337
  '202401010813680202'
310
338
 
311
- :param run_id: (str) A running ID That want to cut.
312
- :param num: (int) A number of cutting length.
339
+ Args:
340
+ run_id: A running ID to cut.
341
+ num: Number of characters to keep from the end.
313
342
 
314
- :rtype: str
343
+ Returns:
344
+ str: The cut running ID.
315
345
  """
316
346
  if "T" in run_id:
317
347
  dt, simple = run_id.split("T", maxsplit=1)
@@ -333,10 +363,14 @@ def dump_all(
333
363
  value: Union[T, BaseModel],
334
364
  by_alias: bool = False,
335
365
  ) -> Union[T, DictData]:
336
- """Dump all nested BaseModel object to dict object.
366
+ """Dump all nested BaseModel objects to dictionary objects.
337
367
 
338
- :param value: (T | BaseModel)
339
- :param by_alias: (bool)
368
+ Args:
369
+ value: A value that may contain BaseModel objects.
370
+ by_alias: Whether to use field aliases when dumping.
371
+
372
+ Returns:
373
+ Union[T, DictData]: The value with BaseModel objects converted to dictionaries.
340
374
  """
341
375
  if isinstance(value, dict):
342
376
  return {k: dump_all(value[k], by_alias=by_alias) for k in value}
@@ -351,8 +385,16 @@ def dump_all(
351
385
 
352
386
 
353
387
  def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
388
+ """Get object name or class name.
389
+
390
+ Args:
391
+ obj: An object or string to get the name from.
392
+
393
+ Returns:
394
+ Optional[str]: The object name, class name, or None if obj is None.
395
+ """
354
396
  if not obj:
355
- obj_type: Optional[str] = None
397
+ return None
356
398
  elif isinstance(obj, str):
357
399
  obj_type: str = obj
358
400
  elif isclass(obj):
@@ -61,7 +61,7 @@ from .result import (
61
61
  validate_statuses,
62
62
  )
63
63
  from .reusables import has_template, param2template
64
- from .traces import Trace, get_trace
64
+ from .traces import TraceManager, get_trace
65
65
  from .utils import (
66
66
  UTC,
67
67
  gen_id,
@@ -145,7 +145,7 @@ class Workflow(BaseModel):
145
145
  description="A parameters that need to use on this workflow.",
146
146
  )
147
147
  on: Event = Field(
148
- default_factory=list,
148
+ default_factory=Event,
149
149
  description="An events for this workflow.",
150
150
  )
151
151
  jobs: dict[str, Job] = Field(
@@ -211,11 +211,6 @@ class Workflow(BaseModel):
211
211
  ```
212
212
  """
213
213
  load: YamlParser = YamlParser(name, path=path, extras=extras, obj=cls)
214
-
215
- # NOTE: Validate the config type match with current connection model
216
- if load.type != cls.__name__:
217
- raise ValueError(f"Type {load.type} does not match with {cls}")
218
-
219
214
  data: DictData = copy.deepcopy(load.data)
220
215
  data["name"] = name
221
216
 
@@ -289,6 +284,50 @@ class Workflow(BaseModel):
289
284
 
290
285
  return self
291
286
 
287
+ def detail(self) -> DictData: # pragma: no cov
288
+ """Return the detail of this workflow for generate markdown."""
289
+ return self.model_dump(by_alias=True)
290
+
291
+ def md(self, author: Optional[str] = None) -> str: # pragma: no cov
292
+ """Generate the markdown template."""
293
+
294
+ def align_newline(value: str) -> str:
295
+ return value.rstrip("\n").replace("\n", "\n ")
296
+
297
+ info: str = (
298
+ f"| Author: {author or 'nobody'} "
299
+ f"| created_at: `{self.created_at:%Y-%m-%d %H:%M:%S}` "
300
+ f"| updated_at: `{self.updated_dt:%Y-%m-%d %H:%M:%S}` |\n"
301
+ f"| --- | --- | --- |"
302
+ )
303
+ jobs: str = ""
304
+ for job in self.jobs:
305
+ job_model: Job = self.jobs[job]
306
+ jobs += f"### {job}\n{job_model.desc or ''}\n"
307
+ stags: str = ""
308
+ for stage_model in job_model.stages:
309
+ stags += (
310
+ f"#### {stage_model.name}\n\n"
311
+ f"Stage ID: {stage_model.id or ''}\n"
312
+ f"Stage Model: {stage_model.__class__.__name__}\n\n"
313
+ )
314
+ jobs += f"{stags}\n"
315
+ return dedent(
316
+ f"""
317
+ # Workflow: {self.name}\n
318
+ {align_newline(info)}\n
319
+ {align_newline(self.desc)}\n
320
+ ## Parameters\n
321
+ | name | type | default | description |
322
+ | --- | --- | --- | : --- : |
323
+
324
+ ## Jobs\n
325
+ {align_newline(jobs)}
326
+ """.lstrip(
327
+ "\n"
328
+ )
329
+ )
330
+
292
331
  def job(self, name: str) -> Job:
293
332
  """Return the workflow's Job model that getting by an input job's name
294
333
  or job's ID. This method will pass an extra parameter from this model
@@ -368,7 +407,7 @@ class Workflow(BaseModel):
368
407
  with the set `on` field.
369
408
 
370
409
  Args:
371
- dt: A datetime object that want to validate.
410
+ dt (datetime): A datetime object that want to validate.
372
411
 
373
412
  Returns:
374
413
  datetime: The validated release datetime.
@@ -377,7 +416,9 @@ class Workflow(BaseModel):
377
416
  dt = dt.replace(tzinfo=UTC)
378
417
 
379
418
  release: datetime = replace_sec(dt.astimezone(UTC))
380
- if not self.on:
419
+
420
+ # NOTE: Return itself if schedule event does not set.
421
+ if not self.on.schedule:
381
422
  return release
382
423
 
383
424
  for on in self.on.schedule:
@@ -415,7 +456,7 @@ class Workflow(BaseModel):
415
456
  - Writing result audit
416
457
 
417
458
  Args:
418
- release: (datetime) A release datetime.
459
+ release (datetime): A release datetime.
419
460
  params: A workflow parameter that pass to execute method.
420
461
  release_type:
421
462
  run_id: (str) A workflow running ID.
@@ -442,7 +483,7 @@ class Workflow(BaseModel):
442
483
  parent_run_id: str = run_id
443
484
 
444
485
  context: DictData = {"status": WAIT}
445
- trace: Trace = get_trace(
486
+ trace: TraceManager = get_trace(
446
487
  run_id, parent_run_id=parent_run_id, extras=self.extras
447
488
  )
448
489
  release: datetime = self.validate_release(dt=release)
@@ -537,7 +578,7 @@ class Workflow(BaseModel):
537
578
  Returns:
538
579
  tuple[Status, DictData]: The pair of status and result context data.
539
580
  """
540
- trace: Trace = get_trace(
581
+ trace: TraceManager = get_trace(
541
582
  run_id, parent_run_id=parent_run_id, extras=self.extras
542
583
  )
543
584
  if event and event.is_set():
@@ -654,7 +695,7 @@ class Workflow(BaseModel):
654
695
  ts: float = time.monotonic()
655
696
  parent_run_id: Optional[str] = run_id
656
697
  run_id: str = gen_id(self.name, extras=self.extras)
657
- trace: Trace = get_trace(
698
+ trace: TraceManager = get_trace(
658
699
  run_id, parent_run_id=parent_run_id, extras=self.extras
659
700
  )
660
701
  context: DictData = self.parameterize(params)
@@ -711,8 +752,12 @@ class Workflow(BaseModel):
711
752
 
712
753
  with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
713
754
  futures: list[Future] = []
714
- backoff_sleep = 0.01 # Start with smaller sleep time
715
- consecutive_waits = 0 # Track consecutive wait states
755
+
756
+ # NOTE: Start with smaller sleep time
757
+ backoff_sleep: float = 0.01
758
+
759
+ # NOTE: Track consecutive wait states
760
+ consecutive_waits: int = 0
716
761
 
717
762
  while not job_queue.empty() and (
718
763
  not_timeout_flag := ((time.monotonic() - ts) < timeout)
@@ -760,6 +805,7 @@ class Workflow(BaseModel):
760
805
  skip_count += 1
761
806
  continue
762
807
 
808
+ # IMPORTANT: Start execution with parallel mode.
763
809
  if max_job_parallel > 1:
764
810
  futures.append(
765
811
  executor.submit(
@@ -792,7 +838,9 @@ class Workflow(BaseModel):
792
838
  st, _ = future.result()
793
839
  sequence_statuses.append(st)
794
840
  job_queue.put(job_id)
795
- elif future.cancelled():
841
+ # NOTE: The release future can not track a cancelled status
842
+ # because it only has one future.
843
+ elif future.cancelled(): # pragma: no cov
796
844
  sequence_statuses.append(CANCEL)
797
845
  job_queue.put(job_id)
798
846
  elif future.running() or "state=pending" in str(future):
@@ -813,7 +861,7 @@ class Workflow(BaseModel):
813
861
  for total, future in enumerate(as_completed(futures), start=0):
814
862
  try:
815
863
  statuses[total], _ = future.result()
816
- except WorkflowError as e:
864
+ except (WorkflowError, Exception) as e:
817
865
  statuses[total] = get_status_from_error(e)
818
866
 
819
867
  # NOTE: Update skipped status from the job trigger.
@@ -871,7 +919,7 @@ class Workflow(BaseModel):
871
919
  event: Optional[ThreadEvent] = None,
872
920
  timeout: float = 3600,
873
921
  max_job_parallel: int = 2,
874
- ) -> Result:
922
+ ) -> Result: # pragma: no cov
875
923
  """Re-Execute workflow with passing the error context data.
876
924
 
877
925
  :param context: A context result that get the failed status.
@@ -892,7 +940,7 @@ class Workflow(BaseModel):
892
940
  ts: float = time.monotonic()
893
941
  parent_run_id: str = run_id
894
942
  run_id: str = gen_id(self.name, extras=self.extras)
895
- trace: Trace = get_trace(
943
+ trace: TraceManager = get_trace(
896
944
  run_id, parent_run_id=parent_run_id, extras=self.extras
897
945
  )
898
946
  if context["status"] == SUCCESS:
@@ -900,12 +948,9 @@ class Workflow(BaseModel):
900
948
  "[WORKFLOW]: Does not rerun because it already executed with "
901
949
  "success status."
902
950
  )
903
- return Result(
904
- run_id=run_id,
905
- parent_run_id=parent_run_id,
951
+ return Result.from_trace(trace).catch(
906
952
  status=SUCCESS,
907
953
  context=catch(context=context, status=SUCCESS),
908
- extras=self.extras,
909
954
  )
910
955
 
911
956
  err: dict[str, str] = context.get("errors", {})
@@ -921,12 +966,9 @@ class Workflow(BaseModel):
921
966
  )
922
967
  if not self.jobs:
923
968
  trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
924
- return Result(
925
- run_id=run_id,
926
- parent_run_id=parent_run_id,
969
+ return Result.from_trace(trace).catch(
927
970
  status=SUCCESS,
928
971
  context=catch(context=context, status=SUCCESS),
929
- extras=self.extras,
930
972
  )
931
973
 
932
974
  # NOTE: Prepare the new context variable for rerun process.
@@ -951,12 +993,9 @@ class Workflow(BaseModel):
951
993
  "[WORKFLOW]: It does not have job to rerun. it will change "
952
994
  "status to skip."
953
995
  )
954
- return Result(
955
- run_id=run_id,
956
- parent_run_id=parent_run_id,
996
+ return Result.from_trace(trace).catch(
957
997
  status=SKIP,
958
998
  context=catch(context=context, status=SKIP),
959
- extras=self.extras,
960
999
  )
961
1000
 
962
1001
  not_timeout_flag: bool = True
@@ -969,9 +1008,7 @@ class Workflow(BaseModel):
969
1008
 
970
1009
  catch(context, status=WAIT)
971
1010
  if event and event.is_set():
972
- return Result(
973
- run_id=run_id,
974
- parent_run_id=parent_run_id,
1011
+ return Result.from_trace(trace).catch(
975
1012
  status=CANCEL,
976
1013
  context=catch(
977
1014
  context,
@@ -983,7 +1020,6 @@ class Workflow(BaseModel):
983
1020
  ).to_dict(),
984
1021
  },
985
1022
  ),
986
- extras=self.extras,
987
1023
  )
988
1024
 
989
1025
  with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
@@ -1011,9 +1047,7 @@ class Workflow(BaseModel):
1011
1047
  backoff_sleep = 0.01
1012
1048
 
1013
1049
  if check == FAILED: # pragma: no cov
1014
- return Result(
1015
- run_id=run_id,
1016
- parent_run_id=parent_run_id,
1050
+ return Result.from_trace(trace).catch(
1017
1051
  status=FAILED,
1018
1052
  context=catch(
1019
1053
  context,
@@ -1026,7 +1060,6 @@ class Workflow(BaseModel):
1026
1060
  ).to_dict(),
1027
1061
  },
1028
1062
  ),
1029
- extras=self.extras,
1030
1063
  )
1031
1064
  elif check == SKIP: # pragma: no cov
1032
1065
  trace.info(
@@ -1102,12 +1135,9 @@ class Workflow(BaseModel):
1102
1135
  statuses[total + 1 + skip_count + i] = s
1103
1136
 
1104
1137
  st: Status = validate_statuses(statuses)
1105
- return Result(
1106
- run_id=run_id,
1107
- parent_run_id=parent_run_id,
1138
+ return Result.from_trace(trace).catch(
1108
1139
  status=st,
1109
1140
  context=catch(context, status=st),
1110
- extras=self.extras,
1111
1141
  )
1112
1142
 
1113
1143
  event.set()
@@ -1121,9 +1151,7 @@ class Workflow(BaseModel):
1121
1151
 
1122
1152
  time.sleep(0.0025)
1123
1153
 
1124
- return Result(
1125
- run_id=run_id,
1126
- parent_run_id=parent_run_id,
1154
+ return Result.from_trace(trace).catch(
1127
1155
  status=FAILED,
1128
1156
  context=catch(
1129
1157
  context,
@@ -1135,5 +1163,4 @@ class Workflow(BaseModel):
1135
1163
  ).to_dict(),
1136
1164
  },
1137
1165
  ),
1138
- extras=self.extras,
1139
1166
  )