griptape-nodes 0.51.2__py3-none-any.whl → 0.52.1__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.
Files changed (46) hide show
  1. griptape_nodes/__init__.py +5 -4
  2. griptape_nodes/app/api.py +22 -30
  3. griptape_nodes/app/app.py +374 -289
  4. griptape_nodes/app/watch.py +17 -2
  5. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
  6. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
  7. griptape_nodes/exe_types/core_types.py +16 -4
  8. griptape_nodes/exe_types/node_types.py +74 -16
  9. griptape_nodes/machines/control_flow.py +21 -26
  10. griptape_nodes/machines/fsm.py +16 -16
  11. griptape_nodes/machines/node_resolution.py +30 -119
  12. griptape_nodes/mcp_server/server.py +14 -10
  13. griptape_nodes/mcp_server/ws_request_manager.py +2 -2
  14. griptape_nodes/node_library/workflow_registry.py +5 -0
  15. griptape_nodes/retained_mode/events/base_events.py +12 -7
  16. griptape_nodes/retained_mode/events/execution_events.py +0 -6
  17. griptape_nodes/retained_mode/events/node_events.py +38 -0
  18. griptape_nodes/retained_mode/events/parameter_events.py +11 -0
  19. griptape_nodes/retained_mode/events/variable_events.py +361 -0
  20. griptape_nodes/retained_mode/events/workflow_events.py +35 -0
  21. griptape_nodes/retained_mode/griptape_nodes.py +61 -26
  22. griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
  23. griptape_nodes/retained_mode/managers/event_manager.py +215 -74
  24. griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
  25. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
  26. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
  27. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
  28. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
  30. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +20 -19
  33. griptape_nodes/retained_mode/managers/node_manager.py +83 -8
  34. griptape_nodes/retained_mode/managers/object_manager.py +4 -0
  35. griptape_nodes/retained_mode/managers/settings.py +1 -0
  36. griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
  37. griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
  39. griptape_nodes/retained_mode/variable_types.py +18 -0
  40. griptape_nodes/utils/__init__.py +4 -0
  41. griptape_nodes/utils/async_utils.py +89 -0
  42. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/METADATA +2 -3
  43. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/RECORD +45 -42
  44. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/WHEEL +1 -1
  45. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
  46. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import asyncio
4
5
  import logging
5
6
  import pickle
6
7
  import pkgutil
@@ -57,6 +58,9 @@ from griptape_nodes.retained_mode.events.workflow_events import (
57
58
  ImportWorkflowAsReferencedSubFlowRequest,
58
59
  ImportWorkflowAsReferencedSubFlowResultFailure,
59
60
  ImportWorkflowAsReferencedSubFlowResultSuccess,
61
+ ImportWorkflowRequest,
62
+ ImportWorkflowResultFailure,
63
+ ImportWorkflowResultSuccess,
60
64
  ListAllWorkflowsRequest,
61
65
  ListAllWorkflowsResultFailure,
62
66
  ListAllWorkflowsResultSuccess,
@@ -271,6 +275,10 @@ class WorkflowManager:
271
275
  ImportWorkflowAsReferencedSubFlowRequest,
272
276
  self.on_import_workflow_as_referenced_sub_flow_request,
273
277
  )
