griptape-nodes 0.70.1__py3-none-any.whl → 0.72.0__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 (67) hide show
  1. griptape_nodes/api_client/client.py +8 -5
  2. griptape_nodes/app/app.py +4 -0
  3. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
  4. griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
  5. griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
  6. griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
  7. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
  8. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
  9. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
  10. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
  13. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
  14. griptape_nodes/common/node_executor.py +61 -14
  15. griptape_nodes/drivers/image_metadata/__init__.py +21 -0
  16. griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
  17. griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
  18. griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
  19. griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
  20. griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
  21. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
  22. griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
  23. griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
  24. griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
  25. griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
  26. griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
  27. griptape_nodes/exe_types/node_types.py +13 -0
  28. griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
  29. griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
  30. griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
  31. griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
  32. griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
  33. griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
  34. griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
  35. griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
  36. griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
  37. griptape_nodes/node_library/workflow_registry.py +5 -8
  38. griptape_nodes/retained_mode/events/app_events.py +1 -0
  39. griptape_nodes/retained_mode/events/base_events.py +42 -26
  40. griptape_nodes/retained_mode/events/flow_events.py +67 -0
  41. griptape_nodes/retained_mode/events/library_events.py +1 -1
  42. griptape_nodes/retained_mode/events/node_events.py +1 -0
  43. griptape_nodes/retained_mode/events/os_events.py +22 -0
  44. griptape_nodes/retained_mode/events/static_file_events.py +28 -4
  45. griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
  46. griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
  47. griptape_nodes/retained_mode/managers/library_manager.py +71 -41
  48. griptape_nodes/retained_mode/managers/model_manager.py +1 -0
  49. griptape_nodes/retained_mode/managers/node_manager.py +8 -5
  50. griptape_nodes/retained_mode/managers/os_manager.py +270 -33
  51. griptape_nodes/retained_mode/managers/project_manager.py +3 -7
  52. griptape_nodes/retained_mode/managers/session_manager.py +1 -0
  53. griptape_nodes/retained_mode/managers/settings.py +5 -0
  54. griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
  55. griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
  56. griptape_nodes/servers/static.py +31 -0
  57. griptape_nodes/utils/__init__.py +9 -1
  58. griptape_nodes/utils/artifact_normalization.py +245 -0
  59. griptape_nodes/utils/file_utils.py +13 -13
  60. griptape_nodes/utils/http_file_patch.py +613 -0
  61. griptape_nodes/utils/image_preview.py +27 -0
  62. griptape_nodes/utils/path_utils.py +58 -0
  63. griptape_nodes/utils/url_utils.py +106 -0
  64. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
  65. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
  66. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
  67. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,245 @@
