griptape-nodes 0.60.3__py3-none-any.whl → 0.61.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 (47) hide show
  1. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
  2. griptape_nodes/common/macro_parser/__init__.py +16 -1
  3. griptape_nodes/common/macro_parser/core.py +15 -3
  4. griptape_nodes/common/macro_parser/exceptions.py +99 -0
  5. griptape_nodes/common/macro_parser/formats.py +13 -4
  6. griptape_nodes/common/macro_parser/matching.py +5 -2
  7. griptape_nodes/common/macro_parser/parsing.py +48 -8
  8. griptape_nodes/common/macro_parser/resolution.py +23 -5
  9. griptape_nodes/common/project_templates/__init__.py +49 -0
  10. griptape_nodes/common/project_templates/default_project_template.py +92 -0
  11. griptape_nodes/common/project_templates/defaults/README.md +36 -0
  12. griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
  13. griptape_nodes/common/project_templates/directory.py +67 -0
  14. griptape_nodes/common/project_templates/loader.py +341 -0
  15. griptape_nodes/common/project_templates/project.py +252 -0
  16. griptape_nodes/common/project_templates/situation.py +155 -0
  17. griptape_nodes/common/project_templates/validation.py +140 -0
  18. griptape_nodes/exe_types/core_types.py +36 -3
  19. griptape_nodes/exe_types/node_types.py +4 -2
  20. griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
  21. griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
  22. griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
  23. griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
  24. griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
  25. griptape_nodes/node_library/workflow_registry.py +1 -1
  26. griptape_nodes/retained_mode/events/execution_events.py +41 -0
  27. griptape_nodes/retained_mode/events/node_events.py +90 -1
  28. griptape_nodes/retained_mode/events/os_events.py +108 -0
  29. griptape_nodes/retained_mode/events/parameter_events.py +1 -1
  30. griptape_nodes/retained_mode/events/project_events.py +413 -0
  31. griptape_nodes/retained_mode/events/workflow_events.py +19 -1
  32. griptape_nodes/retained_mode/griptape_nodes.py +9 -1
  33. griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
  34. griptape_nodes/retained_mode/managers/event_manager.py +6 -9
  35. griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
  36. griptape_nodes/retained_mode/managers/library_manager.py +55 -42
  37. griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
  38. griptape_nodes/retained_mode/managers/node_manager.py +232 -0
  39. griptape_nodes/retained_mode/managers/os_manager.py +346 -1
  40. griptape_nodes/retained_mode/managers/project_manager.py +617 -0
  41. griptape_nodes/retained_mode/managers/settings.py +6 -0
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +17 -71
  43. griptape_nodes/traits/button.py +18 -0
  44. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
  45. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
  46. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
  47. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
@@ -15,6 +15,12 @@ from rich.console import Console
15
15
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
16
16
  from griptape_nodes.retained_mode.events.base_events import ResultDetails, ResultPayload