278
+ event_manager.assign_manager_to_request_type(
279
+ ImportWorkflowRequest,
280
+ self.on_import_workflow_request,
281
+ )
274
282
  event_manager.assign_manager_to_request_type(
275
283
  BranchWorkflowRequest,
276
284
  self.on_branch_workflow_request,
@@ -621,6 +629,38 @@ class WorkflowManager:
621
629
  return RegisterWorkflowResultFailure(result_details=details)
622
630
  return RegisterWorkflowResultSuccess(workflow_name=workflow.metadata.name)
623
631
 
632
+ def on_import_workflow_request(self, request: ImportWorkflowRequest) -> ResultPayload:
633
+ # First, attempt to load metadata from the file
634
+ load_metadata_request = LoadWorkflowMetadata(file_name=request.file_path)
635
+ load_metadata_result = self.on_load_workflow_metadata_request(load_metadata_request)
636
+
637
+ if not isinstance(load_metadata_result, LoadWorkflowMetadataResultSuccess):
638
+ return ImportWorkflowResultFailure(result_details=load_metadata_result.result_details)
639
+
640
+ # Check if workflow is already registered
641
+ workflow_name = load_metadata_result.metadata.name
642
+ if WorkflowRegistry.has_workflow_with_name(workflow_name):
643
+ # Workflow already exists - no need to re-register
644
+ return ImportWorkflowResultSuccess(workflow_name=workflow_name)
645
+
646
+ # Now register the workflow with the extracted metadata
647
+ register_request = RegisterWorkflowRequest(metadata=load_metadata_result.metadata, file_name=request.file_path)
648
+ register_result = self.on_register_workflow_request(register_request)
649
+
650
+ if not isinstance(register_result, RegisterWorkflowResultSuccess):
651
+ return ImportWorkflowResultFailure(result_details=register_result.result_details)
652
+
653
+ # Add the workflow to the user configuration
654
+ try:
655
+ full_path = WorkflowRegistry.get_complete_file_path(request.file_path)
656
+ GriptapeNodes.ConfigManager().save_user_workflow_json(full_path)
657
+ except Exception as e:
658
+ details = f"Failed to add workflow '{register_result.workflow_name}' to user configuration: {e}"
659
+
660
+ return ImportWorkflowResultFailure(result_details=details)
661
+
662
+ return ImportWorkflowResultSuccess(workflow_name=register_result.workflow_name)
663
+
624
664
  def on_list_all_workflows_request(self, _request: ListAllWorkflowsRequest) -> ResultPayload:
625
665
  try:
626
666
  workflows = WorkflowRegistry.list_workflows()
@@ -1358,10 +1398,20 @@ class WorkflowManager:
1358
1398
  file_name = new_file_name
1359
1399
 
1360
1400
  # Get file name stuff prepped.
1361
- if not file_name:
1362
- file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
1363
- relative_file_path = f"{file_name}.py"
1364
- file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
1401
+ # Use the existing registered file path if this is an existing workflow (not a template)
1402
+ if prior_workflow and not prior_workflow.metadata.is_template:
1403
+ # Use the existing registered file path
1404
+ relative_file_path = prior_workflow.file_path
1405
+ file_path = Path(WorkflowRegistry.get_complete_file_path(relative_file_path))
1406
+ # Extract file name from the path for metadata generation
1407
+ if not file_name:
1408
+ file_name = prior_workflow.metadata.name
1409
+ else:
1410
+ # Create new path in workspace for new workflows or templates
1411
+ if not file_name:
1412
+ file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
1413
+ relative_file_path = f"{file_name}.py"
1414
+ file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
1365
1415
 
1366
1416
  # Generate the workflow file contents
1367
1417
  try:
@@ -1384,20 +1434,17 @@ class WorkflowManager:
1384
1434
  logger.error(details)
1385
1435
  return SaveWorkflowResultFailure(result_details=details)
1386
1436
 
1387
- relative_serialized_file_path = f"{file_name}.py"
1388
- serialized_file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_serialized_file_path)
1389
-
1390
1437
  # Check disk space before writing
1391
1438
  config_manager = GriptapeNodes.ConfigManager()
1392
1439
  min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
1393
- if not OSManager.check_available_disk_space(serialized_file_path.parent, min_space_gb):
1394
- error_msg = OSManager.format_disk_space_error(serialized_file_path.parent)
1440
+ if not OSManager.check_available_disk_space(file_path.parent, min_space_gb):
1441
+ error_msg = OSManager.format_disk_space_error(file_path.parent)
1395
1442
  details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
1396
1443
  logger.error(details)
1397
1444
  return SaveWorkflowResultFailure(result_details=details)
1398
1445
 
1399
1446
  try:
1400
- with serialized_file_path.open("w", encoding="utf-8") as file:
1447
+ with file_path.open("w", encoding="utf-8") as file:
1401
1448
  file.write(final_code_output)
1402
1449
  except OSError as e:
1403
1450
  details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
@@ -1407,19 +1454,21 @@ class WorkflowManager:
1407
1454
  # save the created workflow as an entry in the JSON config file.
1408
1455
  registered_workflows = WorkflowRegistry.list_workflows()
