griptape-nodes 0.65.6__py3-none-any.whl → 0.66.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/common/node_executor.py +352 -27
- griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
- griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
- griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
- griptape_nodes/exe_types/connections.py +42 -0
- griptape_nodes/exe_types/core_types.py +2 -2
- griptape_nodes/exe_types/node_groups/__init__.py +2 -1
- griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
- griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
- griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
- griptape_nodes/machines/control_flow.py +5 -4
- griptape_nodes/machines/dag_builder.py +121 -55
- griptape_nodes/machines/fsm.py +10 -0
- griptape_nodes/machines/parallel_resolution.py +39 -38
- griptape_nodes/machines/sequential_resolution.py +29 -3
- griptape_nodes/node_library/library_registry.py +41 -2
- griptape_nodes/retained_mode/events/library_events.py +147 -8
- griptape_nodes/retained_mode/events/os_events.py +12 -4
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
- griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
- griptape_nodes/retained_mode/managers/node_manager.py +9 -3
- griptape_nodes/retained_mode/managers/os_manager.py +429 -65
- griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
- griptape_nodes/utils/git_utils.py +2 -17
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.0.dist-info}/RECORD +49 -54
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
"""Library directory for managing library candidates."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.data_models import LibraryCandidate, LifecycleIssue
|
|
9
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_fsm import EvaluatedState, LibraryLifecycleFSM
|
|
10
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.data_models import LibraryEntry
|
|
14
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance import LibraryProvenance
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger("griptape_nodes")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class LibraryDirectory:
|
|
20
|
-
"""Unified registry of all known libraries - both curated and user-added.
|
|
21
|
-
|
|
22
|
-
This class manages discovery of libraries and works with LibraryPreferences
|
|
23
|
-
for configuration. It's responsible for finding libraries and their provenances,
|
|
24
|
-
while LibraryPreferences handles user configuration.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self) -> None:
|
|
28
|
-
# This is now just a discovery mechanism - the actual configuration
|
|
29
|
-
# lives in LibraryPreferences
|
|
30
|
-
# Key: provenance, Value: entry
|
|
31
|
-
self._discovered_libraries: dict[LibraryProvenance, LibraryEntry] = {}
|
|
32
|
-
# Track library name conflicts centrally
|
|
33
|
-
# Key: library name, Value: set of provenances with that name
|
|
34
|
-
self._library_name_to_provenances: dict[str, set[LibraryProvenance]] = {}
|
|
35
|
-
# Own all FSM instances for library lifecycle management
|
|
36
|
-
self._provenance_to_fsm: dict[LibraryProvenance, LibraryLifecycleFSM] = {}
|
|
37
|
-
|
|
38
|
-
async def discover_library(self, provenance: LibraryProvenance) -> None:
|
|
39
|
-
"""Discover a library and its provenance.
|
|
40
|
-
|
|
41
|
-
Discovery is purely about cataloging - activation state is handled separately.
|
|
42
|
-
"""
|
|
43
|
-
# Check if already discovered
|
|
44
|
-
if provenance in self._discovered_libraries:
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
# Create entry with neutral active state (will be set by specific methods)
|
|
48
|
-
entry = provenance.create_library_entry(active=False)
|
|
49
|
-
self._discovered_libraries[provenance] = entry
|
|
50
|
-
|
|
51
|
-
# Create FSM and run evaluation automatically
|
|
52
|
-
await self._create_fsm_and_evaluate(provenance)
|
|
53
|
-
|
|
54
|
-
async def add_curated_candidate(self, provenance: LibraryProvenance) -> None:
|
|
55
|
-
"""Add a curated library candidate.
|
|
56
|
-
|
|
57
|
-
Curated libraries default to inactive and need to be activated by user.
|
|
58
|
-
"""
|
|
59
|
-
await self.discover_library(provenance)
|
|
60
|
-
|
|
61
|
-
# Set curated library as inactive by default
|
|
62
|
-
if provenance in self._discovered_libraries:
|
|
63
|
-
entry = self._discovered_libraries[provenance]
|
|
64
|
-
entry.active = False
|
|
65
|
-
|
|
66
|
-
async def add_user_candidate(self, provenance: LibraryProvenance) -> None:
|
|
67
|
-
"""Add a user-supplied library candidate.
|
|
68
|
-
|
|
69
|
-
User libraries default to active.
|
|
70
|
-
"""
|
|
71
|
-
await self.discover_library(provenance)
|
|
72
|
-
|
|
73
|
-
# Set user library as active by default
|
|
74
|
-
if provenance in self._discovered_libraries:
|
|
75
|
-
entry = self._discovered_libraries[provenance]
|
|
76
|
-
entry.active = True
|
|
77
|
-
|
|
78
|
-
def get_all_candidates(self) -> list[LibraryCandidate]:
|
|
79
|
-
"""Get all known library candidates with their entries.
|
|
80
|
-
|
|
81
|
-
Returns list of LibraryCandidate named tuples.
|
|
82
|
-
"""
|
|
83
|
-
candidates = []
|
|
84
|
-
for provenance, entry in self._discovered_libraries.items():
|
|
85
|
-
candidates.append(LibraryCandidate(provenance=provenance, entry=entry))
|
|
86
|
-
return candidates
|
|
87
|
-
|
|
88
|
-
def get_active_candidates(self) -> list[LibraryCandidate]:
|
|
89
|
-
"""Get all candidates that should be active.
|
|
90
|
-
|
|
91
|
-
Returns list of LibraryCandidate named tuples for active libraries.
|
|
92
|
-
"""
|
|
93
|
-
all_candidates = self.get_all_candidates()
|
|
94
|
-
return [candidate for candidate in all_candidates if candidate.entry.active]
|
|
95
|
-
|
|
96
|
-
def get_candidate(self, provenance: LibraryProvenance) -> LibraryEntry | None:
|
|
97
|
-
"""Get a specific library candidate entry by provenance."""
|
|
98
|
-
return self._discovered_libraries.get(provenance)
|
|
99
|
-
|
|
100
|
-
def remove_candidate(self, provenance: LibraryProvenance) -> None:
|
|
101
|
-
"""Remove a library candidate from discovery."""
|
|
102
|
-
self._discovered_libraries.pop(provenance, None)
|
|
103
|
-
# Remove from name mapping
|
|
104
|
-
for library_name, provenances in list(self._library_name_to_provenances.items()):
|
|
105
|
-
provenances.discard(provenance)
|
|
106
|
-
if not provenances: # Remove empty sets
|
|
107
|
-
del self._library_name_to_provenances[library_name]
|
|
108
|
-
# Remove FSM
|
|
109
|
-
self._provenance_to_fsm.pop(provenance, None)
|
|
110
|
-
|
|
111
|
-
def clear(self) -> None:
|
|
112
|
-
"""Clear all library candidates."""
|
|
113
|
-
self._discovered_libraries.clear()
|
|
114
|
-
self._library_name_to_provenances.clear()
|
|
115
|
-
self._provenance_to_fsm.clear()
|
|
116
|
-
|
|
117
|
-
def get_discovered_libraries(self) -> dict[LibraryProvenance, LibraryEntry]:
|
|
118
|
-
"""Get all discovered libraries and their entries.
|
|
119
|
-
|
|
120
|
-
Returns dict mapping provenance -> entry.
|
|
121
|
-
"""
|
|
122
|
-
return self._discovered_libraries.copy()
|
|
123
|
-
|
|
124
|
-
def get_conflicting_provenances(self, library_name: str) -> set[LibraryProvenance]:
|
|
125
|
-
"""Get all provenances that have the given library name.
|
|
126
|
-
|
|
127
|
-
Returns empty set if library name not found.
|
|
128
|
-
"""
|
|
129
|
-
return self._library_name_to_provenances.get(library_name, set()).copy()
|
|
130
|
-
|
|
131
|
-
def has_library_name_conflicts(self, library_name: str) -> bool:
|
|
132
|
-
"""Check if a library name has conflicts (more than one provenance)."""
|
|
133
|
-
return len(self._library_name_to_provenances.get(library_name, set())) > 1
|
|
134
|
-
|
|
135
|
-
def get_all_conflicting_library_names(self) -> list[str]:
|
|
136
|
-
"""Get all library names that have conflicts."""
|
|
137
|
-
return [name for name, provenances in self._library_name_to_provenances.items() if len(provenances) > 1]
|
|
138
|
-
|
|
139
|
-
def can_install_library(self, provenance: LibraryProvenance, library_name: str) -> bool: # noqa: ARG002
|
|
140
|
-
"""Check if a library can be installed (no name conflicts)."""
|
|
141
|
-
return not self.has_library_name_conflicts(library_name)
|
|
142
|
-
|
|
143
|
-
def get_conflicting_library_display_names(
|
|
144
|
-
self, library_name: str, excluding_provenance: LibraryProvenance | None = None
|
|
145
|
-
) -> list[str]:
|
|
146
|
-
"""Get display names of libraries that conflict with the given library name.
|
|
147
|
-
|
|
148
|
-
Optionally exclude a specific provenance from the results.
|
|
149
|
-
"""
|
|
150
|
-
conflicting_provenances = self.get_conflicting_provenances(library_name)
|
|
151
|
-
if excluding_provenance:
|
|
152
|
-
conflicting_provenances.discard(excluding_provenance)
|
|
153
|
-
return [p.get_display_name() for p in conflicting_provenances]
|
|
154
|
-
|
|
155
|
-
def get_installable_candidates(self) -> list[LibraryCandidate]:
|
|
156
|
-
"""Get all active candidates that are ready for installation (evaluated, usable, no conflicts)."""
|
|
157
|
-
active_candidates = self.get_active_candidates()
|
|
158
|
-
return [
|
|
159
|
-
candidate for candidate in active_candidates if not self.get_installation_blockers(candidate.provenance)
|
|
160
|
-
]
|
|
161
|
-
|
|
162
|
-
def get_installation_blockers(self, provenance: LibraryProvenance) -> list[LifecycleIssue]:
|
|
163
|
-
"""Get all issues preventing this library from being installed."""
|
|
164
|
-
blockers = []
|
|
165
|
-
fsm = self._provenance_to_fsm.get(provenance)
|
|
166
|
-
|
|
167
|
-
if not fsm:
|
|
168
|
-
blockers.append(LifecycleIssue(message="No FSM found for library", severity=LibraryStatus.MISSING))
|
|
169
|
-
return blockers
|
|
170
|
-
|
|
171
|
-
# Check if library is in evaluated state
|
|
172
|
-
if fsm.current_state != EvaluatedState:
|
|
173
|
-
blockers.append(
|
|
174
|
-
LifecycleIssue(
|
|
175
|
-
message=f"Library not in evaluated state: {fsm.get_current_state_name()}",
|
|
176
|
-
severity=LibraryStatus.UNUSABLE,
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
return blockers
|
|
180
|
-
|
|
181
|
-
context = fsm.get_context()
|
|
182
|
-
|
|
183
|
-
# Check if inspection result is usable
|
|
184
|
-
if not context.inspection_result or not context.inspection_result.is_usable():
|
|
185
|
-
blockers.append(LifecycleIssue(message="Library has inspection issues", severity=LibraryStatus.UNUSABLE))
|
|
186
|
-
return blockers
|
|
187
|
-
|
|
188
|
-
# Check if schema is available
|
|
189
|
-
if not context.inspection_result.schema:
|
|
190
|
-
blockers.append(LifecycleIssue(message="Library schema not available", severity=LibraryStatus.UNUSABLE))
|
|
191
|
-
return blockers
|
|
192
|
-
|
|
193
|
-
# Check for name conflicts
|
|
194
|
-
library_name = context.inspection_result.schema.name
|
|
195
|
-
if self.has_library_name_conflicts(library_name):
|
|
196
|
-
conflicting_libraries = self.get_conflicting_library_display_names(library_name, provenance)
|
|
197
|
-
blockers.append(
|
|
198
|
-
LifecycleIssue(
|
|
199
|
-
message=f"Library has name conflicts with: {conflicting_libraries}", severity=LibraryStatus.FLAWED
|
|
200
|
-
)
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
return blockers
|
|
204
|
-
|
|
205
|
-
async def _create_fsm_and_evaluate(self, provenance: LibraryProvenance) -> None:
|
|
206
|
-
"""Create FSM for provenance and run through evaluation phase.
|
|
207
|
-
|
|
208
|
-
This method is called automatically when a library is discovered.
|
|
209
|
-
"""
|
|
210
|
-
logger.debug("Creating FSM and starting evaluation for library: %s", provenance.get_display_name())
|
|
211
|
-
|
|
212
|
-
# Create FSM instance for this library
|
|
213
|
-
fsm = LibraryLifecycleFSM(provenance)
|
|
214
|
-
self._provenance_to_fsm[provenance] = fsm
|
|
215
|
-
|
|
216
|
-
# Start the lifecycle and run through evaluation
|
|
217
|
-
await fsm.start_lifecycle()
|
|
218
|
-
|
|
219
|
-
# Progress through inspection
|
|
220
|
-
if fsm.can_begin_inspection():
|
|
221
|
-
await fsm.begin_inspection()
|
|
222
|
-
else:
|
|
223
|
-
logger.error(
|
|
224
|
-
"Cannot inspect library '%s' - inspection step cannot proceed",
|
|
225
|
-
provenance.get_display_name(),
|
|
226
|
-
)
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
# Progress through evaluation
|
|
230
|
-
if fsm.can_begin_evaluation():
|
|
231
|
-
await fsm.begin_evaluation()
|
|
232
|
-
else:
|
|
233
|
-
logger.error(
|
|
234
|
-
"Cannot evaluate library '%s' - evaluation step cannot proceed",
|
|
235
|
-
provenance.get_display_name(),
|
|
236
|
-
)
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
# Update library name mapping after successful inspection and evaluation
|
|
240
|
-
# At this point, we know inspection_result and schema are valid since we completed both phases
|
|
241
|
-
context = fsm.get_context()
|
|
242
|
-
if context.inspection_result and context.inspection_result.schema:
|
|
243
|
-
library_name = context.inspection_result.schema.name
|
|
244
|
-
if library_name not in self._library_name_to_provenances:
|
|
245
|
-
self._library_name_to_provenances[library_name] = set()
|
|
246
|
-
self._library_name_to_provenances[library_name].add(provenance)
|
|
247
|
-
|
|
248
|
-
logger.debug("Completed FSM evaluation for library: %s", provenance.get_display_name())
|
|
249
|
-
|
|
250
|
-
async def install_library(self, provenance: LibraryProvenance) -> bool:
|
|
251
|
-
"""Install a library by running its FSM through the installation phase.
|
|
252
|
-
|
|
253
|
-
Returns True if installation was successful, False otherwise.
|
|
254
|
-
"""
|
|
255
|
-
fsm = self._provenance_to_fsm.get(provenance)
|
|
256
|
-
if not fsm:
|
|
257
|
-
logger.error("No FSM found for provenance: %s", provenance.get_display_name())
|
|
258
|
-
return False
|
|
259
|
-
|
|
260
|
-
# Check if library has name conflicts that prevent installation
|
|
261
|
-
context = fsm.get_context()
|
|
262
|
-
if context.inspection_result and context.inspection_result.schema:
|
|
263
|
-
library_name = context.inspection_result.schema.name
|
|
264
|
-
if self.has_library_name_conflicts(library_name):
|
|
265
|
-
conflicting_libraries = self.get_conflicting_library_display_names(library_name, provenance)
|
|
266
|
-
logger.error(
|
|
267
|
-
"Cannot install library '%s' due to name conflicts with: %s",
|
|
268
|
-
provenance.get_display_name(),
|
|
269
|
-
conflicting_libraries,
|
|
270
|
-
)
|
|
271
|
-
return False
|
|
272
|
-
|
|
273
|
-
# Proceed with installation
|
|
274
|
-
if fsm.can_begin_installation():
|
|
275
|
-
await fsm.begin_installation()
|
|
276
|
-
logger.info("Installation completed for library: %s", provenance.get_display_name())
|
|
277
|
-
return True
|
|
278
|
-
logger.error(
|
|
279
|
-
"Cannot install library '%s' - installation step cannot proceed",
|
|
280
|
-
provenance.get_display_name(),
|
|
281
|
-
)
|
|
282
|
-
return False
|
|
283
|
-
|
|
284
|
-
async def load_library(self, provenance: LibraryProvenance) -> bool:
|
|
285
|
-
"""Load a library by running its FSM through the loading phase.
|
|
286
|
-
|
|
287
|
-
Returns True if loading was successful, False otherwise.
|
|
288
|
-
"""
|
|
289
|
-
fsm = self._provenance_to_fsm.get(provenance)
|
|
290
|
-
if not fsm:
|
|
291
|
-
logger.error("No FSM found for provenance: %s", provenance.get_display_name())
|
|
292
|
-
return False
|
|
293
|
-
|
|
294
|
-
if not fsm.can_begin_loading():
|
|
295
|
-
logger.error(
|
|
296
|
-
"Cannot load library '%s' - loading step cannot proceed",
|
|
297
|
-
provenance.get_display_name(),
|
|
298
|
-
)
|
|
299
|
-
return False
|
|
300
|
-
|
|
301
|
-
# Proceed with loading
|
|
302
|
-
await fsm.begin_loading()
|
|
303
|
-
|
|
304
|
-
if not fsm.is_loaded():
|
|
305
|
-
logger.error(
|
|
306
|
-
"Failed to load library '%s' - did not reach loaded state: %s",
|
|
307
|
-
provenance.get_display_name(),
|
|
308
|
-
fsm.get_current_state_name(),
|
|
309
|
-
)
|
|
310
|
-
return False
|
|
311
|
-
|
|
312
|
-
logger.info("Successfully loaded library '%s'", provenance.get_display_name())
|
|
313
|
-
return True
|
|
314
|
-
|
|
315
|
-
def get_library_name_from_provenance(self, provenance: LibraryProvenance) -> str | None:
|
|
316
|
-
"""Get the library name for a given provenance after evaluation.
|
|
317
|
-
|
|
318
|
-
Returns None if the provenance hasn't been evaluated or doesn't have a valid schema.
|
|
319
|
-
"""
|
|
320
|
-
fsm = self._provenance_to_fsm.get(provenance)
|
|
321
|
-
if not fsm:
|
|
322
|
-
return None
|
|
323
|
-
|
|
324
|
-
context = fsm.get_context()
|
|
325
|
-
if not context.inspection_result or not context.inspection_result.schema:
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
return context.inspection_result.schema.name
|
|
329
|
-
|
|
330
|
-
def get_provenances_for_library_name(self, library_name: str) -> list[LibraryProvenance]:
|
|
331
|
-
"""Get all provenances that have the given library name.
|
|
332
|
-
|
|
333
|
-
Returns empty list if library name not found.
|
|
334
|
-
"""
|
|
335
|
-
return list(self._library_name_to_provenances.get(library_name, set()))
|
|
336
|
-
|
|
337
|
-
def find_provenance_by_library_name(self, library_name: str) -> LibraryProvenance | None:
|
|
338
|
-
"""Find a single provenance for a library name.
|
|
339
|
-
|
|
340
|
-
Returns None if library name not found or if there are multiple provenances
|
|
341
|
-
(indicating a conflict that should be resolved first).
|
|
342
|
-
"""
|
|
343
|
-
provenances = self.get_provenances_for_library_name(library_name)
|
|
344
|
-
if len(provenances) == 1:
|
|
345
|
-
return provenances[0]
|
|
346
|
-
return None
|