17
17
  from griptape_nodes.retained_mode.events.os_events import (
18
+ CopyFileRequest,
19
+ CopyFileResultFailure,
20
+ CopyFileResultSuccess,
21
+ CopyTreeRequest,
22
+ CopyTreeResultFailure,
23
+ CopyTreeResultSuccess,
18
24
  CreateFileRequest,
19
25
  CreateFileResultFailure,
20
26
  CreateFileResultSuccess,
@@ -73,6 +79,24 @@ class FileContentResult(NamedTuple):
73
79
  file_size: int
74
80
 
75
81
 
82
+ @dataclass
83
+ class CopyTreeValidationResult:
84
+ """Result from validating copy tree paths."""
85
+
86
+ source_normalized: str
87
+ dest_normalized: str
88
+ source_path: Path
89
+ destination_path: Path
90
+
91
+
92
+ @dataclass
93
+ class CopyTreeStats:
94
+ """Statistics from copying a directory tree."""
95
+
96
+ files_copied: int
97
+ total_bytes_copied: int
98
+
99
+
76
100
  class OSManager:
77
101
  """A class to manage OS-level scenarios.
78
102
 
@@ -105,6 +129,14 @@ class OSManager:
105
129
  request_type=WriteFileRequest, callback=self.on_write_file_request
106
130
  )
107
131
 
132
+ event_manager.assign_manager_to_request_type(
133
+ request_type=CopyTreeRequest, callback=self.on_copy_tree_request
134
+ )
135
+
136
+ event_manager.assign_manager_to_request_type(
137
+ request_type=CopyFileRequest, callback=self.on_copy_file_request
138
+ )
139
+
108
140
  # Register for app initialization event to setup system resources
109
141
  event_manager.add_listener_to_app_event(AppInitializationComplete, self.on_app_initialization_complete)
110
142
 
@@ -194,7 +226,7 @@ class OSManager:
194
226
  path_str = str(path.resolve())
195
227
 
196
228
  # Windows long path handling (paths > WINDOWS_MAX_PATH chars need \\?\ prefix)
197
- if self.is_windows() and len(path_str) > WINDOWS_MAX_PATH and not path_str.startswith("\\\\?\\"):
229
+ if self.is_windows() and len(path_str) >= WINDOWS_MAX_PATH and not path_str.startswith("\\\\?\\"):
198
230
  # UNC paths (\\server\share) need \\?\UNC\ prefix
199
231
  if path_str.startswith("\\\\"):
200
232
  return f"\\\\?\\UNC\\{path_str[2:]}"
@@ -757,6 +789,30 @@ class OSManager:
757
789
  result_details=f"File written successfully: {file_path}",
758
790
  )
759
791
 
792
+ def _copy_file(self, src_path: Path, dest_path: Path) -> int:
793
+ """Copy a single file from source to destination with platform path normalization.
794
+
795
+ Args:
796
+ src_path: Source file path (Path object)
797
+ dest_path: Destination file path (Path object)
798
+
799
+ Returns:
800
+ Number of bytes copied
801
+
802
+ Raises:
803
+ OSError: If copy operation fails
804
+ PermissionError: If permission denied
805
+ """
806
+ # Normalize both paths for platform (handles Windows long paths)
807
+ src_normalized = self.normalize_path_for_platform(src_path)
808
+ dest_normalized = self.normalize_path_for_platform(dest_path)
809
+
810
+ # Copy file preserving metadata
811
+ shutil.copy2(src_normalized, dest_normalized)
812
+
813
+ # Return size of copied file
814
+ return os.path.getsize(src_normalized) # noqa: PTH202
815
+
760
816
  def _write_file_content(self, normalized_path: str, content: str | bytes, encoding: str, *, append: bool) -> int:
761
817
  """Write content to a file and return bytes written.
762
818
 
@@ -1135,6 +1191,295 @@ class OSManager:
1135
1191
  result_details=ResultDetails(message=details, level=logging.INFO),
1136
1192
  )
1137
1193
 