1409
1456
  if file_name not in registered_workflows:
1410
- try:
1411
- GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1412
- except OSError as e:
1413
- details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
1414
- logger.error(details)
1415
- return SaveWorkflowResultFailure(result_details=details)
1457
+ # Only add to config if it's in the workspace directory (not an external file)
1458
+ if not Path(relative_file_path).is_absolute():
1459
+ try:
1460
+ GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1461
+ except OSError as e:
1462
+ details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
1463
+ logger.error(details)
1464
+ return SaveWorkflowResultFailure(result_details=details)
1416
1465
  WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
1417
1466
  # Update existing workflow's metadata in the registry
1418
1467
  existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
1419
1468
  existing_workflow.metadata = workflow_metadata
1420
- details = f"Successfully saved workflow to: {serialized_file_path}"
1469
+ details = f"Successfully saved workflow to: {file_path}"
1421
1470
  logger.info(details)
1422
- return SaveWorkflowResultSuccess(file_path=str(serialized_file_path))
1471
+ return SaveWorkflowResultSuccess(file_path=str(file_path))
1423
1472
 
1424
1473
  def _generate_workflow_metadata( # noqa: PLR0913
1425
1474
  self,
@@ -1515,6 +1564,7 @@ class WorkflowManager:
1515
1564
 
1516
1565
  # === imports ===
1517
1566
  import_recorder.add_import("argparse")
1567
+ import_recorder.add_import("asyncio")
1518
1568
  import_recorder.add_import("json")
1519
1569
  import_recorder.add_from_import(
1520
1570
  "griptape_nodes.bootstrap.workflow_executors.local_workflow_executor", "LocalWorkflowExecutor"
@@ -1522,6 +1572,7 @@ class WorkflowManager:
1522
1572
  import_recorder.add_from_import(
1523
1573
  "griptape_nodes.bootstrap.workflow_executors.workflow_executor", "WorkflowExecutor"
1524
1574
  )
1575
+ import_recorder.add_from_import("griptape_nodes.drivers.storage.storage_backend", "StorageBackend")
1525
1576
 
1526
1577
  # === 1) build the `def execute_workflow(input: dict, storage_backend: str = StorageBackend.LOCAL, workflow_executor: WorkflowExecutor | None = None) -> dict | None:` ===
1527
1578
  # args
@@ -1554,6 +1605,16 @@ class WorkflowManager:
1554
1605
  # Generate the ensure flow context function call
1555
1606
  ensure_context_call = self._generate_ensure_flow_context_call()
1556
1607
 
1608
+ # Convert string storage_backend to StorageBackend enum
1609
+ storage_backend_convert = ast.Assign(
1610
+ targets=[ast.Name(id="storage_backend_enum", ctx=ast.Store())],
1611
+ value=ast.Call(
1612
+ func=ast.Name(id="StorageBackend", ctx=ast.Load()),
1613
+ args=[ast.Name(id="storage_backend", ctx=ast.Load())],
1614
+ keywords=[],
1615
+ ),
1616
+ )
1617
+
1557
1618
  # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor()
1558
1619
  executor_assign = ast.Assign(
1559
1620
  targets=[ast.Name(id="workflow_executor", ctx=ast.Store())],
@@ -1570,18 +1631,20 @@ class WorkflowManager:
1570
1631
  ),
1571
1632
  )
1572
1633
  run_call = ast.Expr(
1573
- value=ast.Call(
1574
- func=ast.Attribute(
1575
- value=ast.Name(id="workflow_executor", ctx=ast.Load()),
1576
- attr="run",
1577
- ctx=ast.Load(),
1578
- ),
1579
- args=[],
1580
- keywords=[
1581
- ast.keyword(arg="workflow_name", value=ast.Constant(flow_name)),
1582
- ast.keyword(arg="flow_input", value=ast.Name(id="input", ctx=ast.Load())),
1583
- ast.keyword(arg="storage_backend", value=ast.Name(id="storage_backend", ctx=ast.Load())),
1584
- ],
1634
+ value=ast.Await(
1635
+ value=ast.Call(
1636
+ func=ast.Attribute(
1637
+ value=ast.Name(id="workflow_executor", ctx=ast.Load()),
1638
+ attr="arun",
1639
+ ctx=ast.Load(),
1640
+ ),
1641
+ args=[],
1642
+ keywords=[
1643
+ ast.keyword(arg="workflow_name", value=ast.Constant(flow_name)),
1644
+ ast.keyword(arg="flow_input", value=ast.Name(id="input", ctx=ast.Load())),
1645
+ ast.keyword(arg="storage_backend", value=ast.Name(id="storage_backend_enum", ctx=ast.Load())),
1646
+ ],
1647
+ )
1585
1648
  )
1586
1649
  )
1587
1650
  return_stmt = ast.Return(
@@ -1592,15 +1655,53 @@ class WorkflowManager:
1592
1655
  )
1593
1656
  )
1594
1657
 
1595
- func_def = ast.FunctionDef(
1658
+ # === Generate async aexecute_workflow function ===
1659
+ async_func_def = ast.AsyncFunctionDef(
1660
+ name="aexecute_workflow",
1661
+ args=args,
1662
+ body=[ensure_context_call, storage_backend_convert, executor_assign, run_call, return_stmt],
1663
+ decorator_list=[],
1664
+ returns=return_annotation,
1665
+ type_params=[],
1666
+ )
1667
+ ast.fix_missing_locations(async_func_def)
1668
+
1669
+ # === Generate sync execute_workflow function (backward compatibility wrapper) ===
1670
+ sync_func_def = ast.FunctionDef(
1596
1671
  name="execute_workflow",
1597
1672
  args=args,
1598
- body=[ensure_context_call, executor_assign, run_call, return_stmt],
1673
+ body=[
1674
+ ast.Return(
1675
+ value=ast.Call(
1676
+ func=ast.Attribute(
1677
+ value=ast.Name(id="asyncio", ctx=ast.Load()),
1678
+ attr="run",
1679
+ ctx=ast.Load(),
1680
+ ),
1681
+ args=[
1682
+ ast.Call(
1683
+ func=ast.Name(id="aexecute_workflow", ctx=ast.Load()),
1684
+ args=[],
1685
+ keywords=[
1686
+ ast.keyword(arg="input", value=ast.Name(id="input", ctx=ast.Load())),
1687
+ ast.keyword(
1688
+ arg="storage_backend", value=ast.Name(id="storage_backend", ctx=ast.Load())
1689
+ ),
1690
+ ast.keyword(
1691
+ arg="workflow_executor", value=ast.Name(id="workflow_executor", ctx=ast.Load())
1692
+ ),
1693
+ ],
1694
+ )
1695
+ ],
1696
+ keywords=[],
1697
+ )
1698
+ )
1699
+ ],
1599
1700
  decorator_list=[],
1600
1701
  returns=return_annotation,
1601
1702
  type_params=[],
1602
1703
  )
1603
- ast.fix_missing_locations(func_def)
1704
+ ast.fix_missing_locations(sync_func_def)
1604
1705
 
1605
1706
  # === 2) build the `if __name__ == "__main__":` block ===
1606
1707
  main_test = ast.Compare(
@@ -1890,7 +1991,7 @@ class WorkflowManager:
1890
1991
  # Generate the ensure flow context function
1891
1992
  ensure_context_func = self._generate_ensure_flow_context_function(import_recorder)
1892
1993
 
1893
- return [ensure_context_func, func_def, if_node]
1994
+ return [ensure_context_func, sync_func_def, async_func_def, if_node]
1894
1995
 
1895
1996
  def _generate_ensure_flow_context_function(
1896
1997
  self,
@@ -2094,6 +2195,9 @@ class WorkflowManager:
2094
2195
  import_recorder.add_from_import(
2095
2196
  "griptape_nodes.retained_mode.events.library_events", "GetAllInfoForAllLibrariesResultSuccess"
2096
2197
  )
2198
+ import_recorder.add_from_import(
2199
+ "griptape_nodes.retained_mode.events.library_events", "ReloadAllLibrariesRequest"
2200
+ )
2097
2201
 
2098
2202
  code_blocks: list[ast.AST] = []
2099
2203
 
@@ -2168,24 +2272,22 @@ class WorkflowManager:
2168
2272
  )
2169
2273
  ast.fix_missing_locations(test)
2170
2274
 
2171
- # 3) the body: GriptapeNodes.LibraryManager().load_all_libraries_from_config()
2275
+ # 3) the body: GriptapeNodes.handle_request(ReloadAllLibrariesRequest())
2172
2276
  # TODO (https://github.com/griptape-ai/griptape-nodes/issues/1615): Generate requests to load ONLY the libraries used in this workflow
2173
2277
  load_call = ast.Expr(
2174
2278
  value=ast.Call(
2175
2279
  func=ast.Attribute(
2176
- value=ast.Call(
2177
- func=ast.Attribute(
2178
- value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
2179
- attr="LibraryManager",
2180
- ctx=ast.Load(),
2181
- ),
2182
- args=[],
2183
- keywords=[],
2184
- ),
2185
- attr="load_all_libraries_from_config",
2280
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
2281
+ attr="handle_request",
2186
2282
  ctx=ast.Load(),
2187
2283
  ),
2188
- args=[],
2284
+ args=[
2285
+ ast.Call(
2286
+ func=ast.Name(id="ReloadAllLibrariesRequest", ctx=ast.Load()),
2287
+ args=[],
2288
+ keywords=[],
2289
+ )
2290
+ ],
2189
2291
  keywords=[],
2190
2292
  )
2191
2293
  )
@@ -2954,7 +3056,8 @@ class WorkflowManager:
2954
3056
  set_parameter_value_asts.append(with_node_context)
2955
3057
  return set_parameter_value_asts
2956
3058
 
2957
- def _convert_parameter_to_minimal_dict(self, parameter: Parameter) -> dict[str, Any]:
3059
+ @classmethod
3060
+ def _convert_parameter_to_minimal_dict(cls, parameter: Parameter) -> dict[str, Any]:
2958
3061
  """Converts a parameter to a minimal dictionary for loading up a dynamic, black-box Node."""
2959
3062
  param_dict = parameter.to_dict()
2960
3063
  fields_to_include = [
@@ -2973,9 +3076,12 @@ class WorkflowManager:
2973
3076
  "traits",
2974
3077
  "ui_options",
2975
3078
  "settable",
2976
- "user_defined",
3079
+ "is_user_defined",
2977
3080
  ]
2978
3081
  minimal_dict = {key: param_dict[key] for key in fields_to_include if key in param_dict}
3082
+ minimal_dict["settable"] = bool(getattr(parameter, "settable", True))
3083
+ minimal_dict["is_user_defined"] = bool(getattr(param_dict, "is_user_defined", True))
3084
+
2979
3085
  return minimal_dict
2980
3086
 
2981
3087
  def _create_workflow_shape_from_nodes(
@@ -3053,7 +3159,7 @@ class WorkflowManager:
3053
3159
 
3054
3160
  return workflow_shape
3055
3161
 
3056
- def on_publish_workflow_request(self, request: PublishWorkflowRequest) -> ResultPayload:
3162
+ async def on_publish_workflow_request(self, request: PublishWorkflowRequest) -> ResultPayload:
3057
3163
  try:
3058
3164
  publisher_name = request.publisher_name
3059
3165
  event_handler_mappings = GriptapeNodes.LibraryManager().get_registered_event_handlers(
@@ -3065,7 +3171,7 @@ class WorkflowManager:
3065
3171
  msg = f"No publishing handler found for '{publisher_name}' in request type '{type(request).__name__}'."
3066
3172
  raise ValueError(msg) # noqa: TRY301
3067
3173
 
3068
- result = publishing_handler.handler(request)
3174
+ result = await asyncio.to_thread(publishing_handler.handler, request)
3069
3175
  if isinstance(result, PublishWorkflowResultSuccess):
3070
3176
  file = Path(result.published_workflow_file_path)
3071
3177
  self._register_published_workflow_file(file)
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+ from typing import Any
4
+
5
+
6
+ class VariableScope(StrEnum):
7
+ CURRENT_FLOW_ONLY = "current_flow_only"
8
+ HIERARCHICAL = "hierarchical"
9
+ GLOBAL_ONLY = "global_only"
10
+ ALL = "all" # For ListVariables to get all variables from all flows
11
+
12
+
13
+ @dataclass
14
+ class FlowVariable:
15
+ name: str
16
+ owning_flow_name: str | None # None for global variables
17
+ type: str
18
+ value: Any
@@ -1 +1,5 @@
1
1
  """Various utility functions."""
2
+
3
+ from griptape_nodes.utils.async_utils import call_function
4
+
5
+ __all__ = ["call_function"]
@@ -0,0 +1,89 @@
1
+ """Utilities for handling async/sync callback patterns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import subprocess
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable, Sequence
12
+
13
+
14
+ async def call_function(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
15
+ """Call a function, handling both sync and async cases.
16
+
17
+ Args:
18
+ func: The function to call (sync or async)
19
+ *args: Positional arguments to pass to the function
20
+ **kwargs: Keyword arguments to pass to the function
21
+
22
+ Returns:
23
+ The result of the function call
24
+ """
25
+ if inspect.iscoroutinefunction(func):
26
+ return await func(*args, **kwargs)
27
+ return func(*args, **kwargs)
28
+
29
+
30
+ async def subprocess_run(
31
+ args: Sequence[str],
32
+ *,
33
+ capture_output: bool = False,
34
+ text: bool = False,
35
+ check: bool = False,
36
+ ) -> subprocess.CompletedProcess[str | bytes]:
37
+ """Run a subprocess asynchronously with an interface similar to subprocess.run().
38
+
39
+ Args:
40
+ args: Command and arguments to execute
41
+ capture_output: Whether to capture stdout and stderr
42
+ text: Whether to decode output as text
43
+ check: Whether to raise CalledProcessError on non-zero exit
44
+
45
+ Returns:
46
+ CompletedProcess with the result
47
+
48
+ Raises:
49
+ subprocess.CalledProcessError: If check=True and the process exits with non-zero code
50
+ """
51
+ if capture_output:
52
+ stdout_arg = asyncio.subprocess.PIPE
53
+ stderr_arg = asyncio.subprocess.PIPE
54
+ else:
55
+ stdout_arg = None
56
+ stderr_arg = None
57
+
58
+ process = await asyncio.create_subprocess_exec(
59
+ *args,
60
+ stdout=stdout_arg,
61
+ stderr=stderr_arg,
62
+ )
63
+
64
+ stdout_bytes, stderr_bytes = await process.communicate()
65
+
66
+ # Convert bytes to string if text=True
67
+ if text:
68
+ stdout = stdout_bytes.decode() if stdout_bytes else ""
69
+ stderr = stderr_bytes.decode() if stderr_bytes else ""
70
+ else:
71
+ stdout = stdout_bytes if stdout_bytes else b""
72
+ stderr = stderr_bytes if stderr_bytes else b""
73
+
74
+ completed_process = subprocess.CompletedProcess(
75
+ args=list(args),
76
+ returncode=process.returncode or 0,
77
+ stdout=stdout,
78
+ stderr=stderr,
79
+ )
80
+
81
+ if check and completed_process.returncode != 0:
82
+ raise subprocess.CalledProcessError(
83
+ completed_process.returncode,
84
+ args,
85
+ completed_process.stdout,
86
+ completed_process.stderr,
87
+ )
88
+
89
+ return completed_process
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.51.2
3
+ Version: 0.52.1
4
4
  Summary: Add your description here
5
- Requires-Dist: griptape>=1.8.0
5
+ Requires-Dist: griptape>=1.8.2
6
6
  Requires-Dist: pydantic>=2.10.6
7
7
  Requires-Dist: python-dotenv>=1.0.1
8
8
  Requires-Dist: xdg-base-dirs>=6.0.2
@@ -15,7 +15,6 @@ Requires-Dist: uvicorn>=0.34.2
15
15
  Requires-Dist: packaging>=25.0
16
16
  Requires-Dist: python-multipart>=0.0.20
17
17
  Requires-Dist: json-repair>=0.46.1
18
- Requires-Dist: imageio-ffmpeg>=0.6.0
19
18
  Requires-Dist: mcp[ws]>=1.10.1
20
19
  Requires-Dist: binaryornot>=0.4.4
21
20
  Requires-Dist: pillow>=11.3.0