1
+ """Core utilities for normalizing artifact inputs (images, videos, audio).
2
+
3
+ This module provides normalization functions that convert string paths to
4
+ their respective artifact types (ImageUrlArtifact, VideoUrlArtifact, AudioUrlArtifact).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _resolve_localhost_url_to_path(url: str) -> str:
20
+ """Resolve localhost static file URLs to workspace file paths.
21
+
22
+ Converts URLs like http://localhost:8124/workspace/static_files/file.jpg
23
+ to actual workspace file paths like static_files/file.jpg
24
+
25
+ Args:
26
+ url: URL string that may be a localhost URL
27
+
28
+ Returns:
29
+ Resolved file path relative to workspace, or original string if not a localhost URL
30
+ """
31
+ if not isinstance(url, str):
32
+ return url
33
+
34
+ # Strip query parameters (cachebuster ?t=...)
35
+ if "?" in url:
36
+ url = url.split("?")[0]
37
+
38
+ # Check if it's a localhost URL (any port)
39
+ if url.startswith(("http://localhost:", "https://localhost:")):
40
+ parsed = urlparse(url)
41
+ # Extract path after /workspace/
42
+ if "/workspace/" in parsed.path:
43
+ workspace_relative_path = parsed.path.split("/workspace/", 1)[1]
44
+ return workspace_relative_path
45
+
46
+ # Not a localhost workspace URL, return as-is
47
+ return url
48
+
49
+
50
+ def _resolve_file_path(file_path: str) -> Path | None: # noqa: PLR0911
51
+ """Resolve file path to absolute path relative to workspace.
52
+
53
+ Args:
54
+ file_path: File path (may be absolute, relative, or localhost URL)
55
+
56
+ Returns:
57
+ Resolved Path object, or None if path cannot be resolved
58
+ """
59
+ # First resolve localhost URLs
60
+ file_path = _resolve_localhost_url_to_path(file_path)
61
+
62
+ # Get workspace path (can raise exceptions from ConfigManager)
63
+ try:
64
+ workspace_path = GriptapeNodes.ConfigManager().workspace_path
65
+ except (AttributeError, RuntimeError, KeyError) as e:
66
+ logger.debug("Failed to get workspace path: %s", e)
67
+ return None
68
+
69
+ # Create Path object (Path() constructor doesn't raise exceptions, but we validate)
70
+ path = Path(file_path)
71
+
72
+ # Check if path is absolute (is_absolute() doesn't raise exceptions)
73
+ if not path.is_absolute():
74
+ # Relative path - resolve relative to workspace (path operations don't raise exceptions)
75
+ return workspace_path / path
76
+
77
+ # Absolute path - check if relative to workspace
78
+ is_relative_to_workspace = False
79
+ try:
80
+ is_relative_to_workspace = path.is_relative_to(workspace_path)
81
+ except (ValueError, AttributeError):
82
+ # Path.is_relative_to() not available in older Python versions, use relative_to() instead
83
+ try:
84
+ path.relative_to(workspace_path)
85
+ is_relative_to_workspace = True
86
+ except ValueError:
87
+ # Absolute path outside workspace
88
+ is_relative_to_workspace = False
89
+ except (OSError, RuntimeError) as e:
90
+ # Unexpected errors from relative_to()
91
+ logger.debug("Unexpected error calling relative_to() for '%s': %s", file_path, e)
92
+ return None
93
+
94
+ if is_relative_to_workspace:
95
+ return path
96
+
97
+ # Absolute path outside workspace - return as-is (might be a system path)
98
+ # exists() can raise OSError or PermissionError
99
+ try:
100
+ path_exists = path.exists()
101
+ except (OSError, PermissionError) as e:
102
+ logger.debug("Failed to check if path exists for '%s': %s", file_path, e)
103
+ return None
104
+
105
+ if path_exists:
106
+ return path
107
+
108
+ return None
109
+
110
+
111
+ def _upload_file_to_static_storage(file_path: Path, artifact_type: type[Any]) -> Any | None:
112
+ """Upload a file to static storage and return an artifact.
113
+
114
+ Args:
115
+ file_path: Path to the file to upload
116
+ artifact_type: The artifact class to create (ImageUrlArtifact, VideoUrlArtifact, AudioUrlArtifact)
117
+
118
+ Returns:
119
+ Artifact object with localhost URL, or None if upload fails
120
+ """
121
+ if not file_path.exists() or not file_path.is_file():
122
+ return None
123
+
124
+ try:
125
+ file_data = file_path.read_bytes()
126
+ file_name = file_path.name
127
+ static_files_manager = GriptapeNodes.StaticFilesManager()
128
+ url = static_files_manager.save_static_file(file_data, file_name)
129
+ return artifact_type(url)
130
+ except Exception as e:
131
+ logger.debug("Failed to upload file '%s' to static storage: %s", file_path, e)
132
+ return None
133
+
134
+
135
+ def _normalize_string_input(artifact_input: str, artifact_type: type[Any]) -> Any: # noqa: PLR0911
136
+ """Normalize a string input to an artifact.
137
+
138
+ Args:
139
+ artifact_input: String input (URL or file path)
140
+ artifact_type: The artifact class to create
141
+
142
+ Returns:
143
+ Artifact object or original input if normalization fails
144
+ """
145
+ # If it's already a URL (http/https), return it as-is
146
+ if artifact_input.startswith(("http://", "https://")):
147
+ # Check if it's a localhost URL that needs resolving
148
+ if artifact_input.startswith(("http://localhost:", "https://localhost:")):
149
+ resolved_path = _resolve_localhost_url_to_path(artifact_input)
150
+ # If path wasn't resolved, return as URL artifact
151
+ if resolved_path == artifact_input:
152
+ return artifact_type(artifact_input)
153
+
154
+ # Try to resolve and upload the resolved path
155
+ file_path = _resolve_file_path(resolved_path)
156
+ if not file_path:
157
+ return artifact_type(artifact_input)
158
+
159
+ artifact = _upload_file_to_static_storage(file_path, artifact_type)
160
+ if not artifact:
161
+ return artifact_type(artifact_input)
162
+
163
+ # Success path: return the uploaded artifact
164
+ return artifact
165
+ # Regular URL, return as-is
166
+ return artifact_type(artifact_input)
167
+
168
+ # Try to resolve and upload file path
169
+ file_path = _resolve_file_path(artifact_input)
170
+ if file_path:
171
+ artifact = _upload_file_to_static_storage(file_path, artifact_type)
172
+ if artifact:
173
+ return artifact
174
+
175
+ return artifact_input
176
+
177
+
178
+ def normalize_artifact_input(
179
+ artifact_input: Any,
180
+ artifact_type: type[Any],
181
+ *,
182
+ accepted_types: tuple[type[Any], ...] | None = None,
183
+ ) -> Any:
184
+ """Normalize an artifact input, converting string paths to the specified artifact type.
185
+
186
+ This ensures consistency whether values come from user input or node connections.
187
+ String paths are uploaded to static storage and converted to artifact objects.
188
+ Objects that are already the correct artifact type are returned unchanged.
189
+
190
+ Args:
191
+ artifact_input: Artifact input (may be string, artifact object, etc.)
192
+ artifact_type: The artifact class to create (ImageUrlArtifact, VideoUrlArtifact, AudioUrlArtifact)
193
+ accepted_types: Optional tuple of artifact types that should be passed through unchanged.
194
+ For example, for images, both ImageUrlArtifact and ImageArtifact are valid.
195
+
196
+ Returns:
197
+ Artifact of the specified type if input was a string path, otherwise returns input unchanged
198
+ """
199
+ # Return unchanged if already the correct artifact type
200
+ if isinstance(artifact_input, artifact_type):
201
+ return artifact_input
202
+
203
+ # Also return unchanged if it's one of the accepted types (e.g., ImageArtifact for images)
204
+ if accepted_types and isinstance(artifact_input, accepted_types):
205
+ return artifact_input
206
+
207
+ # Process string paths
208
+ if isinstance(artifact_input, str) and artifact_input:
209
+ return _normalize_string_input(artifact_input, artifact_type)
210
+
211
+ return artifact_input
212
+
213
+
214
+ def normalize_artifact_list(
215
+ artifact_list: list[Any],
216
+ artifact_type: type[Any],
217
+ *,
218
+ accepted_types: tuple[type[Any], ...] | None = None,
219
+ ) -> list[Any]:
220
+ """Normalize a list of artifact inputs, converting string paths to the specified artifact type.
221
+
222
+ This ensures consistency whether values come from user input or node connections.
223
+ String paths are uploaded to static storage and converted to artifact objects.
224
+ Objects that are already the correct artifact type are passed through unchanged.
225
+
226
+ Args:
227
+ artifact_list: List of artifact inputs (may contain strings, artifact objects, etc.)
228
+ artifact_type: The artifact class to create (ImageUrlArtifact, VideoUrlArtifact, AudioUrlArtifact)
229
+ accepted_types: Optional tuple of artifact types that should be passed through unchanged.
230
+ For example, for images, both ImageUrlArtifact and ImageArtifact are valid.
231
+
232
+ Returns:
233
+ List with string paths converted to artifacts of the specified type
234
+ """
235
+ if not artifact_list:
236
+ return artifact_list
237
+
238
+ normalized_list = []
239
+ for item in artifact_list:
240
+ normalized_item = normalize_artifact_input(item, artifact_type, accepted_types=accepted_types)
241
+ normalized_list.append(normalized_item)
242
+ return normalized_list
243
+
244
+
245
+ __all__ = ["normalize_artifact_input", "normalize_artifact_list"]
@@ -101,7 +101,7 @@ def find_all_files_in_directory(directory: Path, pattern: str) -> list[Path]:
101
101
  return matches
102
102
 
103
103
 
104
- def find_files_recursive(directory: Path, pattern: str, *, skip_hidden: bool = True) -> set[Path]:
104
+ def find_files_recursive(directory: Path, pattern: str, *, skip_hidden: bool = True) -> list[Path]:
105
105
  """Search directory recursively for files matching pattern.
