griptape-nodes 0.64.11__py3-none-any.whl → 0.65.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/app/app.py +25 -5
- griptape_nodes/cli/commands/init.py +65 -54
- griptape_nodes/cli/commands/libraries.py +92 -85
- griptape_nodes/cli/commands/self.py +121 -0
- griptape_nodes/common/node_executor.py +2142 -101
- griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
- griptape_nodes/exe_types/connections.py +114 -19
- griptape_nodes/exe_types/core_types.py +225 -7
- griptape_nodes/exe_types/flow.py +3 -3
- griptape_nodes/exe_types/node_types.py +681 -225
- griptape_nodes/exe_types/param_components/README.md +414 -0
- griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
- griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
- griptape_nodes/machines/control_flow.py +77 -38
- griptape_nodes/machines/dag_builder.py +148 -70
- griptape_nodes/machines/parallel_resolution.py +61 -35
- griptape_nodes/machines/sequential_resolution.py +11 -113
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +16 -13
- griptape_nodes/retained_mode/events/connection_events.py +3 -0
- griptape_nodes/retained_mode/events/execution_events.py +35 -0
- griptape_nodes/retained_mode/events/flow_events.py +15 -2
- griptape_nodes/retained_mode/events/library_events.py +347 -0
- griptape_nodes/retained_mode/events/node_events.py +48 -0
- griptape_nodes/retained_mode/events/os_events.py +86 -3
- griptape_nodes/retained_mode/events/project_events.py +15 -1
- griptape_nodes/retained_mode/events/workflow_events.py +48 -1
- griptape_nodes/retained_mode/griptape_nodes.py +6 -2
- griptape_nodes/retained_mode/managers/config_manager.py +10 -8
- griptape_nodes/retained_mode/managers/event_manager.py +168 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
- griptape_nodes/retained_mode/managers/library_manager.py +1134 -138
- griptape_nodes/retained_mode/managers/model_manager.py +2 -3
- griptape_nodes/retained_mode/managers/node_manager.py +148 -25
- griptape_nodes/retained_mode/managers/object_manager.py +3 -1
- griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
- griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
- griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
- griptape_nodes/retained_mode/managers/settings.py +21 -1
- griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
- griptape_nodes/retained_mode/retained_mode.py +3 -3
- griptape_nodes/traits/button.py +44 -2
- griptape_nodes/traits/file_system_picker.py +2 -2
- griptape_nodes/utils/file_utils.py +101 -0
- griptape_nodes/utils/git_utils.py +1226 -0
- griptape_nodes/utils/library_utils.py +122 -0
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
|
@@ -13,7 +13,7 @@ from collections import defaultdict
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from importlib.resources import files
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypeVar, cast
|
|
17
17
|
|
|
18
18
|
from packaging.requirements import InvalidRequirement, Requirement
|
|
19
19
|
from pydantic import ValidationError
|
|
@@ -23,6 +23,7 @@ from rich.console import Console
|
|
|
23
23
|
from rich.panel import Panel
|
|
24
24
|
from rich.table import Table
|
|
25
25
|
from rich.text import Text
|
|
26
|
+
from semver import Version
|
|
26
27
|
from xdg_base_dirs import xdg_data_home
|
|
27
28
|
|
|
28
29
|
from griptape_nodes.exe_types.node_types import BaseNode
|
|
@@ -45,7 +46,7 @@ from griptape_nodes.retained_mode.events.app_events import (
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
# Runtime imports for ResultDetails since it's used at runtime
|
|
48
|
-
from griptape_nodes.retained_mode.events.base_events import AppEvent, ResultDetails
|
|
49
|
+
from griptape_nodes.retained_mode.events.base_events import AppEvent, ResultDetails, ResultPayloadFailure
|
|
49
50
|
from griptape_nodes.retained_mode.events.config_events import (
|
|
50
51
|
GetConfigCategoryRequest,
|
|
51
52
|
GetConfigCategoryResultSuccess,
|
|
@@ -53,6 +54,12 @@ from griptape_nodes.retained_mode.events.config_events import (
|
|
|
53
54
|
SetConfigCategoryResultSuccess,
|
|
54
55
|
)
|
|
55
56
|
from griptape_nodes.retained_mode.events.library_events import (
|
|
57
|
+
CheckLibraryUpdateRequest,
|
|
58
|
+
CheckLibraryUpdateResultFailure,
|
|
59
|
+
CheckLibraryUpdateResultSuccess,
|
|
60
|
+
DownloadLibraryRequest,
|
|
61
|
+
DownloadLibraryResultFailure,
|
|
62
|
+
DownloadLibraryResultSuccess,
|
|
56
63
|
GetAllInfoForAllLibrariesRequest,
|
|
57
64
|
GetAllInfoForAllLibrariesResultFailure,
|
|
58
65
|
GetAllInfoForAllLibrariesResultSuccess,
|
|
@@ -65,6 +72,12 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
65
72
|
GetNodeMetadataFromLibraryRequest,
|
|
66
73
|
GetNodeMetadataFromLibraryResultFailure,
|
|
67
74
|
GetNodeMetadataFromLibraryResultSuccess,
|
|
75
|
+
InspectLibraryRepoRequest,
|
|
76
|
+
InspectLibraryRepoResultFailure,
|
|
77
|
+
InspectLibraryRepoResultSuccess,
|
|
78
|
+
InstallLibraryDependenciesRequest,
|
|
79
|
+
InstallLibraryDependenciesResultFailure,
|
|
80
|
+
InstallLibraryDependenciesResultSuccess,
|
|
68
81
|
ListCapableLibraryEventHandlersRequest,
|
|
69
82
|
ListCapableLibraryEventHandlersResultFailure,
|
|
70
83
|
ListCapableLibraryEventHandlersResultSuccess,
|
|
@@ -93,11 +106,24 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
93
106
|
ReloadAllLibrariesRequest,
|
|
94
107
|
ReloadAllLibrariesResultFailure,
|
|
95
108
|
ReloadAllLibrariesResultSuccess,
|
|
109
|
+
SwitchLibraryRefRequest,
|
|
110
|
+
SwitchLibraryRefResultFailure,
|
|
111
|
+
SwitchLibraryRefResultSuccess,
|
|
112
|
+
SyncLibrariesRequest,
|
|
113
|
+
SyncLibrariesResultFailure,
|
|
114
|
+
SyncLibrariesResultSuccess,
|
|
96
115
|
UnloadLibraryFromRegistryRequest,
|
|
97
116
|
UnloadLibraryFromRegistryResultFailure,
|
|
98
117
|
UnloadLibraryFromRegistryResultSuccess,
|
|
118
|
+
UpdateLibraryRequest,
|
|
119
|
+
UpdateLibraryResultFailure,
|
|
120
|
+
UpdateLibraryResultSuccess,
|
|
99
121
|
)
|
|
100
122
|
from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
|
|
123
|
+
from griptape_nodes.retained_mode.events.os_events import (
|
|
124
|
+
DeleteFileRequest,
|
|
125
|
+
DeleteFileResultFailure,
|
|
126
|
+
)
|
|
101
127
|
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
102
128
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
103
129
|
from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
@@ -105,10 +131,8 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
105
131
|
AfterLibraryCallbackProblem,
|
|
106
132
|
BeforeLibraryCallbackProblem,
|
|
107
133
|
CreateConfigCategoryProblem,
|
|
108
|
-
DependencyInstallationFailedProblem,
|
|
109
134
|
DuplicateLibraryProblem,
|
|
110
135
|
EngineVersionErrorProblem,
|
|
111
|
-
InsufficientDiskSpaceProblem,
|
|
112
136
|
InvalidVersionStringProblem,
|
|
113
137
|
LibraryJsonDecodeProblem,
|
|
114
138
|
LibraryLoadExceptionProblem,
|
|
@@ -119,9 +143,9 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
119
143
|
NodeClassNotBaseNodeProblem,
|
|
120
144
|
NodeClassNotFoundProblem,
|
|
121
145
|
NodeModuleImportProblem,
|
|
146
|
+
OldXdgLocationWarningProblem,
|
|
122
147
|
SandboxDirectoryMissingProblem,
|
|
123
148
|
UpdateConfigCategoryProblem,
|
|
124
|
-
VenvCreationFailedProblem,
|
|
125
149
|
)
|
|
126
150
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_directory import LibraryDirectory
|
|
127
151
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.local_file import (
|
|
@@ -129,8 +153,32 @@ from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.
|
|
|
129
153
|
)
|
|
130
154
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
131
155
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
156
|
+
from griptape_nodes.retained_mode.managers.settings import LIBRARIES_TO_DOWNLOAD_KEY, LIBRARIES_TO_REGISTER_KEY
|
|
132
157
|
from griptape_nodes.utils.async_utils import subprocess_run
|
|
133
158
|
from griptape_nodes.utils.dict_utils import merge_dicts
|
|
159
|
+
from griptape_nodes.utils.file_utils import find_file_in_directory
|
|
160
|
+
from griptape_nodes.utils.git_utils import (
|
|
161
|
+
GitCloneError,
|
|
162
|
+
GitPullError,
|
|
163
|
+
GitRefError,
|
|
164
|
+
GitRemoteError,
|
|
165
|
+
GitRepositoryError,
|
|
166
|
+
clone_repository,
|
|
167
|
+
extract_repo_name_from_url,
|
|
168
|
+
get_current_ref,
|
|
169
|
+
get_git_remote,
|
|
170
|
+
get_local_commit_sha,
|
|
171
|
+
is_git_url,
|
|
172
|
+
parse_git_url_with_ref,
|
|
173
|
+
switch_branch_or_tag,
|
|
174
|
+
update_library_git,
|
|
175
|
+
)
|
|
176
|
+
from griptape_nodes.utils.library_utils import (
|
|
177
|
+
LIBRARY_GIT_URLS,
|
|
178
|
+
clone_and_get_library_version,
|
|
179
|
+
filter_old_xdg_library_paths,
|
|
180
|
+
is_monorepo,
|
|
181
|
+
)
|
|
134
182
|
from griptape_nodes.utils.uv_utils import find_uv_bin
|
|
135
183
|
from griptape_nodes.utils.version_utils import get_complete_version_string
|
|
136
184
|
|
|
@@ -151,6 +199,32 @@ EXCLUDED_SCAN_DIRECTORIES = frozenset({"venv", "__pycache__"})
|
|
|
151
199
|
TRegisteredEventData = TypeVar("TRegisteredEventData")
|
|
152
200
|
|
|
153
201
|
|
|
202
|
+
class LibraryGitOperationContext(NamedTuple):
|
|
203
|
+
"""Context information for git operations on a library."""
|
|
204
|
+
|
|
205
|
+
library: Library
|
|
206
|
+
old_version: str
|
|
207
|
+
library_file_path: str
|
|
208
|
+
library_dir: Path
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LibraryUpdateInfo(NamedTuple):
|
|
212
|
+
"""Information about a library pending update."""
|
|
213
|
+
|
|
214
|
+
library_name: str
|
|
215
|
+
old_version: str
|
|
216
|
+
new_version: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LibraryUpdateResult(NamedTuple):
|
|
220
|
+
"""Result of updating a single library."""
|
|
221
|
+
|
|
222
|
+
library_name: str
|
|
223
|
+
old_version: str
|
|
224
|
+
new_version: str
|
|
225
|
+
result: ResultPayload
|
|
226
|
+
|
|
227
|
+
|
|
154
228
|
class LibraryManager:
|
|
155
229
|
SANDBOX_LIBRARY_NAME = "Sandbox Library"
|
|
156
230
|
LIBRARY_CONFIG_FILENAME = "griptape_nodes_library.json"
|
|
@@ -258,6 +332,15 @@ class LibraryManager:
|
|
|
258
332
|
)
|
|
259
333
|
event_manager.assign_manager_to_request_type(ReloadAllLibrariesRequest, self.reload_libraries_request)
|
|
260
334
|
event_manager.assign_manager_to_request_type(LoadLibrariesRequest, self.load_libraries_request)
|
|
335
|
+
event_manager.assign_manager_to_request_type(CheckLibraryUpdateRequest, self.check_library_update_request)
|
|
336
|
+
event_manager.assign_manager_to_request_type(UpdateLibraryRequest, self.update_library_request)
|
|
337
|
+
event_manager.assign_manager_to_request_type(SwitchLibraryRefRequest, self.switch_library_ref_request)
|
|
338
|
+
event_manager.assign_manager_to_request_type(DownloadLibraryRequest, self.download_library_request)
|
|
339
|
+
event_manager.assign_manager_to_request_type(
|
|
340
|
+
InstallLibraryDependenciesRequest, self.install_library_dependencies_request
|
|
341
|
+
)
|
|
342
|
+
event_manager.assign_manager_to_request_type(SyncLibrariesRequest, self.sync_libraries_request)
|
|
343
|
+
event_manager.assign_manager_to_request_type(InspectLibraryRepoRequest, self.inspect_library_repo_request)
|
|
261
344
|
|
|
262
345
|
event_manager.add_listener_to_app_event(
|
|
263
346
|
AppInitializationComplete,
|
|
@@ -420,7 +503,6 @@ class LibraryManager:
|
|
|
420
503
|
library = LibraryRegistry.get_library(name=request.library)
|
|
421
504
|
except KeyError:
|
|
422
505
|
details = f"Attempted to list node types in a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
423
|
-
logger.error(details)
|
|
424
506
|
|
|
425
507
|
result = ListNodeTypesInLibraryResultFailure(result_details=details)
|
|
426
508
|
return result
|
|
@@ -443,7 +525,6 @@ class LibraryManager:
|
|
|
443
525
|
library = LibraryRegistry.get_library(name=request.library)
|
|
444
526
|
except KeyError:
|
|
445
527
|
details = f"Attempted to get metadata for Library '{request.library}'. Failed because no Library with that name was registered."
|
|
446
|
-
logger.error(details)
|
|
447
528
|
|
|
448
529
|
result = GetLibraryMetadataResultFailure(result_details=details)
|
|
449
530
|
return result
|
|
@@ -469,7 +550,6 @@ class LibraryManager:
|
|
|
469
550
|
# Check if the file exists
|
|
470
551
|
if not json_path.exists():
|
|
471
552
|
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
472
|
-
logger.error(details)
|
|
473
553
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
474
554
|
library_path=file_path,
|
|
475
555
|
library_name=None,
|
|
@@ -484,7 +564,6 @@ class LibraryManager:
|
|
|
484
564
|
library_json = json.load(f)
|
|
485
565
|
except json.JSONDecodeError:
|
|
486
566
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
|
|
487
|
-
logger.error(details)
|
|
488
567
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
489
568
|
library_path=file_path,
|
|
490
569
|
library_name=None,
|
|
@@ -494,7 +573,6 @@ class LibraryManager:
|
|
|
494
573
|
)
|
|
495
574
|
except Exception as err:
|
|
496
575
|
details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
|
|
497
|
-
logger.error(details)
|
|
498
576
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
499
577
|
library_path=file_path,
|
|
500
578
|
library_name=None,
|
|
@@ -519,7 +597,6 @@ class LibraryManager:
|
|
|
519
597
|
problem = LibrarySchemaValidationProblem(location=loc, error_type=error_type, message=msg)
|
|
520
598
|
problems.append(problem)
|
|
521
599
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
522
|
-
logger.error(details)
|
|
523
600
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
524
601
|
library_path=file_path,
|
|
525
602
|
library_name=library_name,
|
|
@@ -529,7 +606,6 @@ class LibraryManager:
|
|
|
529
606
|
)
|
|
530
607
|
except Exception as err:
|
|
531
608
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
532
|
-
logger.error(details)
|
|
533
609
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
534
610
|
library_path=file_path,
|
|
535
611
|
library_name=library_name,
|
|
@@ -538,9 +614,18 @@ class LibraryManager:
|
|
|
538
614
|
result_details=details,
|
|
539
615
|
)
|
|
540
616
|
|
|
617
|
+
# Get git remote and ref if this library is in a git repository
|
|
618
|
+
library_dir = json_path.parent.absolute()
|
|
619
|
+
git_remote = get_git_remote(library_dir)
|
|
620
|
+
git_ref = get_current_ref(library_dir)
|
|
621
|
+
|
|
541
622
|
details = f"Successfully loaded library metadata from JSON file at {json_path}"
|
|
542
623
|
return LoadLibraryMetadataFromFileResultSuccess(
|
|
543
|
-
library_schema=library_data,
|
|
624
|
+
library_schema=library_data,
|
|
625
|
+
file_path=file_path,
|
|
626
|
+
git_remote=git_remote,
|
|
627
|
+
git_ref=git_ref,
|
|
628
|
+
result_details=details,
|
|
544
629
|
)
|
|
545
630
|
|
|
546
631
|
def load_metadata_for_all_libraries_request(self, request: LoadMetadataForAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
|
|
@@ -686,9 +771,17 @@ class LibraryManager:
|
|
|
686
771
|
nodes=node_definitions,
|
|
687
772
|
)
|
|
688
773
|
|
|
774
|
+
# Get git remote and ref if the sandbox directory is in a git repository
|
|
775
|
+
git_remote = get_git_remote(sandbox_library_dir)
|
|
776
|
+
git_ref = get_current_ref(sandbox_library_dir)
|
|
777
|
+
|
|
689
778
|
details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_library_dir}"
|
|
690
779
|
return LoadLibraryMetadataFromFileResultSuccess(
|
|
691
|
-
library_schema=library_schema,
|
|
780
|
+
library_schema=library_schema,
|
|
781
|
+
file_path=str(sandbox_library_dir),
|
|
782
|
+
git_remote=git_remote,
|
|
783
|
+
git_ref=git_ref,
|
|
784
|
+
result_details=details,
|
|
692
785
|
)
|
|
693
786
|
|
|
694
787
|
def get_node_metadata_from_library_request(self, request: GetNodeMetadataFromLibraryRequest) -> ResultPayload:
|
|
@@ -722,7 +815,6 @@ class LibraryManager:
|
|
|
722
815
|
library = LibraryRegistry.get_library(name=request.library)
|
|
723
816
|
except KeyError:
|
|
724
817
|
details = f"Attempted to get categories in a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
725
|
-
logger.error(details)
|
|
726
818
|
result = ListCategoriesInLibraryResultFailure(result_details=details)
|
|
727
819
|
return result
|
|
728
820
|
|
|
@@ -747,7 +839,6 @@ class LibraryManager:
|
|
|
747
839
|
problems=[LibraryNotFoundProblem(library_path=file_path)],
|
|
748
840
|
)
|
|
749
841
|
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
750
|
-
logger.error(details)
|
|
751
842
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
752
843
|
|
|
753
844
|
# Use the new metadata loading functionality
|
|
@@ -779,7 +870,6 @@ class LibraryManager:
|
|
|
779
870
|
problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
|
|
780
871
|
)
|
|
781
872
|
details = f"Attempted to load Library '{library_data.name}' JSON file from '{json_path}'. Failed because version string '{library_data.metadata.library_version}' wasn't valid. Must be in major.minor.patch format."
|
|
782
|
-
logger.error(details)
|
|
783
873
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
784
874
|
|
|
785
875
|
# Get the directory containing the JSON file to resolve relative paths
|
|
@@ -787,6 +877,39 @@ class LibraryManager:
|
|
|
787
877
|
# Add the directory to the Python path to allow for relative imports
|
|
788
878
|
sys.path.insert(0, str(base_dir))
|
|
789
879
|
|
|
880
|
+
# Install dependencies and add library venv to sys.path if library has dependencies
|
|
881
|
+
if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
|
|
882
|
+
venv_path = self._get_library_venv_path(library_data.name, file_path)
|
|
883
|
+
|
|
884
|
+
install_request = InstallLibraryDependenciesRequest(library_file_path=file_path)
|
|
885
|
+
install_result = await self.install_library_dependencies_request(install_request)
|
|
886
|
+
|
|
887
|
+
if isinstance(install_result, InstallLibraryDependenciesResultFailure):
|
|
888
|
+
details = (
|
|
889
|
+
f"Failed to install dependencies for library '{library_data.name}': {install_result.result_details}"
|
|
890
|
+
)
|
|
891
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
892
|
+
|
|
893
|
+
if isinstance(install_result, InstallLibraryDependenciesResultSuccess):
|
|
894
|
+
logger.info(
|
|
895
|
+
"Installed %d dependencies for library '%s'",
|
|
896
|
+
install_result.dependencies_installed,
|
|
897
|
+
library_data.name,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
# Add venv site-packages to sys.path so node imports can find dependencies
|
|
901
|
+
if venv_path.exists():
|
|
902
|
+
site_packages = str(
|
|
903
|
+
Path(
|
|
904
|
+
sysconfig.get_path(
|
|
905
|
+
"purelib",
|
|
906
|
+
vars={"base": str(venv_path), "platbase": str(venv_path)},
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
)
|
|
910
|
+
sys.path.insert(0, site_packages)
|
|
911
|
+
logger.debug("Added library '%s' venv to sys.path: %s", library_data.name, site_packages)
|
|
912
|
+
|
|
790
913
|
# Load the advanced library module if specified
|
|
791
914
|
advanced_library_instance = None
|
|
792
915
|
if library_data.advanced_library_path:
|
|
@@ -808,7 +931,6 @@ class LibraryManager:
|
|
|
808
931
|
],
|
|
809
932
|
)
|
|
810
933
|
details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
|
|
811
|
-
logger.error(details)
|
|
812
934
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
813
935
|
|
|
814
936
|
# Create or get the library
|
|
@@ -831,91 +953,6 @@ class LibraryManager:
|
|
|
831
953
|
)
|
|
832
954
|
|
|
833
955
|
details = f"Attempted to load Library JSON file from '{json_path}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
|
|
834
|
-
logger.error(details)
|
|
835
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
836
|
-
|
|
837
|
-
# Install node library dependencies
|
|
838
|
-
try:
|
|
839
|
-
if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
|
|
840
|
-
pip_install_flags = library_data.metadata.dependencies.pip_install_flags
|
|
841
|
-
if pip_install_flags is None:
|
|
842
|
-
pip_install_flags = []
|
|
843
|
-
pip_dependencies = library_data.metadata.dependencies.pip_dependencies
|
|
844
|
-
|
|
845
|
-
# Determine venv path for dependency installation
|
|
846
|
-
venv_path = self._get_library_venv_path(library_data.name, file_path)
|
|
847
|
-
|
|
848
|
-
# Only install dependencies if conditions are met
|
|
849
|
-
try:
|
|
850
|
-
library_venv_python_path = await self._init_library_venv(venv_path)
|
|
851
|
-
except RuntimeError as e:
|
|
852
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
853
|
-
library_path=file_path,
|
|
854
|
-
library_name=library_data.name,
|
|
855
|
-
library_version=library_version,
|
|
856
|
-
status=LibraryStatus.UNUSABLE,
|
|
857
|
-
problems=[VenvCreationFailedProblem(error_message=str(e))],
|
|
858
|
-
)
|
|
859
|
-
details = f"Attempted to load Library JSON file from '{json_path}'. Failed when creating the virtual environment: {e}."
|
|
860
|
-
logger.error(details)
|
|
861
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
862
|
-
if self._can_write_to_venv_location(library_venv_python_path):
|
|
863
|
-
# Check disk space before installing dependencies
|
|
864
|
-
config_manager = GriptapeNodes.ConfigManager()
|
|
865
|
-
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
866
|
-
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
867
|
-
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
868
|
-
details = f"Attempted to load Library JSON from '{json_path}'. Failed when installing dependencies due to insufficient disk space (requires {min_space_gb} GB): {error_msg}"
|
|
869
|
-
logger.error(details)
|
|
870
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
871
|
-
library_path=file_path,
|
|
872
|
-
library_name=library_data.name,
|
|
873
|
-
library_version=library_version,
|
|
874
|
-
status=LibraryStatus.UNUSABLE,
|
|
875
|
-
problems=[InsufficientDiskSpaceProblem(min_space_gb=min_space_gb, error_message=error_msg)],
|
|
876
|
-
)
|
|
877
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
878
|
-
|
|
879
|
-
# Grab the python executable from the virtual environment so that we can pip install there
|
|
880
|
-
logger.info(
|
|
881
|
-
"Installing dependencies for library '%s' with pip in venv at %s", library_data.name, venv_path
|
|
882
|
-
)
|
|
883
|
-
is_debug = config_manager.get_config_value("log_level").upper() == "DEBUG"
|
|
884
|
-
await subprocess_run(
|
|
885
|
-
[
|
|
886
|
-
sys.executable,
|
|
887
|
-
"-m",
|
|
888
|
-
"uv",
|
|
889
|
-
"pip",
|
|
890
|
-
"install",
|
|
891
|
-
*pip_dependencies,
|
|
892
|
-
*pip_install_flags,
|
|
893
|
-
"--python",
|
|
894
|
-
str(library_venv_python_path),
|
|
895
|
-
],
|
|
896
|
-
check=True,
|
|
897
|
-
capture_output=not is_debug,
|
|
898
|
-
text=True,
|
|
899
|
-
)
|
|
900
|
-
else:
|
|
901
|
-
logger.debug(
|
|
902
|
-
"Skipping dependency installation for library '%s' - venv location at %s is not writable",
|
|
903
|
-
library_data.name,
|
|
904
|
-
venv_path,
|
|
905
|
-
)
|
|
906
|
-
except subprocess.CalledProcessError as e:
|
|
907
|
-
# Failed to create the library
|
|
908
|
-
error_details = f"return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
|
|
909
|
-
|
|
910
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
911
|
-
library_path=file_path,
|
|
912
|
-
library_name=library_data.name,
|
|
913
|
-
library_version=library_version,
|
|
914
|
-
status=LibraryStatus.UNUSABLE,
|
|
915
|
-
problems=[DependencyInstallationFailedProblem(error_details=error_details)],
|
|
916
|
-
)
|
|
917
|
-
details = f"Attempted to load Library JSON file from '{json_path}'. Failed when installing dependencies: {error_details}"
|
|
918
|
-
logger.error(details)
|
|
919
956
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
920
957
|
|
|
921
958
|
# We are at least potentially viable.
|
|
@@ -927,7 +964,10 @@ class LibraryManager:
|
|
|
927
964
|
# Assign them into the config space.
|
|
928
965
|
for library_data_setting in library_data.settings:
|
|
929
966
|
# Does the category exist?
|
|
930
|
-
get_category_request = GetConfigCategoryRequest(
|
|
967
|
+
get_category_request = GetConfigCategoryRequest(
|
|
968
|
+
category=library_data_setting.category,
|
|
969
|
+
failure_log_level=logging.DEBUG, # Missing category is expected, suppress error toast
|
|
970
|
+
)
|
|
931
971
|
get_category_result = GriptapeNodes.handle_request(get_category_request)
|
|
932
972
|
if not isinstance(get_category_result, GetConfigCategoryResultSuccess):
|
|
933
973
|
# That's OK, we'll invent it. Or at least we'll try.
|
|
@@ -983,7 +1023,6 @@ class LibraryManager:
|
|
|
983
1023
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
984
1024
|
case _:
|
|
985
1025
|
details = f"Attempted to load Library JSON file from '{json_path}'. Failed because an unknown/unexpected status '{library_load_results.status}' was returned."
|
|
986
|
-
logger.error(details)
|
|
987
1026
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
988
1027
|
|
|
989
1028
|
async def register_library_from_requirement_specifier_request(
|
|
@@ -994,21 +1033,29 @@ class LibraryManager:
|
|
|
994
1033
|
# Determine venv path for dependency installation
|
|
995
1034
|
venv_path = self._get_library_venv_path(package_name, None)
|
|
996
1035
|
|
|
1036
|
+
# Check if venv already exists before initialization
|
|
1037
|
+
venv_already_exists = venv_path.exists()
|
|
1038
|
+
|
|
997
1039
|
# Only install dependencies if conditions are met
|
|
998
1040
|
try:
|
|
999
1041
|
library_python_venv_path = await self._init_library_venv(venv_path)
|
|
1000
1042
|
except RuntimeError as e:
|
|
1001
1043
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed when creating the virtual environment: {e}"
|
|
1002
|
-
logger.error(details)
|
|
1003
1044
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1004
|
-
|
|
1045
|
+
|
|
1046
|
+
if venv_already_exists:
|
|
1047
|
+
logger.debug(
|
|
1048
|
+
"Skipping dependency installation for package '%s' - venv already exists at %s",
|
|
1049
|
+
package_name,
|
|
1050
|
+
venv_path,
|
|
1051
|
+
)
|
|
1052
|
+
elif self._can_write_to_venv_location(library_python_venv_path):
|
|
1005
1053
|
# Check disk space before installing dependencies
|
|
1006
1054
|
config_manager = GriptapeNodes.ConfigManager()
|
|
1007
1055
|
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
1008
1056
|
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
1009
1057
|
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
1010
1058
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed when installing dependencies due to insufficient disk space (requires {min_space_gb} GB): {error_msg}"
|
|
1011
|
-
logger.error(details)
|
|
1012
1059
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1013
1060
|
|
|
1014
1061
|
uv_path = find_uv_bin()
|
|
@@ -1036,11 +1083,9 @@ class LibraryManager:
|
|
|
1036
1083
|
)
|
|
1037
1084
|
except subprocess.CalledProcessError as e:
|
|
1038
1085
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed: return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
|
|
1039
|
-
logger.error(details)
|
|
1040
1086
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1041
1087
|
except InvalidRequirement as e:
|
|
1042
1088
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to invalid requirement specifier: {e}"
|
|
1043
|
-
logger.error(details)
|
|
1044
1089
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1045
1090
|
|
|
1046
1091
|
library_path = str(files(package_name).joinpath(request.library_config_name))
|
|
@@ -1048,7 +1093,6 @@ class LibraryManager:
|
|
|
1048
1093
|
register_result = GriptapeNodes.handle_request(RegisterLibraryFromFileRequest(file_path=library_path))
|
|
1049
1094
|
if isinstance(register_result, RegisterLibraryFromFileResultFailure):
|
|
1050
1095
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to {register_result}"
|
|
1051
|
-
logger.error(details)
|
|
1052
1096
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1053
1097
|
|
|
1054
1098
|
return RegisterLibraryFromRequirementSpecifierResultSuccess(
|
|
@@ -1110,17 +1154,6 @@ class LibraryManager:
|
|
|
1110
1154
|
else:
|
|
1111
1155
|
library_venv_python_path = library_venv_path / "bin" / "python"
|
|
1112
1156
|
|
|
1113
|
-
# Need to insert into the path so that the library picks up on the venv
|
|
1114
|
-
site_packages = str(
|
|
1115
|
-
Path(
|
|
1116
|
-
sysconfig.get_path(
|
|
1117
|
-
"purelib",
|
|
1118
|
-
vars={"base": str(library_venv_path), "platbase": str(library_venv_path)},
|
|
1119
|
-
)
|
|
1120
|
-
)
|
|
1121
|
-
)
|
|
1122
|
-
sys.path.insert(0, site_packages)
|
|
1123
|
-
|
|
1124
1157
|
return library_venv_python_path
|
|
1125
1158
|
|
|
1126
1159
|
def _get_library_venv_path(self, library_name: str, library_file_path: str | None = None) -> Path:
|
|
@@ -1179,7 +1212,6 @@ class LibraryManager:
|
|
|
1179
1212
|
LibraryRegistry.unregister_library(library_name=request.library_name)
|
|
1180
1213
|
except Exception as e:
|
|
1181
1214
|
details = f"Attempted to unload library '{request.library_name}'. Failed due to {e}"
|
|
1182
|
-
logger.error(details)
|
|
1183
1215
|
return UnloadLibraryFromRegistryResultFailure(result_details=details)
|
|
1184
1216
|
|
|
1185
1217
|
# Clean up all stable module aliases for this library
|
|
@@ -1199,7 +1231,6 @@ class LibraryManager:
|
|
|
1199
1231
|
|
|
1200
1232
|
if not list_libraries_result.succeeded():
|
|
1201
1233
|
details = "Attempted to get all info for all libraries, but listing the registered libraries failed."
|
|
1202
|
-
logger.error(details)
|
|
1203
1234
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1204
1235
|
|
|
1205
1236
|
try:
|
|
@@ -1214,7 +1245,6 @@ class LibraryManager:
|
|
|
1214
1245
|
|
|
1215
1246
|
if not library_all_info_result.succeeded():
|
|
1216
1247
|
details = f"Attempted to get all info for all libraries, but failed when getting all info for library named '{library_name}'."
|
|
1217
|
-
logger.error(details)
|
|
1218
1248
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1219
1249
|
|
|
1220
1250
|
library_all_info_success = cast("GetAllInfoForLibraryResultSuccess", library_all_info_result)
|
|
@@ -1222,7 +1252,6 @@ class LibraryManager:
|
|
|
1222
1252
|
library_name_to_all_info[library_name] = library_all_info_success
|
|
1223
1253
|
except Exception as err:
|
|
1224
1254
|
details = f"Attempted to get all info for all libraries. Encountered error {err}."
|
|
1225
|
-
logger.error(details)
|
|
1226
1255
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1227
1256
|
|
|
1228
1257
|
# We're home free now
|
|
@@ -1245,7 +1274,6 @@ class LibraryManager:
|
|
|
1245
1274
|
LibraryRegistry.get_library(name=request.library)
|
|
1246
1275
|
except KeyError:
|
|
1247
1276
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
1248
|
-
logger.error(details)
|
|
1249
1277
|
result = GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1250
1278
|
return result
|
|
1251
1279
|
|
|
@@ -1254,7 +1282,6 @@ class LibraryManager:
|
|
|
1254
1282
|
|
|
1255
1283
|
if not library_metadata_result.succeeded():
|
|
1256
1284
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the library's metadata."
|
|
1257
|
-
logger.error(details)
|
|
1258
1285
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1259
1286
|
|
|
1260
1287
|
list_categories_request = ListCategoriesInLibraryRequest(library=request.library)
|
|
@@ -1262,7 +1289,6 @@ class LibraryManager:
|
|
|
1262
1289
|
|
|
1263
1290
|
if not list_categories_result.succeeded():
|
|
1264
1291
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the list of categories in the library."
|
|
1265
|
-
logger.error(details)
|
|
1266
1292
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1267
1293
|
|
|
1268
1294
|
node_type_list_request = ListNodeTypesInLibraryRequest(library=request.library)
|
|
@@ -1270,7 +1296,6 @@ class LibraryManager:
|
|
|
1270
1296
|
|
|
1271
1297
|
if not node_type_list_result.succeeded():
|
|
1272
1298
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the list of node types in the library."
|
|
1273
|
-
logger.error(details)
|
|
1274
1299
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1275
1300
|
|
|
1276
1301
|
# Cast everyone to their success counterparts.
|
|
@@ -1282,7 +1307,6 @@ class LibraryManager:
|
|
|
1282
1307
|
details = (
|
|
1283
1308
|
f"Attempted to get all library info for a Library named '{request.library}'. Encountered error: {err}."
|
|
1284
1309
|
)
|
|
1285
|
-
logger.error(details)
|
|
1286
1310
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1287
1311
|
|
|
1288
1312
|
# Now build the map of node types to metadata.
|
|
@@ -1293,14 +1317,12 @@ class LibraryManager:
|
|
|
1293
1317
|
|
|
1294
1318
|
if not node_metadata_result.succeeded():
|
|
1295
1319
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the metadata for a node type called '{node_type_name}'."
|
|
1296
|
-
logger.error(details)
|
|
1297
1320
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1298
1321
|
|
|
1299
1322
|
try:
|
|
1300
1323
|
node_metadata_result_success = cast("GetNodeMetadataFromLibraryResultSuccess", node_metadata_result)
|
|
1301
1324
|
except Exception as err:
|
|
1302
1325
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Encountered error: {err}."
|
|
1303
|
-
logger.error(details)
|
|
1304
1326
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1305
1327
|
|
|
1306
1328
|
# Put it into the map.
|
|
@@ -1689,13 +1711,132 @@ class LibraryManager:
|
|
|
1689
1711
|
self.print_library_load_status()
|
|
1690
1712
|
|
|
1691
1713
|
# Remove any missing libraries AFTER we've printed them for the user.
|
|
1692
|
-
user_libraries_section =
|
|
1714
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
1693
1715
|
self._remove_missing_libraries_from_config(config_category=user_libraries_section)
|
|
1694
1716
|
finally:
|
|
1695
1717
|
self._libraries_loading_complete.set()
|
|
1696
1718
|
|
|
1719
|
+
async def _ensure_libraries_from_config(self) -> None:
|
|
1720
|
+
"""Ensure libraries from git URLs specified in config are downloaded.
|
|
1721
|
+
|
|
1722
|
+
This method:
|
|
1723
|
+
1. Reads libraries_to_download from config
|
|
1724
|
+
2. Downloads any missing libraries concurrently
|
|
1725
|
+
3. Logs summary of successful/failed operations
|
|
1726
|
+
|
|
1727
|
+
Supports URL format with @ref suffix (e.g., "https://github.com/user/repo@stable").
|
|
1728
|
+
Libraries are registered later by load_all_libraries_from_config().
|
|
1729
|
+
"""
|
|
1730
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
1731
|
+
git_urls = config_mgr.get_config_value(LIBRARIES_TO_DOWNLOAD_KEY, default=[])
|
|
1732
|
+
|
|
1733
|
+
if not git_urls:
|
|
1734
|
+
logger.debug("No libraries to download from config")
|
|
1735
|
+
return
|
|
1736
|
+
|
|
1737
|
+
logger.info("Starting download of %d libraries from config", len(git_urls))
|
|
1738
|
+
|
|
1739
|
+
# Use shared download method
|
|
1740
|
+
results = await self._download_libraries_from_git_urls(git_urls)
|
|
1741
|
+
|
|
1742
|
+
# Count successes and failures
|
|
1743
|
+
successful = sum(1 for r in results.values() if r["success"])
|
|
1744
|
+
failed = len(results) - successful
|
|
1745
|
+
|
|
1746
|
+
logger.info(
|
|
1747
|
+
"Completed automatic library downloads: %d successful, %d failed",
|
|
1748
|
+
successful,
|
|
1749
|
+
failed,
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
async def _download_libraries_from_git_urls(
|
|
1753
|
+
self,
|
|
1754
|
+
git_urls_with_refs: list[str],
|
|
1755
|
+
) -> dict[str, dict[str, Any]]:
|
|
1756
|
+
"""Download multiple libraries from git URLs concurrently.
|
|
1757
|
+
|
|
1758
|
+
Args:
|
|
1759
|
+
git_urls_with_refs: List of git URLs with optional @ref suffix (e.g., "url@v1.0")
|
|
1760
|
+
|
|
1761
|
+
Returns:
|
|
1762
|
+
Dictionary mapping git_url_with_ref to result info:
|
|
1763
|
+
{
|
|
1764
|
+
"url@ref": {
|
|
1765
|
+
"success": bool,
|
|
1766
|
+
"library_name": str | None,
|
|
1767
|
+
"error": str | None,
|
|
1768
|
+
"skipped": bool (optional, True if already exists),
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
"""
|
|
1772
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
1773
|
+
libraries_dir_setting = config_mgr.get_config_value("libraries_directory")
|
|
1774
|
+
|
|
1775
|
+
if not libraries_dir_setting:
|
|
1776
|
+
logger.warning("Cannot download libraries: libraries_directory not configured")
|
|
1777
|
+
return {}
|
|
1778
|
+
|
|
1779
|
+
libraries_path = config_mgr.workspace_path / libraries_dir_setting
|
|
1780
|
+
|
|
1781
|
+
async def download_one(git_url_with_ref: str) -> tuple[str, dict[str, Any]]:
|
|
1782
|
+
"""Download a single library if not already present."""
|
|
1783
|
+
# Parse URL to extract git URL and optional ref
|
|
1784
|
+
git_url, ref = parse_git_url_with_ref(git_url_with_ref)
|
|
1785
|
+
target_directory_name = extract_repo_name_from_url(git_url)
|
|
1786
|
+
target_path = libraries_path / target_directory_name
|
|
1787
|
+
|
|
1788
|
+
# Skip if already exists
|
|
1789
|
+
if target_path.exists():
|
|
1790
|
+
logger.info("Library at '%s' already exists, skipping", target_path)
|
|
1791
|
+
return git_url_with_ref, {
|
|
1792
|
+
"success": False,
|
|
1793
|
+
"library_name": None,
|
|
1794
|
+
"error": None,
|
|
1795
|
+
"skipped": True,
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
logger.info("Downloading library from '%s'", git_url_with_ref)
|
|
1799
|
+
download_result = await GriptapeNodes.ahandle_request(
|
|
1800
|
+
DownloadLibraryRequest(
|
|
1801
|
+
git_url=git_url,
|
|
1802
|
+
branch_tag_commit=ref,
|
|
1803
|
+
fail_on_exists=False,
|
|
1804
|
+
auto_register=False,
|
|
1805
|
+
)
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
if isinstance(download_result, DownloadLibraryResultSuccess):
|
|
1809
|
+
logger.info("Downloaded library '%s'", download_result.library_name)
|
|
1810
|
+
return git_url_with_ref, {
|
|
1811
|
+
"success": True,
|
|
1812
|
+
"library_name": download_result.library_name,
|
|
1813
|
+
"error": None,
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
error = str(download_result.result_details)
|
|
1817
|
+
logger.warning("Failed to download '%s': %s", git_url_with_ref, error)
|
|
1818
|
+
return git_url_with_ref, {
|
|
1819
|
+
"success": False,
|
|
1820
|
+
"library_name": None,
|
|
1821
|
+
"error": error,
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
# Download all concurrently
|
|
1825
|
+
async with asyncio.TaskGroup() as tg:
|
|
1826
|
+
tasks = [tg.create_task(download_one(url)) for url in git_urls_with_refs]
|
|
1827
|
+
|
|
1828
|
+
# Collect results
|
|
1829
|
+
return dict(task.result() for task in tasks)
|
|
1830
|
+
|
|
1697
1831
|
async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
1698
|
-
#
|
|
1832
|
+
# Automatically migrate old XDG library paths from config
|
|
1833
|
+
# TODO: Remove https://github.com/griptape-ai/griptape-nodes/issues/3348
|
|
1834
|
+
self._migrate_old_xdg_library_paths()
|
|
1835
|
+
|
|
1836
|
+
# App just got init'd. First download any missing libraries from git URLs.
|
|
1837
|
+
await self._ensure_libraries_from_config()
|
|
1838
|
+
|
|
1839
|
+
# Now load all libraries from config (including newly downloaded ones)
|
|
1699
1840
|
await self.load_all_libraries_from_config()
|
|
1700
1841
|
|
|
1701
1842
|
# Register all secrets now that libraries are loaded and settings are merged
|
|
@@ -1774,7 +1915,7 @@ class LibraryManager:
|
|
|
1774
1915
|
config_mgr = GriptapeNodes.ConfigManager()
|
|
1775
1916
|
|
|
1776
1917
|
# Get the current libraries_to_register list
|
|
1777
|
-
user_libraries_section =
|
|
1918
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
1778
1919
|
libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
|
|
1779
1920
|
|
|
1780
1921
|
# Filter out empty or whitespace-only entries
|
|
@@ -1929,6 +2070,25 @@ class LibraryManager:
|
|
|
1929
2070
|
if issue.severity == LibraryStatus.UNUSABLE:
|
|
1930
2071
|
has_disqualifying_issues = True
|
|
1931
2072
|
|
|
2073
|
+
# Check if library is in old XDG location
|
|
2074
|
+
old_xdg_libraries_path = xdg_data_home() / "griptape_nodes" / "libraries"
|
|
2075
|
+
library_path_obj = Path(library_file_path)
|
|
2076
|
+
try:
|
|
2077
|
+
# Check if the library path is relative to the old XDG location
|
|
2078
|
+
if library_path_obj.is_relative_to(old_xdg_libraries_path):
|
|
2079
|
+
problems.append(OldXdgLocationWarningProblem(old_path=str(library_path_obj)))
|
|
2080
|
+
logger.warning(
|
|
2081
|
+
"Library '%s' is located in old XDG data directory: %s. "
|
|
2082
|
+
"Starting with version 0.65.0, libraries are managed in your workspace directory. "
|
|
2083
|
+
"To migrate: run 'gtn init' (CLI) or go to App Settings and click 'Re-run Setup Wizard' (desktop app).",
|
|
2084
|
+
library_data.name,
|
|
2085
|
+
library_file_path,
|
|
2086
|
+
)
|
|
2087
|
+
except ValueError:
|
|
2088
|
+
# is_relative_to() raises ValueError if paths are on different drives
|
|
2089
|
+
# In this case, library is definitely not in the old XDG location
|
|
2090
|
+
pass
|
|
2091
|
+
|
|
1932
2092
|
# Early exit if any version issues are disqualifying
|
|
1933
2093
|
if has_disqualifying_issues:
|
|
1934
2094
|
return LibraryManager.LibraryInfo(
|
|
@@ -2169,13 +2329,94 @@ class LibraryManager:
|
|
|
2169
2329
|
]
|
|
2170
2330
|
config_mgr.set_config_value(config_category, libraries_to_register_category)
|
|
2171
2331
|
|
|
2332
|
+
def _migrate_old_xdg_library_paths(self) -> None:
|
|
2333
|
+
"""Automatically removes old XDG library paths and adds git URLs to download list.
|
|
2334
|
+
|
|
2335
|
+
This method removes library paths that were stored in the old XDG data home location
|
|
2336
|
+
(~/.local/share/griptape_nodes/libraries/) from the libraries_to_register configuration,
|
|
2337
|
+
and automatically adds the corresponding git URLs to libraries_to_download to ensure
|
|
2338
|
+
the libraries are re-downloaded. This migration happens automatically on app startup,
|
|
2339
|
+
so users don't need to run gtn init.
|
|
2340
|
+
"""
|
|
2341
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
2342
|
+
|
|
2343
|
+
# Get both config lists
|
|
2344
|
+
register_key = LIBRARIES_TO_REGISTER_KEY
|
|
2345
|
+
download_key = LIBRARIES_TO_DOWNLOAD_KEY
|
|
2346
|
+
|
|
2347
|
+
libraries_to_register = config_mgr.get_config_value(register_key)
|
|
2348
|
+
libraries_to_download = config_mgr.get_config_value(download_key) or []
|
|
2349
|
+
|
|
2350
|
+
if not libraries_to_register:
|
|
2351
|
+
return
|
|
2352
|
+
|
|
2353
|
+
# Filter and get which libraries were removed
|
|
2354
|
+
filtered_libraries, removed_library_names = filter_old_xdg_library_paths(libraries_to_register)
|
|
2355
|
+
|
|
2356
|
+
# If any paths were removed
|
|
2357
|
+
paths_removed = len(libraries_to_register) - len(filtered_libraries)
|
|
2358
|
+
if paths_removed > 0:
|
|
2359
|
+
# Update libraries_to_register
|
|
2360
|
+
config_mgr.set_config_value(register_key, filtered_libraries)
|
|
2361
|
+
|
|
2362
|
+
# Add corresponding git URLs to libraries_to_download
|
|
2363
|
+
updated_downloads = self._add_git_urls_for_removed_libraries(
|
|
2364
|
+
libraries_to_download,
|
|
2365
|
+
removed_library_names,
|
|
2366
|
+
)
|
|
2367
|
+
|
|
2368
|
+
urls_added = len(updated_downloads) - len(libraries_to_download)
|
|
2369
|
+
if urls_added > 0:
|
|
2370
|
+
config_mgr.set_config_value(download_key, updated_downloads)
|
|
2371
|
+
|
|
2372
|
+
logger.info(
|
|
2373
|
+
"Automatically migrated library configuration: removed %d old XDG path(s), added %d git URL(s) to download",
|
|
2374
|
+
paths_removed,
|
|
2375
|
+
urls_added,
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
def _add_git_urls_for_removed_libraries(
|
|
2379
|
+
self,
|
|
2380
|
+
current_downloads: list[str],
|
|
2381
|
+
removed_library_names: set[str],
|
|
2382
|
+
) -> list[str]:
|
|
2383
|
+
"""Add git URLs for removed libraries if not already present.
|
|
2384
|
+
|
|
2385
|
+
Args:
|
|
2386
|
+
current_downloads: Current list of git URLs in libraries_to_download
|
|
2387
|
+
removed_library_names: Set of library names that were removed (e.g., "griptape_nodes_library")
|
|
2388
|
+
|
|
2389
|
+
Returns:
|
|
2390
|
+
Updated list with new git URLs added (deduplicated)
|
|
2391
|
+
"""
|
|
2392
|
+
if not removed_library_names:
|
|
2393
|
+
return current_downloads
|
|
2394
|
+
|
|
2395
|
+
# Get current repository names for deduplication
|
|
2396
|
+
current_repo_names = {extract_repo_name_from_url(url) for url in current_downloads}
|
|
2397
|
+
|
|
2398
|
+
new_downloads = current_downloads.copy()
|
|
2399
|
+
|
|
2400
|
+
for lib_name in removed_library_names:
|
|
2401
|
+
if lib_name not in LIBRARY_GIT_URLS:
|
|
2402
|
+
continue
|
|
2403
|
+
|
|
2404
|
+
git_url = LIBRARY_GIT_URLS[lib_name]
|
|
2405
|
+
repo_name = extract_repo_name_from_url(git_url)
|
|
2406
|
+
|
|
2407
|
+
# Only add if not already present
|
|
2408
|
+
if repo_name not in current_repo_names:
|
|
2409
|
+
new_downloads.append(git_url)
|
|
2410
|
+
current_repo_names.add(repo_name)
|
|
2411
|
+
|
|
2412
|
+
return new_downloads
|
|
2413
|
+
|
|
2172
2414
|
async def reload_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
|
|
2173
2415
|
# Start with a clean slate.
|
|
2174
2416
|
clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
|
|
2175
2417
|
clear_all_result = await GriptapeNodes.ahandle_request(clear_all_request)
|
|
2176
2418
|
if not clear_all_result.succeeded():
|
|
2177
2419
|
details = "Failed to clear the existing object state when preparing to reload all libraries."
|
|
2178
|
-
logger.error(details)
|
|
2179
2420
|
return ReloadAllLibrariesResultFailure(result_details=details)
|
|
2180
2421
|
|
|
2181
2422
|
# Unload all libraries now.
|
|
@@ -2228,7 +2469,7 @@ class LibraryManager:
|
|
|
2228
2469
|
List of library file paths found
|
|
2229
2470
|
"""
|
|
2230
2471
|
config_mgr = GriptapeNodes.ConfigManager()
|
|
2231
|
-
user_libraries_section =
|
|
2472
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
2232
2473
|
|
|
2233
2474
|
discovered_libraries = set()
|
|
2234
2475
|
|
|
@@ -2250,3 +2491,758 @@ class LibraryManager:
|
|
|
2250
2491
|
process_path(library_path)
|
|
2251
2492
|
|
|
2252
2493
|
return list(discovered_libraries)
|
|
2494
|
+
|
|
2495
|
+
async def check_library_update_request(self, request: CheckLibraryUpdateRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
|
|
2496
|
+
"""Check if a library has updates available via git."""
|
|
2497
|
+
library_name = request.library_name
|
|
2498
|
+
|
|
2499
|
+
# Check if the library exists
|
|
2500
|
+
try:
|
|
2501
|
+
library = LibraryRegistry.get_library(name=library_name)
|
|
2502
|
+
except KeyError:
|
|
2503
|
+
details = f"Attempted to check for updates for Library '{library_name}'. Failed because no Library with that name was registered."
|
|
2504
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2505
|
+
|
|
2506
|
+
# Find the library file path
|
|
2507
|
+
library_file_path = None
|
|
2508
|
+
for file_path, library_info in self._library_file_path_to_info.items():
|
|
2509
|
+
if library_info.library_name == library_name:
|
|
2510
|
+
library_file_path = file_path
|
|
2511
|
+
break
|
|
2512
|
+
|
|
2513
|
+
if library_file_path is None:
|
|
2514
|
+
details = f"Attempted to check for updates for Library '{library_name}'. Failed because no file path could be found for this library."
|
|
2515
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2516
|
+
|
|
2517
|
+
# Get the library directory (parent of the JSON file)
|
|
2518
|
+
library_dir = Path(library_file_path).parent.absolute()
|
|
2519
|
+
|
|
2520
|
+
# Check if library is in a monorepo (multiple libraries in same git repository)
|
|
2521
|
+
if await asyncio.to_thread(is_monorepo, library_dir):
|
|
2522
|
+
details = (
|
|
2523
|
+
f"Library '{library_name}' is in a monorepo with multiple libraries. Updates must be managed manually."
|
|
2524
|
+
)
|
|
2525
|
+
logger.info(details)
|
|
2526
|
+
# Get git info for the response
|
|
2527
|
+
git_remote = await asyncio.to_thread(get_git_remote, library_dir)
|
|
2528
|
+
git_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2529
|
+
current_version = library.get_metadata().library_version
|
|
2530
|
+
return CheckLibraryUpdateResultSuccess(
|
|
2531
|
+
has_update=False,
|
|
2532
|
+
current_version=current_version,
|
|
2533
|
+
latest_version=current_version,
|
|
2534
|
+
git_remote=git_remote,
|
|
2535
|
+
git_ref=git_ref,
|
|
2536
|
+
local_commit=None,
|
|
2537
|
+
remote_commit=None,
|
|
2538
|
+
result_details=details,
|
|
2539
|
+
)
|
|
2540
|
+
|
|
2541
|
+
# Check if the library directory is a git repository and get remote URL and ref
|
|
2542
|
+
try:
|
|
2543
|
+
git_remote = await asyncio.to_thread(get_git_remote, library_dir)
|
|
2544
|
+
if git_remote is None:
|
|
2545
|
+
details = f"Library '{library_name}' is not a git repository or has no remote configured."
|
|
2546
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2547
|
+
except GitRemoteError as e:
|
|
2548
|
+
details = f"Failed to get git remote for Library '{library_name}': {e}"
|
|
2549
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2550
|
+
|
|
2551
|
+
try:
|
|
2552
|
+
git_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2553
|
+
except GitRefError as e:
|
|
2554
|
+
details = f"Failed to get current git reference for Library '{library_name}': {e}"
|
|
2555
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2556
|
+
|
|
2557
|
+
# Get current library version
|
|
2558
|
+
current_version = library.get_metadata().library_version
|
|
2559
|
+
if current_version is None:
|
|
2560
|
+
details = f"Library '{library_name}' has no version information."
|
|
2561
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2562
|
+
|
|
2563
|
+
# Get local commit SHA
|
|
2564
|
+
local_commit = await asyncio.to_thread(get_local_commit_sha, library_dir)
|
|
2565
|
+
|
|
2566
|
+
# Clone remote and get latest version and commit SHA (using current ref or HEAD if detached)
|
|
2567
|
+
try:
|
|
2568
|
+
ref_to_check = git_ref or "HEAD"
|
|
2569
|
+
version_info = await asyncio.to_thread(clone_and_get_library_version, git_remote, ref_to_check)
|
|
2570
|
+
latest_version = version_info.library_version
|
|
2571
|
+
remote_commit = version_info.commit_sha
|
|
2572
|
+
except GitCloneError as e:
|
|
2573
|
+
details = f"Failed to retrieve latest version from git remote for Library '{library_name}': {e}"
|
|
2574
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2575
|
+
|
|
2576
|
+
# Determine if update is available using version comparison and commit comparison
|
|
2577
|
+
try:
|
|
2578
|
+
current_ver = Version.parse(current_version)
|
|
2579
|
+
latest_ver = Version.parse(latest_version)
|
|
2580
|
+
|
|
2581
|
+
# Update detection logic:
|
|
2582
|
+
# 1. If remote version > local version -> update available (semantic versioning)
|
|
2583
|
+
if latest_ver > current_ver:
|
|
2584
|
+
has_update = True
|
|
2585
|
+
update_reason = "version increased"
|
|
2586
|
+
# 2. If remote version < local version -> no update (prevent regression)
|
|
2587
|
+
elif latest_ver < current_ver:
|
|
2588
|
+
has_update = False
|
|
2589
|
+
update_reason = "version decreased (regression blocked)"
|
|
2590
|
+
# 3. If versions equal -> check commits
|
|
2591
|
+
elif local_commit is not None and remote_commit is not None and local_commit != remote_commit:
|
|
2592
|
+
has_update = True
|
|
2593
|
+
update_reason = "commits differ (same version)"
|
|
2594
|
+
else:
|
|
2595
|
+
has_update = False
|
|
2596
|
+
update_reason = "versions and commits match"
|
|
2597
|
+
|
|
2598
|
+
except ValueError as e:
|
|
2599
|
+
details = f"Failed to parse version strings for Library '{library_name}': {e}"
|
|
2600
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2601
|
+
|
|
2602
|
+
details = f"Successfully checked for updates for Library '{library_name}'. Current version: {current_version}, Latest version: {latest_version}, Has update: {has_update} ({update_reason})"
|
|
2603
|
+
logger.info(details)
|
|
2604
|
+
|
|
2605
|
+
return CheckLibraryUpdateResultSuccess(
|
|
2606
|
+
has_update=has_update,
|
|
2607
|
+
current_version=current_version,
|
|
2608
|
+
latest_version=latest_version,
|
|
2609
|
+
git_remote=git_remote,
|
|
2610
|
+
git_ref=git_ref,
|
|
2611
|
+
local_commit=local_commit,
|
|
2612
|
+
remote_commit=remote_commit,
|
|
2613
|
+
result_details=details,
|
|
2614
|
+
)
|
|
2615
|
+
|
|
2616
|
+
async def _validate_and_prepare_library_for_git_operation(
|
|
2617
|
+
self,
|
|
2618
|
+
library_name: str,
|
|
2619
|
+
failure_result_class: type[ResultPayloadFailure],
|
|
2620
|
+
operation_description: str,
|
|
2621
|
+
) -> LibraryGitOperationContext | ResultPayloadFailure:
|
|
2622
|
+
"""Validate library exists and prepare for git operation.
|
|
2623
|
+
|
|
2624
|
+
Args:
|
|
2625
|
+
library_name: Name of the library to validate
|
|
2626
|
+
failure_result_class: Class to use for failure results (e.g., UpdateLibraryResultFailure)
|
|
2627
|
+
operation_description: Description of operation for error messages (e.g., "update", "switch branch/tag for")
|
|
2628
|
+
|
|
2629
|
+
Returns:
|
|
2630
|
+
On success: LibraryGitOperationContext with library info
|
|
2631
|
+
On failure: ResultPayloadFailure instance
|
|
2632
|
+
"""
|
|
2633
|
+
# Check if the library exists
|
|
2634
|
+
try:
|
|
2635
|
+
library = LibraryRegistry.get_library(name=library_name)
|
|
2636
|
+
except KeyError:
|
|
2637
|
+
details = f"Attempted to {operation_description} Library '{library_name}'. Failed because no Library with that name was registered."
|
|
2638
|
+
return failure_result_class(result_details=details)
|
|
2639
|
+
|
|
2640
|
+
# Get current version
|
|
2641
|
+
old_version = library.get_metadata().library_version
|
|
2642
|
+
if old_version is None:
|
|
2643
|
+
details = f"Library '{library_name}' has no version information."
|
|
2644
|
+
return failure_result_class(result_details=details)
|
|
2645
|
+
|
|
2646
|
+
# Find the library file path
|
|
2647
|
+
library_file_path = None
|
|
2648
|
+
for file_path, library_info in self._library_file_path_to_info.items():
|
|
2649
|
+
if library_info.library_name == library_name:
|
|
2650
|
+
library_file_path = file_path
|
|
2651
|
+
break
|
|
2652
|
+
|
|
2653
|
+
if library_file_path is None:
|
|
2654
|
+
details = f"Attempted to {operation_description} Library '{library_name}'. Failed because no file path could be found for this library."
|
|
2655
|
+
return failure_result_class(result_details=details)
|
|
2656
|
+
|
|
2657
|
+
# Get the library directory (parent of the JSON file)
|
|
2658
|
+
library_dir = Path(library_file_path).parent.absolute()
|
|
2659
|
+
|
|
2660
|
+
return LibraryGitOperationContext(
|
|
2661
|
+
library=library,
|
|
2662
|
+
old_version=old_version,
|
|
2663
|
+
library_file_path=library_file_path,
|
|
2664
|
+
library_dir=library_dir,
|
|
2665
|
+
)
|
|
2666
|
+
|
|
2667
|
+
async def _reload_library_after_git_operation(
|
|
2668
|
+
self,
|
|
2669
|
+
library_name: str,
|
|
2670
|
+
library_file_path: str,
|
|
2671
|
+
*,
|
|
2672
|
+
failure_result_class: type[ResultPayloadFailure],
|
|
2673
|
+
) -> str | ResultPayloadFailure:
|
|
2674
|
+
"""Reload library after git operation.
|
|
2675
|
+
|
|
2676
|
+
Args:
|
|
2677
|
+
library_name: Name of the library to reload
|
|
2678
|
+
library_file_path: Path to the library JSON file
|
|
2679
|
+
failure_result_class: Class to use for failure results
|
|
2680
|
+
|
|
2681
|
+
Returns:
|
|
2682
|
+
On success: new_version (str, may be "unknown")
|
|
2683
|
+
On failure: ResultPayloadFailure instance
|
|
2684
|
+
"""
|
|
2685
|
+
# Unload the library
|
|
2686
|
+
unload_result = GriptapeNodes.handle_request(UnloadLibraryFromRegistryRequest(library_name=library_name))
|
|
2687
|
+
if not unload_result.succeeded():
|
|
2688
|
+
details = f"Failed to unload Library '{library_name}' after git operation."
|
|
2689
|
+
return failure_result_class(result_details=details)
|
|
2690
|
+
|
|
2691
|
+
# Search for the library JSON file using flexible pattern to handle filename variations
|
|
2692
|
+
# (after git operations, the filename might change between griptape-nodes-library.json and griptape_nodes_library.json)
|
|
2693
|
+
library_dir = Path(library_file_path).parent
|
|
2694
|
+
actual_library_file = find_file_in_directory(library_dir, "griptape[-_]nodes[-_]library.json")
|
|
2695
|
+
|
|
2696
|
+
if actual_library_file is None:
|
|
2697
|
+
details = (
|
|
2698
|
+
f"Failed to find library JSON file in {library_dir} after git operation for Library '{library_name}'."
|
|
2699
|
+
)
|
|
2700
|
+
return failure_result_class(result_details=details)
|
|
2701
|
+
|
|
2702
|
+
# Use the found file path for reloading
|
|
2703
|
+
actual_library_file_path = str(actual_library_file)
|
|
2704
|
+
|
|
2705
|
+
# Reload the library from file
|
|
2706
|
+
reload_result = await GriptapeNodes.ahandle_request(
|
|
2707
|
+
RegisterLibraryFromFileRequest(file_path=actual_library_file_path)
|
|
2708
|
+
)
|
|
2709
|
+
if not isinstance(reload_result, RegisterLibraryFromFileResultSuccess):
|
|
2710
|
+
details = f"Failed to reload Library '{library_name}' after git operation."
|
|
2711
|
+
return failure_result_class(result_details=details)
|
|
2712
|
+
|
|
2713
|
+
# Get new version after reload
|
|
2714
|
+
try:
|
|
2715
|
+
updated_library = LibraryRegistry.get_library(name=library_name)
|
|
2716
|
+
new_version = updated_library.get_metadata().library_version
|
|
2717
|
+
if new_version is None:
|
|
2718
|
+
new_version = "unknown"
|
|
2719
|
+
except KeyError:
|
|
2720
|
+
new_version = "unknown"
|
|
2721
|
+
|
|
2722
|
+
return new_version
|
|
2723
|
+
|
|
2724
|
+
async def update_library_request(self, request: UpdateLibraryRequest) -> ResultPayload:
|
|
2725
|
+
"""Update a library to the latest version using the appropriate git strategy.
|
|
2726
|
+
|
|
2727
|
+
Automatically detects whether the library uses branch-based or tag-based workflow:
|
|
2728
|
+
- Branch-based: Uses git fetch + git reset --hard (forces local to match remote)
|
|
2729
|
+
- Tag-based: Uses git fetch --tags --force + git checkout
|
|
2730
|
+
"""
|
|
2731
|
+
library_name = request.library_name
|
|
2732
|
+
|
|
2733
|
+
# Validate library and prepare for git operation
|
|
2734
|
+
validation_result = await self._validate_and_prepare_library_for_git_operation(
|
|
2735
|
+
library_name=library_name,
|
|
2736
|
+
failure_result_class=UpdateLibraryResultFailure,
|
|
2737
|
+
operation_description="update",
|
|
2738
|
+
)
|
|
2739
|
+
if isinstance(validation_result, ResultPayloadFailure):
|
|
2740
|
+
return validation_result
|
|
2741
|
+
|
|
2742
|
+
old_version = validation_result.old_version
|
|
2743
|
+
library_file_path = validation_result.library_file_path
|
|
2744
|
+
library_dir = validation_result.library_dir
|
|
2745
|
+
|
|
2746
|
+
# Check if library is in a monorepo (multiple libraries in same git repository)
|
|
2747
|
+
if await asyncio.to_thread(is_monorepo, library_dir):
|
|
2748
|
+
details = f"Cannot update Library '{library_name}'. Repository contains multiple libraries and must be updated manually."
|
|
2749
|
+
return UpdateLibraryResultFailure(result_details=details)
|
|
2750
|
+
|
|
2751
|
+
# Perform git update (auto-detects branch vs tag workflow)
|
|
2752
|
+
try:
|
|
2753
|
+
await asyncio.to_thread(
|
|
2754
|
+
update_library_git,
|
|
2755
|
+
library_dir,
|
|
2756
|
+
overwrite_existing=request.overwrite_existing,
|
|
2757
|
+
)
|
|
2758
|
+
except (GitPullError, GitRepositoryError) as e:
|
|
2759
|
+
error_msg = str(e).lower()
|
|
2760
|
+
|
|
2761
|
+
# Check if error is retryable (uncommitted changes)
|
|
2762
|
+
retryable = "uncommitted changes" in error_msg or "unstaged changes" in error_msg
|
|
2763
|
+
|
|
2764
|
+
details = f"Failed to update Library '{library_name}': {e}"
|
|
2765
|
+
return UpdateLibraryResultFailure(result_details=details, retryable=retryable)
|
|
2766
|
+
|
|
2767
|
+
# Reload library
|
|
2768
|
+
reload_result = await self._reload_library_after_git_operation(
|
|
2769
|
+
library_name=library_name,
|
|
2770
|
+
library_file_path=library_file_path,
|
|
2771
|
+
failure_result_class=UpdateLibraryResultFailure,
|
|
2772
|
+
)
|
|
2773
|
+
if isinstance(reload_result, ResultPayloadFailure):
|
|
2774
|
+
return reload_result
|
|
2775
|
+
|
|
2776
|
+
new_version = reload_result
|
|
2777
|
+
|
|
2778
|
+
details = f"Successfully updated Library '{library_name}' from version {old_version} to {new_version}."
|
|
2779
|
+
return UpdateLibraryResultSuccess(
|
|
2780
|
+
old_version=old_version,
|
|
2781
|
+
new_version=new_version,
|
|
2782
|
+
result_details=details,
|
|
2783
|
+
)
|
|
2784
|
+
|
|
2785
|
+
async def switch_library_ref_request(self, request: SwitchLibraryRefRequest) -> ResultPayload:
|
|
2786
|
+
"""Switch a library to a different git branch or tag."""
|
|
2787
|
+
library_name = request.library_name
|
|
2788
|
+
ref_name = request.ref_name
|
|
2789
|
+
|
|
2790
|
+
# Validate library and prepare for git operation
|
|
2791
|
+
validation_result = await self._validate_and_prepare_library_for_git_operation(
|
|
2792
|
+
library_name=library_name,
|
|
2793
|
+
failure_result_class=SwitchLibraryRefResultFailure,
|
|
2794
|
+
operation_description="switch branch/tag for",
|
|
2795
|
+
)
|
|
2796
|
+
if isinstance(validation_result, ResultPayloadFailure):
|
|
2797
|
+
return validation_result
|
|
2798
|
+
|
|
2799
|
+
old_version = validation_result.old_version
|
|
2800
|
+
library_file_path = validation_result.library_file_path
|
|
2801
|
+
library_dir = validation_result.library_dir
|
|
2802
|
+
|
|
2803
|
+
# Get current ref (branch or tag) before switch
|
|
2804
|
+
try:
|
|
2805
|
+
old_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2806
|
+
if old_ref is None:
|
|
2807
|
+
details = f"Library '{library_name}' is not on a branch/tag or is not a git repository."
|
|
2808
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2809
|
+
except GitRefError as e:
|
|
2810
|
+
details = f"Failed to get current branch/tag for Library '{library_name}': {e}"
|
|
2811
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2812
|
+
|
|
2813
|
+
# Perform git ref switch (branch or tag)
|
|
2814
|
+
try:
|
|
2815
|
+
await asyncio.to_thread(switch_branch_or_tag, library_dir, ref_name)
|
|
2816
|
+
except (GitRefError, GitRepositoryError) as e:
|
|
2817
|
+
details = f"Failed to switch to '{ref_name}' for Library '{library_name}': {e}"
|
|
2818
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2819
|
+
|
|
2820
|
+
# Reload library
|
|
2821
|
+
reload_result = await self._reload_library_after_git_operation(
|
|
2822
|
+
library_name=library_name,
|
|
2823
|
+
library_file_path=library_file_path,
|
|
2824
|
+
failure_result_class=SwitchLibraryRefResultFailure,
|
|
2825
|
+
)
|
|
2826
|
+
if isinstance(reload_result, ResultPayloadFailure):
|
|
2827
|
+
return reload_result
|
|
2828
|
+
|
|
2829
|
+
new_version = reload_result
|
|
2830
|
+
|
|
2831
|
+
# Get new ref (branch or tag) after switch
|
|
2832
|
+
try:
|
|
2833
|
+
new_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2834
|
+
if new_ref is None:
|
|
2835
|
+
new_ref = "unknown"
|
|
2836
|
+
except GitRefError:
|
|
2837
|
+
new_ref = "unknown"
|
|
2838
|
+
|
|
2839
|
+
details = f"Successfully switched Library '{library_name}' from '{old_ref}' (version {old_version}) to '{new_ref}' (version {new_version})."
|
|
2840
|
+
return SwitchLibraryRefResultSuccess(
|
|
2841
|
+
old_ref=old_ref,
|
|
2842
|
+
new_ref=new_ref,
|
|
2843
|
+
old_version=old_version,
|
|
2844
|
+
new_version=new_version,
|
|
2845
|
+
result_details=details,
|
|
2846
|
+
)
|
|
2847
|
+
|
|
2848
|
+
async def download_library_request(self, request: DownloadLibraryRequest) -> ResultPayload: # noqa: PLR0911, PLR0912, PLR0915, C901
|
|
2849
|
+
"""Download a library from a git repository."""
|
|
2850
|
+
git_url = request.git_url
|
|
2851
|
+
branch_tag_commit = request.branch_tag_commit
|
|
2852
|
+
target_directory_name = request.target_directory_name
|
|
2853
|
+
download_directory = request.download_directory
|
|
2854
|
+
|
|
2855
|
+
# Determine the parent directory for the download
|
|
2856
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
2857
|
+
|
|
2858
|
+
if download_directory is not None:
|
|
2859
|
+
# Use custom download directory if provided
|
|
2860
|
+
libraries_path = Path(download_directory)
|
|
2861
|
+
else:
|
|
2862
|
+
# Use default from config
|
|
2863
|
+
libraries_dir_setting = config_mgr.get_config_value("libraries_directory")
|
|
2864
|
+
if not libraries_dir_setting:
|
|
2865
|
+
details = "Cannot download library: libraries_directory setting is not configured."
|
|
2866
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2867
|
+
libraries_path = config_mgr.workspace_path / libraries_dir_setting
|
|
2868
|
+
|
|
2869
|
+
# Ensure parent directory exists
|
|
2870
|
+
libraries_path.mkdir(parents=True, exist_ok=True)
|
|
2871
|
+
|
|
2872
|
+
# Determine target directory name
|
|
2873
|
+
if target_directory_name is None:
|
|
2874
|
+
# Extract from git URL (e.g., "https://github.com/user/repo.git" -> "repo")
|
|
2875
|
+
target_directory_name = git_url.rstrip("/").split("/")[-1]
|
|
2876
|
+
target_directory_name = target_directory_name.removesuffix(".git")
|
|
2877
|
+
|
|
2878
|
+
# Construct full target path
|
|
2879
|
+
target_path = libraries_path / target_directory_name
|
|
2880
|
+
|
|
2881
|
+
# Check if target directory already exists
|
|
2882
|
+
skip_clone = False
|
|
2883
|
+
if target_path.exists():
|
|
2884
|
+
if request.overwrite_existing:
|
|
2885
|
+
# Delete existing directory before cloning
|
|
2886
|
+
delete_request = DeleteFileRequest(path=str(target_path), workspace_only=False)
|
|
2887
|
+
delete_result = await GriptapeNodes.ahandle_request(delete_request)
|
|
2888
|
+
|
|
2889
|
+
if isinstance(delete_result, DeleteFileResultFailure):
|
|
2890
|
+
details = f"Cannot delete existing directory at {target_path}: {delete_result.result_details}"
|
|
2891
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2892
|
+
|
|
2893
|
+
logger.info("Deleted existing directory at %s for overwrite", target_path)
|
|
2894
|
+
else:
|
|
2895
|
+
# Check fail_on_exists flag
|
|
2896
|
+
if request.fail_on_exists:
|
|
2897
|
+
# Fail with retryable error for interactive CLI
|
|
2898
|
+
details = f"Cannot download library: target directory already exists at {target_path}"
|
|
2899
|
+
return DownloadLibraryResultFailure(result_details=details, retryable=True)
|
|
2900
|
+
|
|
2901
|
+
# Skip cloning since directory already exists, but continue with registration
|
|
2902
|
+
skip_clone = True
|
|
2903
|
+
logger.debug(
|
|
2904
|
+
"Library directory already exists at %s, skipping download but will proceed with registration",
|
|
2905
|
+
target_path,
|
|
2906
|
+
)
|
|
2907
|
+
|
|
2908
|
+
# Clone the repository (unless skipping because it already exists)
|
|
2909
|
+
if skip_clone:
|
|
2910
|
+
logger.debug("Using existing library directory at %s", target_path)
|
|
2911
|
+
else:
|
|
2912
|
+
try:
|
|
2913
|
+
await asyncio.to_thread(clone_repository, git_url, target_path, branch_tag_commit)
|
|
2914
|
+
except GitCloneError as e:
|
|
2915
|
+
details = f"Failed to clone repository from {git_url} to {target_path}: {e}"
|
|
2916
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2917
|
+
|
|
2918
|
+
# Recursively search for griptape_nodes_library.json file
|
|
2919
|
+
library_json_path = find_file_in_directory(target_path, "griptape[-_]nodes[-_]library.json")
|
|
2920
|
+
if library_json_path is None:
|
|
2921
|
+
details = f"Downloaded library from {git_url} but no library JSON file found in {target_path}"
|
|
2922
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2923
|
+
|
|
2924
|
+
try:
|
|
2925
|
+
with library_json_path.open() as f:
|
|
2926
|
+
library_data = json.load(f)
|
|
2927
|
+
except json.JSONDecodeError as e:
|
|
2928
|
+
details = f"Failed to parse griptape_nodes_library.json from downloaded library: {e}"
|
|
2929
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2930
|
+
|
|
2931
|
+
# Extract library name
|
|
2932
|
+
library_name = library_data.get("name")
|
|
2933
|
+
if library_name is None:
|
|
2934
|
+
details = "Downloaded library has no 'name' field in griptape_nodes_library.json"
|
|
2935
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2936
|
+
|
|
2937
|
+
# Automatically register the downloaded library (unless disabled for startup downloads)
|
|
2938
|
+
if request.auto_register:
|
|
2939
|
+
register_request = RegisterLibraryFromFileRequest(file_path=str(library_json_path))
|
|
2940
|
+
register_result = await GriptapeNodes.ahandle_request(register_request)
|
|
2941
|
+
if not register_result.succeeded():
|
|
2942
|
+
logger.warning(
|
|
2943
|
+
"Library '%s' was downloaded but registration failed: %s",
|
|
2944
|
+
library_name,
|
|
2945
|
+
register_result.result_details,
|
|
2946
|
+
)
|
|
2947
|
+
else:
|
|
2948
|
+
logger.info("Library '%s' registered successfully", library_name)
|
|
2949
|
+
|
|
2950
|
+
# Add library JSON file path to config so it's registered on future startups
|
|
2951
|
+
libraries_to_register = config_mgr.get_config_value(LIBRARIES_TO_REGISTER_KEY, default=[])
|
|
2952
|
+
library_json_str = str(library_json_path)
|
|
2953
|
+
if library_json_str not in libraries_to_register:
|
|
2954
|
+
libraries_to_register.append(library_json_str)
|
|
2955
|
+
config_mgr.set_config_value(LIBRARIES_TO_REGISTER_KEY, libraries_to_register)
|
|
2956
|
+
logger.info("Added library '%s' to config for auto-registration on startup", library_name)
|
|
2957
|
+
|
|
2958
|
+
if skip_clone:
|
|
2959
|
+
details = f"Library '{library_name}' already exists at {target_path} and has been registered"
|
|
2960
|
+
else:
|
|
2961
|
+
details = f"Successfully downloaded library '{library_name}' from {git_url} to {target_path}"
|
|
2962
|
+
return DownloadLibraryResultSuccess(
|
|
2963
|
+
library_name=library_name,
|
|
2964
|
+
library_path=str(library_json_path),
|
|
2965
|
+
result_details=details,
|
|
2966
|
+
)
|
|
2967
|
+
|
|
2968
|
+
async def install_library_dependencies_request(self, request: InstallLibraryDependenciesRequest) -> ResultPayload: # noqa: PLR0911
|
|
2969
|
+
"""Install dependencies for a library."""
|
|
2970
|
+
library_file_path = request.library_file_path
|
|
2971
|
+
|
|
2972
|
+
# Load library metadata from file
|
|
2973
|
+
metadata_request = LoadLibraryMetadataFromFileRequest(file_path=library_file_path)
|
|
2974
|
+
metadata_result = self.load_library_metadata_from_file_request(metadata_request)
|
|
2975
|
+
|
|
2976
|
+
if not isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
2977
|
+
details = f"Failed to load library metadata from {library_file_path}: {metadata_result.result_details}"
|
|
2978
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
2979
|
+
|
|
2980
|
+
library_data = metadata_result.library_schema
|
|
2981
|
+
library_name = library_data.name
|
|
2982
|
+
library_metadata = library_data.metadata
|
|
2983
|
+
|
|
2984
|
+
if not library_metadata.dependencies or not library_metadata.dependencies.pip_dependencies:
|
|
2985
|
+
details = f"Library '{library_name}' has no dependencies to install"
|
|
2986
|
+
logger.info(details)
|
|
2987
|
+
return InstallLibraryDependenciesResultSuccess(
|
|
2988
|
+
library_name=library_name, dependencies_installed=0, result_details=details
|
|
2989
|
+
)
|
|
2990
|
+
|
|
2991
|
+
pip_dependencies = library_metadata.dependencies.pip_dependencies
|
|
2992
|
+
pip_install_flags = library_metadata.dependencies.pip_install_flags or []
|
|
2993
|
+
|
|
2994
|
+
# Get venv path and initialize it
|
|
2995
|
+
venv_path = self._get_library_venv_path(library_name, library_file_path)
|
|
2996
|
+
|
|
2997
|
+
try:
|
|
2998
|
+
library_venv_python_path = await self._init_library_venv(venv_path)
|
|
2999
|
+
except RuntimeError as e:
|
|
3000
|
+
details = f"Failed to initialize venv for library '{library_name}': {e}"
|
|
3001
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3002
|
+
|
|
3003
|
+
if not self._can_write_to_venv_location(library_venv_python_path):
|
|
3004
|
+
details = f"Venv location for library '{library_name}' at {venv_path} is not writable"
|
|
3005
|
+
logger.warning(details)
|
|
3006
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3007
|
+
|
|
3008
|
+
# Check disk space
|
|
3009
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
3010
|
+
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
3011
|
+
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
3012
|
+
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
3013
|
+
details = f"Insufficient disk space for dependencies (requires {min_space_gb} GB) for library '{library_name}': {error_msg}"
|
|
3014
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3015
|
+
|
|
3016
|
+
# Install dependencies
|
|
3017
|
+
logger.info("Installing %d dependencies for library '%s'", len(pip_dependencies), library_name)
|
|
3018
|
+
is_debug = config_manager.get_config_value("log_level").upper() == "DEBUG"
|
|
3019
|
+
|
|
3020
|
+
try:
|
|
3021
|
+
await subprocess_run(
|
|
3022
|
+
[
|
|
3023
|
+
sys.executable,
|
|
3024
|
+
"-m",
|
|
3025
|
+
"uv",
|
|
3026
|
+
"pip",
|
|
3027
|
+
"install",
|
|
3028
|
+
*pip_dependencies,
|
|
3029
|
+
*pip_install_flags,
|
|
3030
|
+
"--python",
|
|
3031
|
+
str(library_venv_python_path),
|
|
3032
|
+
],
|
|
3033
|
+
check=True,
|
|
3034
|
+
capture_output=not is_debug,
|
|
3035
|
+
text=True,
|
|
3036
|
+
)
|
|
3037
|
+
except subprocess.CalledProcessError as e:
|
|
3038
|
+
details = f"Failed to install dependencies for library '{library_name}': return code={e.returncode}, stderr={e.stderr}"
|
|
3039
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3040
|
+
|
|
3041
|
+
details = f"Successfully installed {len(pip_dependencies)} dependencies for library '{library_name}'"
|
|
3042
|
+
logger.info(details)
|
|
3043
|
+
return InstallLibraryDependenciesResultSuccess(
|
|
3044
|
+
library_name=library_name, dependencies_installed=len(pip_dependencies), result_details=details
|
|
3045
|
+
)
|
|
3046
|
+
|
|
3047
|
+
async def sync_libraries_request(self, request: SyncLibrariesRequest) -> ResultPayload: # noqa: C901, PLR0915
|
|
3048
|
+
"""Sync all libraries to latest versions and ensure dependencies are installed."""
|
|
3049
|
+
# Phase 1: Download missing libraries from both config keys
|
|
3050
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
3051
|
+
|
|
3052
|
+
# Collect git URLs from both config keys
|
|
3053
|
+
download_config = config_mgr.get_config_value(LIBRARIES_TO_DOWNLOAD_KEY, default=[])
|
|
3054
|
+
register_config = config_mgr.get_config_value(LIBRARIES_TO_REGISTER_KEY, default=[])
|
|
3055
|
+
git_urls_from_register = [entry for entry in register_config if is_git_url(entry)]
|
|
3056
|
+
|
|
3057
|
+
# Combine and deduplicate
|
|
3058
|
+
all_git_urls = list(set(download_config + git_urls_from_register))
|
|
3059
|
+
|
|
3060
|
+
# Use shared download method
|
|
3061
|
+
update_summary = {}
|
|
3062
|
+
libraries_downloaded = 0
|
|
3063
|
+
|
|
3064
|
+
if all_git_urls:
|
|
3065
|
+
logger.info("Found %d git URLs, downloading missing libraries", len(all_git_urls))
|
|
3066
|
+
download_results = await self._download_libraries_from_git_urls(all_git_urls)
|
|
3067
|
+
|
|
3068
|
+
# Process results for summary
|
|
3069
|
+
for git_url, result in download_results.items():
|
|
3070
|
+
if result["success"]:
|
|
3071
|
+
libraries_downloaded += 1
|
|
3072
|
+
update_summary[result["library_name"]] = {
|
|
3073
|
+
"status": "downloaded",
|
|
3074
|
+
"git_url": git_url,
|
|
3075
|
+
}
|
|
3076
|
+
elif result.get("error"):
|
|
3077
|
+
logger.warning("Download failed for '%s': %s", git_url, result["error"])
|
|
3078
|
+
|
|
3079
|
+
logger.info("Downloaded %d new libraries", libraries_downloaded)
|
|
3080
|
+
|
|
3081
|
+
# Phase 2: Load libraries to ensure newly downloaded ones are registered
|
|
3082
|
+
logger.info("Loading libraries to register newly downloaded ones")
|
|
3083
|
+
load_request = LoadLibrariesRequest()
|
|
3084
|
+
load_result = await GriptapeNodes.ahandle_request(load_request)
|
|
3085
|
+
|
|
3086
|
+
if not isinstance(load_result, LoadLibrariesResultSuccess):
|
|
3087
|
+
logger.warning("Failed to load libraries after download: %s", load_result.result_details)
|
|
3088
|
+
# Continue anyway - we can still update previously registered libraries
|
|
3089
|
+
|
|
3090
|
+
# Phase 3: Check and update all registered libraries
|
|
3091
|
+
# Get all registered libraries
|
|
3092
|
+
list_result = await GriptapeNodes.ahandle_request(ListRegisteredLibrariesRequest())
|
|
3093
|
+
if not isinstance(list_result, ListRegisteredLibrariesResultSuccess):
|
|
3094
|
+
details = "Failed to list registered libraries for sync"
|
|
3095
|
+
return SyncLibrariesResultFailure(result_details=details)
|
|
3096
|
+
|
|
3097
|
+
libraries_to_check = list_result.libraries
|
|
3098
|
+
|
|
3099
|
+
logger.info("Checking %d registered libraries for updates", len(libraries_to_check))
|
|
3100
|
+
|
|
3101
|
+
# Check all libraries for updates concurrently using task group
|
|
3102
|
+
async def check_library_for_update(library_name: str) -> tuple[str, ResultPayload]:
|
|
3103
|
+
"""Check a single library for updates."""
|
|
3104
|
+
logger.info("Checking library '%s' for updates", library_name)
|
|
3105
|
+
check_result = await GriptapeNodes.ahandle_request(
|
|
3106
|
+
CheckLibraryUpdateRequest(library_name=library_name, failure_log_level=logging.DEBUG)
|
|
3107
|
+
)
|
|
3108
|
+
return library_name, check_result
|
|
3109
|
+
|
|
3110
|
+
# Gather all check results concurrently
|
|
3111
|
+
check_results: dict[str, ResultPayload] = {}
|
|
3112
|
+
async with asyncio.TaskGroup() as tg:
|
|
3113
|
+
tasks = [tg.create_task(check_library_for_update(lib)) for lib in libraries_to_check]
|
|
3114
|
+
|
|
3115
|
+
# Collect results from completed tasks
|
|
3116
|
+
for task in tasks:
|
|
3117
|
+
library_name, result = task.result()
|
|
3118
|
+
check_results[library_name] = result
|
|
3119
|
+
|
|
3120
|
+
# Process check results and determine which libraries need updates
|
|
3121
|
+
libraries_checked = len(libraries_to_check)
|
|
3122
|
+
libraries_updated = 0
|
|
3123
|
+
libraries_to_update: list[LibraryUpdateInfo] = []
|
|
3124
|
+
|
|
3125
|
+
for library_name, check_result in check_results.items():
|
|
3126
|
+
if not isinstance(check_result, CheckLibraryUpdateResultSuccess):
|
|
3127
|
+
logger.warning(
|
|
3128
|
+
"Failed to check for updates for library '%s', skipping: %s",
|
|
3129
|
+
library_name,
|
|
3130
|
+
str(check_result.result_details),
|
|
3131
|
+
)
|
|
3132
|
+
continue
|
|
3133
|
+
|
|
3134
|
+
if not check_result.has_update:
|
|
3135
|
+
logger.info("Library '%s' is up to date (version %s)", library_name, check_result.current_version)
|
|
3136
|
+
continue
|
|
3137
|
+
|
|
3138
|
+
# Library has an update available
|
|
3139
|
+
old_version = check_result.current_version or "unknown"
|
|
3140
|
+
new_version = check_result.latest_version or "unknown"
|
|
3141
|
+
logger.info("Library '%s' has update available: %s -> %s", library_name, old_version, new_version)
|
|
3142
|
+
libraries_to_update.append(
|
|
3143
|
+
LibraryUpdateInfo(library_name=library_name, old_version=old_version, new_version=new_version)
|
|
3144
|
+
)
|
|
3145
|
+
|
|
3146
|
+
# Update libraries concurrently using task group
|
|
3147
|
+
async def update_library(library_name: str, old_version: str, new_version: str) -> LibraryUpdateResult:
|
|
3148
|
+
"""Update a single library."""
|
|
3149
|
+
logger.info("Updating library '%s' from %s to %s", library_name, old_version, new_version)
|
|
3150
|
+
update_result = await GriptapeNodes.ahandle_request(
|
|
3151
|
+
UpdateLibraryRequest(
|
|
3152
|
+
library_name=library_name,
|
|
3153
|
+
overwrite_existing=request.overwrite_existing,
|
|
3154
|
+
)
|
|
3155
|
+
)
|
|
3156
|
+
return LibraryUpdateResult(
|
|
3157
|
+
library_name=library_name,
|
|
3158
|
+
old_version=old_version,
|
|
3159
|
+
new_version=new_version,
|
|
3160
|
+
result=update_result,
|
|
3161
|
+
)
|
|
3162
|
+
|
|
3163
|
+
# Gather all update results concurrently
|
|
3164
|
+
async with asyncio.TaskGroup() as tg:
|
|
3165
|
+
update_tasks = [
|
|
3166
|
+
tg.create_task(update_library(info.library_name, info.old_version, info.new_version))
|
|
3167
|
+
for info in libraries_to_update
|
|
3168
|
+
]
|
|
3169
|
+
|
|
3170
|
+
# Collect update results
|
|
3171
|
+
for task in update_tasks:
|
|
3172
|
+
result = task.result()
|
|
3173
|
+
library_name = result.library_name
|
|
3174
|
+
old_version = result.old_version
|
|
3175
|
+
new_version = result.new_version
|
|
3176
|
+
update_result = result.result
|
|
3177
|
+
|
|
3178
|
+
if not isinstance(update_result, UpdateLibraryResultSuccess):
|
|
3179
|
+
logger.error("Failed to update library '%s': %s", library_name, update_result.result_details)
|
|
3180
|
+
update_summary[library_name] = {
|
|
3181
|
+
"old_version": old_version,
|
|
3182
|
+
"new_version": old_version,
|
|
3183
|
+
"status": "failed",
|
|
3184
|
+
"error": update_result.result_details,
|
|
3185
|
+
}
|
|
3186
|
+
continue
|
|
3187
|
+
|
|
3188
|
+
libraries_updated += 1
|
|
3189
|
+
update_summary[library_name] = {
|
|
3190
|
+
"old_version": update_result.old_version,
|
|
3191
|
+
"new_version": update_result.new_version,
|
|
3192
|
+
"status": "updated",
|
|
3193
|
+
}
|
|
3194
|
+
logger.info(
|
|
3195
|
+
"Successfully updated library '%s' from %s to %s",
|
|
3196
|
+
library_name,
|
|
3197
|
+
update_result.old_version,
|
|
3198
|
+
update_result.new_version,
|
|
3199
|
+
)
|
|
3200
|
+
|
|
3201
|
+
# Build result details
|
|
3202
|
+
details = f"Downloaded {libraries_downloaded} libraries. Checked {libraries_checked} libraries. {libraries_updated} updated."
|
|
3203
|
+
logger.info(details)
|
|
3204
|
+
return SyncLibrariesResultSuccess(
|
|
3205
|
+
libraries_downloaded=libraries_downloaded,
|
|
3206
|
+
libraries_checked=libraries_checked,
|
|
3207
|
+
libraries_updated=libraries_updated,
|
|
3208
|
+
update_summary=update_summary,
|
|
3209
|
+
result_details=details,
|
|
3210
|
+
)
|
|
3211
|
+
|
|
3212
|
+
async def inspect_library_repo_request(self, request: InspectLibraryRepoRequest) -> ResultPayload:
|
|
3213
|
+
"""Inspect a library's metadata from a git repository without downloading the full repository."""
|
|
3214
|
+
git_url = request.git_url
|
|
3215
|
+
ref = request.ref
|
|
3216
|
+
|
|
3217
|
+
# Normalize GitHub shorthand to full URL
|
|
3218
|
+
from griptape_nodes.utils.git_utils import normalize_github_url, sparse_checkout_library_json
|
|
3219
|
+
|
|
3220
|
+
normalized_url = normalize_github_url(git_url)
|
|
3221
|
+
logger.info("Inspecting library metadata from '%s' (ref: %s)", normalized_url, ref)
|
|
3222
|
+
|
|
3223
|
+
# Perform sparse checkout to get library JSON
|
|
3224
|
+
try:
|
|
3225
|
+
library_version, commit_sha, library_data_raw = sparse_checkout_library_json(normalized_url, ref)
|
|
3226
|
+
except GitCloneError as e:
|
|
3227
|
+
details = f"Failed to inspect library from {normalized_url}: {e}"
|
|
3228
|
+
logger.error(details)
|
|
3229
|
+
return InspectLibraryRepoResultFailure(result_details=details)
|
|
3230
|
+
|
|
3231
|
+
# Validate and create LibrarySchema
|
|
3232
|
+
try:
|
|
3233
|
+
library_schema = LibrarySchema(**library_data_raw)
|
|
3234
|
+
except Exception as e:
|
|
3235
|
+
details = f"Invalid library schema from {normalized_url}: {e}"
|
|
3236
|
+
logger.error(details)
|
|
3237
|
+
return InspectLibraryRepoResultFailure(result_details=details)
|
|
3238
|
+
|
|
3239
|
+
# Return success with full library metadata
|
|
3240
|
+
details = f"Successfully inspected library '{library_schema.name}' (version {library_version}) from {normalized_url} at commit {commit_sha[:7]}"
|
|
3241
|
+
logger.info(details)
|
|
3242
|
+
return InspectLibraryRepoResultSuccess(
|
|
3243
|
+
library_schema=library_schema,
|
|
3244
|
+
commit_sha=commit_sha,
|
|
3245
|
+
git_url=normalized_url,
|
|
3246
|
+
ref=ref,
|
|
3247
|
+
result_details=details,
|
|
3248
|
+
)
|