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.
- griptape_nodes/api_client/client.py +8 -5
- griptape_nodes/app/app.py +4 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
- griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
- griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
- griptape_nodes/common/node_executor.py +61 -14
- griptape_nodes/drivers/image_metadata/__init__.py +21 -0
- griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
- griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
- griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
- griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
- griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
- griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
- griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
- griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
- griptape_nodes/exe_types/node_types.py +13 -0
- griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
- griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
- griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
- griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
- griptape_nodes/node_library/workflow_registry.py +5 -8
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +42 -26
- griptape_nodes/retained_mode/events/flow_events.py +67 -0
- griptape_nodes/retained_mode/events/library_events.py +1 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/os_events.py +22 -0
- griptape_nodes/retained_mode/events/static_file_events.py +28 -4
- griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
- griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
- griptape_nodes/retained_mode/managers/library_manager.py +71 -41
- griptape_nodes/retained_mode/managers/model_manager.py +1 -0
- griptape_nodes/retained_mode/managers/node_manager.py +8 -5
- griptape_nodes/retained_mode/managers/os_manager.py +270 -33
- griptape_nodes/retained_mode/managers/project_manager.py +3 -7
- griptape_nodes/retained_mode/managers/session_manager.py +1 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
- griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
- griptape_nodes/servers/static.py +31 -0
- griptape_nodes/utils/__init__.py +9 -1
- griptape_nodes/utils/artifact_normalization.py +245 -0
- griptape_nodes/utils/file_utils.py +13 -13
- griptape_nodes/utils/http_file_patch.py +613 -0
- griptape_nodes/utils/image_preview.py +27 -0
- griptape_nodes/utils/path_utils.py +58 -0
- griptape_nodes/utils/url_utils.py +106 -0
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
- {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) ->
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
[Path("/workspace/a.json"), Path("/workspace/sub/b.json")]
|
|
119
119
|
>>> find_files_recursive(Path("/workspace"), "*.json", skip_hidden=False)
|
|
120
|
-
|
|
120
|
+
[Path("/workspace/.config/b.json"), Path("/workspace/a.json")]
|
|
121
121
|
>>> find_files_recursive(Path("/empty"), "*.txt")
|
|
122
|
-
|
|
122
|
+
[]
|
|
123
123
|
"""
|
|
124
124
|
if not directory.exists():
|
|
125
125
|
logger.debug("Directory does not exist: %s", directory)
|
|
126
|
-
return
|
|
126
|
+
return []
|
|
127
127
|
|
|
128
128
|
if not directory.is_dir():
|
|
129
129
|
logger.debug("Path is not a directory: %s", directory)
|
|
130
|
-
return
|
|
130
|
+
return []
|
|
131
131
|
|
|
132
|
-
def _recurse(path: Path) ->
|
|
132
|
+
def _recurse(path: Path) -> list[Path]:
|
|
133
133
|
"""Recursively find files."""
|
|
134
|
-
results =
|
|
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.
|
|
142
|
+
results.append(item)
|
|
143
143
|
elif item.is_dir():
|
|
144
144
|
# Recurse into directories
|
|
145
|
-
results.
|
|
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)
|