106
106
 
107
107
  Args:
@@ -111,38 +111,38 @@ def find_files_recursive(directory: Path, pattern: str, *, skip_hidden: bool = T
111
111
  This is more efficient when dealing with large hidden directories like .git, .venv, etc.
112
112
 
113
113
  Returns:
114
- Set of all matching file paths. Returns empty set if none found.
114
+ Sorted list of all matching file paths. Returns empty list if none found.
115
115
 
116
116
  Examples:
117
117
  >>> find_files_recursive(Path("/workspace"), "*.json")
118
- {Path("/workspace/a.json"), Path("/workspace/sub/b.json")}
118
+ [Path("/workspace/a.json"), Path("/workspace/sub/b.json")]
119
119
  >>> find_files_recursive(Path("/workspace"), "*.json", skip_hidden=False)
120
- {Path("/workspace/a.json"), Path("/workspace/.config/b.json")}
120
+ [Path("/workspace/.config/b.json"), Path("/workspace/a.json")]
121
121
  >>> find_files_recursive(Path("/empty"), "*.txt")
122
- set()
122
+ []
123
123
  """
124
124
  if not directory.exists():
125
125
  logger.debug("Directory does not exist: %s", directory)
126
- return set()
126
+ return []
127
127
 
128
128
  if not directory.is_dir():
129
129
  logger.debug("Path is not a directory: %s", directory)
130
- return set()
130
+ return []
131
131
 
132
- def _recurse(path: Path) -> set[Path]:
132
+ def _recurse(path: Path) -> list[Path]:
133
133
  """Recursively find files."""
134
- results = set()
134
+ results = []
135
135
  try:
136
- for item in path.iterdir():
136
+ for item in sorted(path.iterdir()):
137
137
  # Skip hidden files/directories if requested
138
138
  if skip_hidden and item.name.startswith("."):
139
139
  continue
140
140
 
141
141
  if item.is_file() and fnmatch(item.name, pattern):
142
- results.add(item)
142
+ results.append(item)
143
143
  elif item.is_dir():
144
144
  # Recurse into directories
145
- results.update(_recurse(item))
145
+ results.extend(_recurse(item))
146
146
  except (PermissionError, OSError) as e:
147
147
  # Skip directories we can't access
148
148
  logger.debug("Cannot access directory %s: %s", path, e)
@@ -156,4 +156,4 @@ def find_files_recursive(directory: Path, pattern: str, *, skip_hidden: bool = T
156
156
  else:
157
157
  logger.debug("Found %d file(s) matching pattern '%s' in directory: %s", len(matches), pattern, directory)
158
158
 
159
- return matches
159
+ return sorted(matches)