1194
+ def on_copy_file_request(self, request: CopyFileRequest) -> ResultPayload: # noqa: PLR0911, C901
1195
+ """Handle a request to copy a single file."""
1196
+ # Resolve source path
1197
+ try:
1198
+ source_path = self._resolve_file_path(request.source_path, workspace_only=False)
1199
+ source_normalized = self.normalize_path_for_platform(source_path)
1200
+ except (ValueError, RuntimeError) as e:
1201
+ msg = f"Invalid source path: {e}"
1202
+ logger.error(msg)
1203
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1204
+
1205
+ # Check if source exists
1206
+ if not Path(source_normalized).exists():
1207
+ msg = f"Source file does not exist: {source_path}"
1208
+ logger.error(msg)
1209
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.FILE_NOT_FOUND, result_details=msg)
1210
+
1211
+ # Check if source is a file (not a directory)
1212
+ if not Path(source_normalized).is_file():
1213
+ msg = f"Source path is not a file: {source_path}"
1214
+ logger.error(msg)
1215
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1216
+
1217
+ # Resolve destination path
1218
+ try:
1219
+ destination_path = self._resolve_file_path(request.destination_path, workspace_only=False)
1220
+ dest_normalized = self.normalize_path_for_platform(destination_path)
1221
+ except (ValueError, RuntimeError) as e:
1222
+ msg = f"Invalid destination path: {e}"
1223
+ logger.error(msg)
1224
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1225
+
1226
+ # Check if destination already exists (unless overwrite is True)
1227
+ if Path(dest_normalized).exists() and not request.overwrite:
1228
+ msg = f"Destination file already exists: {destination_path}"
1229
+ logger.error(msg)
1230
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1231
+
1232
+ # Create parent directory if it doesn't exist
1233
+ dest_parent = Path(dest_normalized).parent
1234
+ if not dest_parent.exists():
1235
+ try:
1236
+ dest_parent.mkdir(parents=True)
1237
+ except PermissionError as e:
1238
+ msg = f"Permission denied creating parent directory {dest_parent}: {e}"
1239
+ logger.error(msg)
1240
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
1241
+ except OSError as e:
1242
+ msg = f"I/O error creating parent directory {dest_parent}: {e}"
1243
+ logger.error(msg)
1244
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
1245
+
1246
+ # Copy the file
1247
+ try:
1248
+ bytes_copied = self._copy_file(source_path, destination_path)
1249
+ except PermissionError as e:
1250
+ msg = f"Permission denied copying {source_path} to {destination_path}: {e}"
1251
+ logger.error(msg)
1252
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
1253
+ except OSError as e:
1254
+ if "No space left" in str(e) or "Disk full" in str(e):
1255
+ msg = f"Disk full copying {source_path} to {destination_path}: {e}"
1256
+ logger.error(msg)
1257
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.DISK_FULL, result_details=msg)
1258
+
1259
+ msg = f"I/O error copying {source_path} to {destination_path}: {e}"
1260
+ logger.error(msg)
1261
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
1262
+ except Exception as e:
1263
+ msg = f"Unexpected error copying {source_path} to {destination_path}: {type(e).__name__}: {e}"
1264
+ logger.error(msg)
1265
+ return CopyFileResultFailure(failure_reason=FileIOFailureReason.UNKNOWN, result_details=msg)
1266
+
1267
+ # SUCCESS PATH
1268
+ return CopyFileResultSuccess(
1269
+ source_path=str(source_path),
1270
+ destination_path=str(destination_path),
1271
+ bytes_copied=bytes_copied,
1272
+ result_details=f"File copied successfully: {source_path} -> {destination_path}",
1273
+ )
1274
+
1275
+ def _validate_copy_tree_paths(
1276
+ self, source_str: str, dest_str: str, *, dirs_exist_ok: bool
1277
+ ) -> CopyTreeValidationResult | CopyTreeResultFailure:
1278
+ """Validate and normalize source and destination paths for copy tree operation.
1279
+
1280
+ Returns:
1281
+ CopyTreeValidationResult on success, CopyTreeResultFailure on validation failure
1282
+ """
1283
+ # Resolve and normalize source path
1284
+ try:
1285
+ source_path = self._resolve_file_path(source_str, workspace_only=False)
1286
+ source_normalized = self.normalize_path_for_platform(source_path)
1287
+ except (ValueError, RuntimeError) as e:
1288
+ msg = f"Invalid source path: {e}"
1289
+ logger.error(msg)
1290
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1291
+
1292
+ # Check if source exists
1293
+ if not Path(source_normalized).exists():
1294
+ msg = f"Source path does not exist: {source_path}"
1295
+ logger.error(msg)
1296
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.FILE_NOT_FOUND, result_details=msg)
1297
+
1298
+ # Check if source is a directory
1299
+ if not Path(source_normalized).is_dir():
1300
+ msg = f"Source path is not a directory: {source_path}"
1301
+ logger.error(msg)
1302
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1303
+
1304
+ # Resolve and normalize destination path
1305
+ try:
1306
+ destination_path = self._resolve_file_path(dest_str, workspace_only=False)
1307
+ dest_normalized = self.normalize_path_for_platform(destination_path)
1308
+ except (ValueError, RuntimeError) as e:
1309
+ msg = f"Invalid destination path: {e}"
1310
+ logger.error(msg)
1311
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1312
+
1313
+ # Check if destination already exists (unless dirs_exist_ok is True)
1314
+ if Path(dest_normalized).exists() and not dirs_exist_ok:
1315
+ msg = f"Destination path already exists: {destination_path}"
1316
+ logger.error(msg)
1317
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1318
+
1319
+ return CopyTreeValidationResult(
1320
+ source_normalized=source_normalized,
1321
+ dest_normalized=dest_normalized,
1322
+ source_path=source_path,
1323
+ destination_path=destination_path,
1324
+ )
1325
+
1326
+ def _copy_directory_tree( # noqa: PLR0912, C901
1327
+ self,
1328
+ source_normalized: str,
1329
+ dest_normalized: str,
1330
+ *,
1331
+ symlinks: bool,
1332
+ ignore_dangling_symlinks: bool,
1333
+ ignore_patterns: list[str] | None = None,
1334
+ ) -> CopyTreeStats:
1335
+ """Copy directory tree from source to destination.
1336
+
1337
+ Args:
1338
+ source_normalized: Normalized source path
1339
+ dest_normalized: Normalized destination path
1340
+ symlinks: If True, copy symbolic links as links
1341
+ ignore_dangling_symlinks: If True, ignore dangling symlinks
1342
+ ignore_patterns: List of glob patterns to ignore (e.g., ["__pycache__", "*.pyc"])
1343
+
1344
+ Returns:
1345
+ CopyTreeStats with files copied and bytes copied
1346
+
1347
+ Raises:
1348
+ OSError: If copy operation fails
1349
+ PermissionError: If permission denied
1350
+ """
1351
+ from fnmatch import fnmatch
1352
+
1353
+ files_copied = 0
1354
+ total_bytes_copied = 0
1355
+ ignore_patterns = ignore_patterns or []
1356
+
1357
+ def should_ignore(name: str) -> bool:
1358
+ """Check if a file/directory name matches any ignore pattern."""
1359
+ return any(fnmatch(name, pattern) for pattern in ignore_patterns)
1360
+
1361
+ # Create destination directory if it doesn't exist
1362
+ dest_path_obj = Path(dest_normalized)
1363
+ if not dest_path_obj.exists():
1364
+ dest_path_obj.mkdir(parents=True)
1365
+
1366
+ # Walk through source directory and copy files/directories
1367
+ for root, dirs, files in os.walk(source_normalized):
1368
+ # Calculate relative path from source
1369
+ root_path = Path(root)
1370
+ source_path_obj = Path(source_normalized)
1371
+ rel_path = root_path.relative_to(source_path_obj)
1372
+
1373
+ # Create corresponding directory in destination
1374
+ if str(rel_path) != ".":
1375
+ dest_dir = dest_path_obj / rel_path
1376
+ else:
1377
+ dest_dir = dest_path_obj
1378
+
1379
+ # Filter out ignored directories and create remaining ones
1380
+ dirs_to_remove = []
1381
+ for dir_name in dirs:
1382
+ if should_ignore(dir_name):
1383
+ dirs_to_remove.append(dir_name)
1384
+ continue
1385
+
1386
+ src_dir = root_path / dir_name
1387
+ dst_dir = dest_dir / dir_name
1388
+
1389
+ # Handle symlinks if requested
1390
+ if src_dir.is_symlink():
1391
+ if symlinks:
1392
+ link_target = src_dir.readlink()
1393
+ dst_dir.symlink_to(link_target)
1394
+ continue
1395
+
1396
+ if not dst_dir.exists():
1397
+ dst_dir.mkdir(parents=True)
1398
+
1399
+ # Remove ignored directories from dirs list to prevent os.walk from descending into them
1400
+ for dir_name in dirs_to_remove:
1401
+ dirs.remove(dir_name)
1402
+
1403
+ # Copy files
1404
+ for file_name in files:
1405
+ # Skip ignored files
1406
+ if should_ignore(file_name):
1407
+ continue
1408
+
1409
+ src_file = root_path / file_name
1410
+ dst_file = dest_dir / file_name
1411
+
1412
+ # Handle symlinks if requested
1413
+ if src_file.is_symlink():
1414
+ if symlinks:
1415
+ try:
1416
+ link_target = src_file.readlink()
1417
+ dst_file.symlink_to(link_target)
1418
+ except OSError:
1419
+ if not ignore_dangling_symlinks:
1420
+ raise
1421
+ continue
1422
+
1423
+ # Copy file
1424
+ bytes_copied = self._copy_file(src_file, dst_file)
1425
+ files_copied += 1
1426
+ total_bytes_copied += bytes_copied
1427
+
1428
+ return CopyTreeStats(files_copied=files_copied, total_bytes_copied=total_bytes_copied)
1429
+
1430
+ def on_copy_tree_request(self, request: CopyTreeRequest) -> ResultPayload:
1431
+ """Handle a request to copy a directory tree."""
1432
+ # Validate paths
1433
+ validation_result = self._validate_copy_tree_paths(
1434
+ request.source_path,
1435
+ request.destination_path,
1436
+ dirs_exist_ok=request.dirs_exist_ok,
1437
+ )
1438
+
1439
+ if isinstance(validation_result, CopyTreeResultFailure):
1440
+ return validation_result
1441
+
1442
+ source_normalized = validation_result.source_normalized
1443
+ dest_normalized = validation_result.dest_normalized
1444
+ source_path = validation_result.source_path
1445
+ destination_path = validation_result.destination_path
1446
+
1447
+ # Copy directory tree
1448
+ try:
1449
+ stats = self._copy_directory_tree(
1450
+ source_normalized,
1451
+ dest_normalized,
1452
+ symlinks=request.symlinks,
1453
+ ignore_dangling_symlinks=request.ignore_dangling_symlinks,
1454
+ ignore_patterns=request.ignore_patterns,
1455
+ )
1456
+ except PermissionError as e:
1457
+ msg = f"Permission denied copying {source_path} to {destination_path}: {e}"
1458
+ logger.error(msg)
1459
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
1460
+ except OSError as e:
1461
+ if "No space left" in str(e) or "Disk full" in str(e):
1462
+ msg = f"Disk full copying {source_path} to {destination_path}: {e}"
1463
+ logger.error(msg)
1464
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.DISK_FULL, result_details=msg)
1465
+
1466
+ msg = f"I/O error copying {source_path} to {destination_path}: {e}"
1467
+ logger.error(msg)
1468
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
1469
+ except Exception as e:
1470
+ msg = f"Unexpected error copying {source_path} to {destination_path}: {type(e).__name__}: {e}"
1471
+ logger.error(msg)
1472
+ return CopyTreeResultFailure(failure_reason=FileIOFailureReason.UNKNOWN, result_details=msg)
1473
+
1474
+ # SUCCESS PATH
1475
+ return CopyTreeResultSuccess(
1476
+ source_path=str(source_path),
1477
+ destination_path=str(destination_path),
1478
+ files_copied=stats.files_copied,
1479
+ total_bytes_copied=stats.total_bytes_copied,
1480
+ result_details=f"Directory tree copied successfully: {source_path} -> {destination_path}",
1481
+ )
1482
+
1138
1483
  def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
1139
1484
  """Handle app initialization complete event by registering system resources."""
1140
1485
  self._register_system_resources()