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.
Files changed (55) hide show
  1. griptape_nodes/app/app.py +25 -5
  2. griptape_nodes/cli/commands/init.py +65 -54
  3. griptape_nodes/cli/commands/libraries.py +92 -85
  4. griptape_nodes/cli/commands/self.py +121 -0
  5. griptape_nodes/common/node_executor.py +2142 -101
  6. griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
  7. griptape_nodes/exe_types/connections.py +114 -19
  8. griptape_nodes/exe_types/core_types.py +225 -7
  9. griptape_nodes/exe_types/flow.py +3 -3
  10. griptape_nodes/exe_types/node_types.py +681 -225
  11. griptape_nodes/exe_types/param_components/README.md +414 -0
  12. griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
  13. griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
  14. griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
  15. griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
  16. griptape_nodes/machines/control_flow.py +77 -38
  17. griptape_nodes/machines/dag_builder.py +148 -70
  18. griptape_nodes/machines/parallel_resolution.py +61 -35
  19. griptape_nodes/machines/sequential_resolution.py +11 -113
  20. griptape_nodes/retained_mode/events/app_events.py +1 -0
  21. griptape_nodes/retained_mode/events/base_events.py +16 -13
  22. griptape_nodes/retained_mode/events/connection_events.py +3 -0
  23. griptape_nodes/retained_mode/events/execution_events.py +35 -0
  24. griptape_nodes/retained_mode/events/flow_events.py +15 -2
  25. griptape_nodes/retained_mode/events/library_events.py +347 -0
  26. griptape_nodes/retained_mode/events/node_events.py +48 -0
  27. griptape_nodes/retained_mode/events/os_events.py +86 -3
  28. griptape_nodes/retained_mode/events/project_events.py +15 -1
  29. griptape_nodes/retained_mode/events/workflow_events.py +48 -1
  30. griptape_nodes/retained_mode/griptape_nodes.py +6 -2
  31. griptape_nodes/retained_mode/managers/config_manager.py +10 -8
  32. griptape_nodes/retained_mode/managers/event_manager.py +168 -0
  33. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
  34. griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
  35. griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
  36. griptape_nodes/retained_mode/managers/library_manager.py +1134 -138
  37. griptape_nodes/retained_mode/managers/model_manager.py +2 -3
  38. griptape_nodes/retained_mode/managers/node_manager.py +148 -25
  39. griptape_nodes/retained_mode/managers/object_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
  41. griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
  42. griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
  43. griptape_nodes/retained_mode/managers/settings.py +21 -1
  44. griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
  45. griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
  46. griptape_nodes/retained_mode/retained_mode.py +3 -3
  47. griptape_nodes/traits/button.py +44 -2
  48. griptape_nodes/traits/file_system_picker.py +2 -2
  49. griptape_nodes/utils/file_utils.py +101 -0
  50. griptape_nodes/utils/git_utils.py +1226 -0
  51. griptape_nodes/utils/library_utils.py +122 -0
  52. {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
  53. {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
  54. {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
  55. {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
@@ -5,14 +5,26 @@ from griptape_nodes.exe_types.param_components.huggingface.huggingface_model_par
5
5
  from griptape_nodes.exe_types.param_components.huggingface.huggingface_utils import (
6
6
  list_repo_revisions_with_file_in_cache,
7
7
  )
8
+ from griptape_nodes.traits.options import Options
8
9
 
9
10
  logger = logging.getLogger("griptape_nodes")
10
11
 
11
12
 
12
13
  class HuggingFaceRepoFileParameter(HuggingFaceModelParameter):
13
- def __init__(self, node: BaseNode, repo_files: list[tuple[str, str]], parameter_name: str = "model"):
14
+ def __init__(
15
+ self,
16
+ node: BaseNode,
17
+ repo_files: list[tuple[str, str]],
18
+ parameter_name: str = "model",
19
+ deprecated_repo_files: list[tuple[str, str]] | None = None,
20
+ ):
14
21
  super().__init__(node, parameter_name)
15
- self._repo_files = repo_files
22
+
23
+ deprecated_repo_files = deprecated_repo_files or []
24
+ self._deprecated_repos = [repo for repo, _ in deprecated_repo_files]
25
+
26
+ self._repo_files = repo_files + deprecated_repo_files
27
+
16
28
  self.refresh_parameters()
17
29
 
18
30
  def fetch_repo_revisions(self) -> list[tuple[str, str]]:
@@ -22,12 +34,74 @@ class HuggingFaceRepoFileParameter(HuggingFaceModelParameter):
22
34
  for repo_revision in list_repo_revisions_with_file_in_cache(repo, file)
23
35
  ]
24
36
 
37
+ def _is_deprecated(self, repo: str) -> bool:
38
+ return repo in self._deprecated_repos
39
+
40
+ def refresh_parameters(self, value_being_set: str | None = None) -> None:
41
+ """Override to filter deprecated models except the currently selected one.
42
+
43
+ Args:
44
+ value_being_set: Optional value that's being set (used during after_value_set)
45
+ """
46
+ parameter = self._node.get_parameter_by_name(self._parameter_name)
47
+ if parameter is None:
48
+ logger.debug(
49
+ "Parameter '%s' not found on node '%s'; cannot refresh choices.",
50
+ self._parameter_name,
51
+ self._node.name,
52
+ )
53
+ return
54
+
55
+ # Get all cached models
56
+ all_choices = self.get_choices()
57
+ if not all_choices:
58
+ return
59
+
60
+ # Get current value - use value_being_set if provided (during after_value_set)
61
+ current_value = (
62
+ value_being_set if value_being_set is not None else self._node.get_parameter_value(self._parameter_name)
63
+ )
64
+
65
+ # Filter: include non-deprecated models, and deprecated model if it's currently selected
66
+ filtered_choices = []
67
+ for choice in all_choices:
68
+ repo_id, _ = self._key_to_repo_revision(choice)
69
+ is_deprecated = self._is_deprecated(repo_id)
70
+
71
+ # Include if: not deprecated OR matches current/being-set value
72
+ if not is_deprecated or choice == current_value:
73
+ filtered_choices.append(choice)
74
+
75
+ # If no choices after filtering, include all (initial state)
76
+ if not filtered_choices:
77
+ return
78
+
79
+ # Determine default value
80
+ if current_value and current_value in filtered_choices:
81
+ default_value = current_value
82
+ else:
83
+ default_value = filtered_choices[0]
84
+
85
+ if parameter.find_elements_by_type(Options):
86
+ self._node._update_option_choices(self._parameter_name, filtered_choices, default_value)
87
+ else:
88
+ parameter.add_trait(Options(choices=filtered_choices))
89
+
25
90
  def get_download_commands(self) -> list[str]:
26
- return [f'huggingface-cli download "{repo}" "{file}"' for (repo, file) in self._repo_files]
91
+ return [
92
+ f'huggingface-cli download "{repo}" "{file}"'
93
+ for (repo, file) in self._repo_files
94
+ if not self._is_deprecated(repo)
95
+ ]
27
96
 
28
97
  def get_download_models(self) -> list[str]:
29
- """Returns a list of model names that should be downloaded."""
30
- return [repo for (repo, file) in self._repo_files]
98
+ """Returns a list of model names that should be downloaded (excluding deprecated models)."""
99
+ return [repo for (repo, file) in self._repo_files if not self._is_deprecated(repo)]
100
+
101
+ def add_input_parameters(self) -> None:
102
+ """Override to apply deprecated model filtering after parameter creation."""
103
+ super().add_input_parameters()
104
+ self.refresh_parameters()
31
105
 
32
106
  def get_repo_filename(self) -> str:
33
107
  repo_id, _ = self.get_repo_revision()
@@ -0,0 +1,443 @@
1
+ """ParameterButton component for button inputs with enhanced UI options."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, Literal
5
+
6
+ from griptape_nodes.exe_types.core_types import NodeMessageResult, Parameter, ParameterMode, Trait
7
+ from griptape_nodes.traits.button import Button, ButtonDetailsMessagePayload, OnClickMessageResultPayload
8
+
9
+ # Type aliases matching Button trait
10
+ ButtonVariant = Literal["default", "secondary", "destructive", "outline", "ghost", "link"]
11
+ ButtonSize = Literal["default", "sm", "icon"]
12
+ ButtonState = Literal["normal", "disabled", "loading", "hidden"]
13
+ IconPosition = Literal["left", "right"]
14
+
15
+
16
+ class ParameterButton(Parameter):
17
+ """A specialized Parameter class for button inputs with enhanced UI options.
18
+
19
+ This class provides a convenient way to create button parameters with all the
20
+ styling and behavior options from the Button trait. It exposes these UI options
21
+ as direct properties for easy runtime modification.
22
+
23
+ Example:
24
+ # Label is the display text, value is separate stored data
25
+ param = ParameterButton(
26
+ name="button_param",
27
+ label="Click me", # Display text on the button
28
+ default_value="some stored data", # Stored value (can be any data)
29
+ variant="secondary",
30
+ icon="check",
31
+ icon_class="text-green-500",
32
+ full_width=True
33
+ )
34
+
35
+ # With callback function (just like Button trait)
36
+ def handle_click(button, details):
37
+ print(f"Button {details.label} was clicked!")
38
+ return NodeMessageResult(success=True, details="Clicked!")
39
+
40
+ param3 = ParameterButton(
41
+ name="callback_button",
42
+ label="Submit",
43
+ default_value="submit_action",
44
+ on_click=handle_click
45
+ )
46
+
47
+ # Or use href for simple link opening
48
+ link_button = ParameterButton(
49
+ name="docs_link",
50
+ label="View Docs",
51
+ href="https://docs.example.com"
52
+ )
53
+
54
+ # Change properties at runtime (just like Button trait)
55
+ # Label and value are independent
56
+ param.label = "New Label" # Changes display text only
57
+ param.default_value = "new_data" # Changes stored value only
58
+ param.variant = "destructive"
59
+ """
60
+
61
+ def __init__( # noqa: PLR0913
62
+ self,
63
+ name: str,
64
+ tooltip: str | None = None,
65
+ *,
66
+ type: str = "button", # noqa: A002
67
+ default_value: Any = None,
68
+ tooltip_as_input: str | None = None,
69
+ tooltip_as_property: str | None = None,
70
+ tooltip_as_output: str | None = None,
71
+ allowed_modes: set[ParameterMode] | None = None,
72
+ traits: set[type[Trait] | Trait] | None = None,
73
+ converters: list[Callable[[Any], Any]] | None = None,
74
+ validators: list[Callable[[Parameter, Any], None]] | None = None,
75
+ ui_options: dict | None = None,
76
+ # Button-specific parameters (matching Button trait defaults)
77
+ label: str = "", # Allows a button with no text, matching Button trait
78
+ variant: ButtonVariant = "secondary",
79
+ size: ButtonSize = "default",
80
+ state: ButtonState = "normal",
81
+ icon: str | None = None,
82
+ icon_class: str | None = None,
83
+ icon_position: IconPosition | None = None,
84
+ full_width: bool = True,
85
+ loading_label: str | None = None,
86
+ loading_icon: str | None = None,
87
+ loading_icon_class: str | None = None,
88
+ on_click: Button.OnClickCallback | None = None,
89
+ get_button_state: Button.GetButtonStateCallback | None = None,
90
+ href: str | None = None,
91
+ hide: bool = False,
92
+ hide_label: bool = False,
93
+ hide_property: bool = False,
94
+ allow_input: bool = False,
95
+ allow_property: bool = True,
96
+ allow_output: bool = False,
97
+ settable: bool = True,
98
+ serializable: bool = True,
99
+ user_defined: bool = False,
100
+ element_id: str | None = None,
101
+ element_type: str | None = None,
102
+ parent_container_name: str | None = None,
103
+ ) -> None:
104
+ """Initialize a button parameter with enhanced UI options.
105
+
106
+ Args:
107
+ name: Parameter name
108
+ tooltip: Parameter tooltip
109
+ type: Parameter type (ignored, always "any" for ParameterButton)
110
+ default_value: Default parameter value
111
+ tooltip_as_input: Tooltip for input mode
112
+ tooltip_as_property: Tooltip for property mode
113
+ tooltip_as_output: Tooltip for output mode
114
+ allowed_modes: Allowed parameter modes
115
+ traits: Parameter traits
116
+ converters: Parameter converters
117
+ validators: Parameter validators
118
+ ui_options: Dictionary of UI options
119
+ label: Button label text
120
+ variant: Button variant style
121
+ size: Button size
122
+ state: Button state (normal, disabled, loading, hidden)
123
+ icon: Icon identifier/name
124
+ icon_class: CSS class for icon
125
+ icon_position: Position of icon relative to label
126
+ full_width: Whether button should take full width
127
+ loading_label: Label to show when in loading state
128
+ loading_icon: Icon to show when in loading state
129
+ loading_icon_class: CSS class for loading icon
130
+ on_click: Callback function for button clicks
131
+ get_button_state: Callback function to get button state
132
+ href: URL to open when button is clicked (alternative to on_click callback)
133
+ hide: Whether to hide the entire parameter
134
+ hide_label: Whether to hide the parameter label
135
+ hide_property: Whether to hide the parameter in property mode
136
+ allow_input: Whether to allow input mode
137
+ allow_property: Whether to allow property mode
138
+ allow_output: Whether to allow output mode
139
+ settable: Whether the parameter is settable
140
+ serializable: Whether the parameter is serializable
141
+ user_defined: Whether the parameter is user-defined
142
+ element_id: Element ID
143
+ element_type: Element type
144
+ parent_container_name: Name of parent container
145
+ """
146
+ # Build ui_options dictionary from the provided UI-specific parameters
147
+ if ui_options is None:
148
+ ui_options = {}
149
+ else:
150
+ ui_options = ui_options.copy()
151
+
152
+ # If href is provided but no on_click callback, create a simple callback that opens the link
153
+ final_on_click = on_click
154
+ if href is not None and on_click is None:
155
+ final_on_click = ParameterButton._create_href_callback(href)
156
+
157
+ # Create Button trait with all the button-specific options
158
+ # Label and value are separate - label is display text, value is stored data
159
+ button_trait = Button(
160
+ label=label,
161
+ variant=variant,
162
+ size=size,
163
+ state=state,
164
+ icon=icon,
165
+ icon_class=icon_class,
166
+ icon_position=icon_position,
167
+ full_width=full_width,
168
+ loading_label=loading_label,
169
+ loading_icon=loading_icon,
170
+ loading_icon_class=loading_icon_class,
171
+ tooltip=tooltip,
172
+ on_click=final_on_click,
173
+ get_button_state=get_button_state,
174
+ )
175
+
176
+ # Store href for property access
177
+ self._href = href
178
+
179
+ # Merge button UI options into parameter UI options
180
+ button_ui_options = button_trait.ui_options_for_trait()
181
+ ui_options.update(button_ui_options)
182
+
183
+ # Add button trait to traits set
184
+ # Button is a Trait, so it can be added to the traits set
185
+ if traits is None:
186
+ final_traits: set[type[Trait] | Trait] = {button_trait} # type: ignore[assignment]
187
+ else:
188
+ final_traits = set(traits) | {button_trait} # type: ignore[assignment]
189
+
190
+ # Call parent with explicit parameters
191
+ super().__init__(
192
+ name=name,
193
+ tooltip=tooltip,
194
+ type="button", # Button parameter type
195
+ input_types=["str", "any"],
196
+ output_type="str",
197
+ default_value=default_value,
198
+ tooltip_as_input=tooltip_as_input,
199
+ tooltip_as_property=tooltip_as_property,
200
+ tooltip_as_output=tooltip_as_output,
201
+ allowed_modes=allowed_modes,
202
+ traits=final_traits,
203
+ converters=converters,
204
+ validators=validators,
205
+ ui_options=ui_options,
206
+ hide=hide,
207
+ hide_label=hide_label,
208
+ hide_property=hide_property,
209
+ allow_input=allow_input,
210
+ allow_property=allow_property,
211
+ allow_output=allow_output,
212
+ settable=settable,
213
+ serializable=serializable,
214
+ user_defined=user_defined,
215
+ element_id=element_id,
216
+ element_type=element_type,
217
+ parent_container_name=parent_container_name,
218
+ )
219
+
220
+ # Store button trait reference for property access
221
+ self._button_trait = button_trait
222
+
223
+ @staticmethod
224
+ def _create_href_callback(href: str) -> Button.OnClickCallback:
225
+ """Create a simple callback that opens a link when the button is clicked."""
226
+
227
+ def href_callback(
228
+ button: Button, # noqa: ARG001
229
+ button_details: ButtonDetailsMessagePayload,
230
+ ) -> NodeMessageResult:
231
+ return NodeMessageResult(
232
+ success=True,
233
+ details=f"Opening link: {href}",
234
+ response=OnClickMessageResultPayload(
235
+ button_details=button_details,
236
+ href=href,
237
+ ),
238
+ altered_workflow_state=False,
239
+ )
240
+
241
+ return href_callback
242
+
243
+ def _get_button_trait(self) -> Button:
244
+ """Get the Button trait associated with this parameter."""
245
+ # Find the Button trait in the parameter's children (traits are stored as children)
246
+ for child in self.children:
247
+ if isinstance(child, Button):
248
+ return child
249
+ # Fallback: should not happen if initialization is correct
250
+ return self._button_trait
251
+
252
+ @property
253
+ def label(self) -> str:
254
+ """Get the button label (primary interface, like Button trait)."""
255
+ # Label is the primary property - get it directly from button trait
256
+ return self._get_button_trait().label
257
+
258
+ @label.setter
259
+ def label(self, value: str) -> None:
260
+ """Set the button label (display text only - separate from parameter value)."""
261
+ # Update button trait (primary source of truth for display)
262
+ self._get_button_trait().label = value
263
+ # Update UI options
264
+ self.update_ui_options_key("button_label", value)
265
+
266
+ @property
267
+ def variant(self) -> ButtonVariant:
268
+ """Get the button variant."""
269
+ return self._get_button_trait().variant
270
+
271
+ @variant.setter
272
+ def variant(self, value: ButtonVariant) -> None:
273
+ """Set the button variant."""
274
+ self._get_button_trait().variant = value
275
+ self.update_ui_options_key("variant", value)
276
+
277
+ @property
278
+ def size(self) -> ButtonSize:
279
+ """Get the button size."""
280
+ return self._get_button_trait().size
281
+
282
+ @size.setter
283
+ def size(self, value: ButtonSize) -> None:
284
+ """Set the button size."""
285
+ self._get_button_trait().size = value
286
+ self.update_ui_options_key("size", value)
287
+
288
+ @property
289
+ def state(self) -> ButtonState:
290
+ """Get the button state."""
291
+ return self._get_button_trait().state
292
+
293
+ @state.setter
294
+ def state(self, value: ButtonState) -> None:
295
+ """Set the button state."""
296
+ self._get_button_trait().state = value
297
+ self.update_ui_options_key("state", value)
298
+
299
+ @property
300
+ def icon(self) -> str | None:
301
+ """Get the button icon."""
302
+ return self._get_button_trait().icon
303
+
304
+ @icon.setter
305
+ def icon(self, value: str | None) -> None:
306
+ """Set the button icon."""
307
+ self._get_button_trait().icon = value
308
+ if value is None:
309
+ ui_options = self.ui_options.copy()
310
+ ui_options.pop("button_icon", None)
311
+ self.ui_options = ui_options
312
+ else:
313
+ self.update_ui_options_key("button_icon", value)
314
+
315
+ @property
316
+ def icon_class(self) -> str | None:
317
+ """Get the button icon class."""
318
+ return self._get_button_trait().icon_class
319
+
320
+ @icon_class.setter
321
+ def icon_class(self, value: str | None) -> None:
322
+ """Set the button icon class."""
323
+ self._get_button_trait().icon_class = value
324
+ if value is None:
325
+ ui_options = self.ui_options.copy()
326
+ ui_options.pop("icon_class", None)
327
+ self.ui_options = ui_options
328
+ else:
329
+ self.update_ui_options_key("icon_class", value)
330
+
331
+ @property
332
+ def icon_position(self) -> IconPosition | None:
333
+ """Get the button icon position."""
334
+ return self._get_button_trait().icon_position
335
+
336
+ @icon_position.setter
337
+ def icon_position(self, value: IconPosition | None) -> None:
338
+ """Set the button icon position."""
339
+ self._get_button_trait().icon_position = value
340
+ if value is None:
341
+ ui_options = self.ui_options.copy()
342
+ ui_options.pop("iconPosition", None)
343
+ self.ui_options = ui_options
344
+ else:
345
+ self.update_ui_options_key("iconPosition", value)
346
+
347
+ @property
348
+ def full_width(self) -> bool:
349
+ """Get whether the button is full width."""
350
+ return self._get_button_trait().full_width
351
+
352
+ @full_width.setter
353
+ def full_width(self, value: bool) -> None:
354
+ """Set whether the button is full width."""
355
+ self._get_button_trait().full_width = value
356
+ self.update_ui_options_key("full_width", value)
357
+
358
+ @property
359
+ def loading_label(self) -> str | None:
360
+ """Get the loading label."""
361
+ return self._get_button_trait().loading_label
362
+
363
+ @loading_label.setter
364
+ def loading_label(self, value: str | None) -> None:
365
+ """Set the loading label."""
366
+ self._get_button_trait().loading_label = value
367
+ if value is None:
368
+ ui_options = self.ui_options.copy()
369
+ ui_options.pop("loading_label", None)
370
+ self.ui_options = ui_options
371
+ else:
372
+ self.update_ui_options_key("loading_label", value)
373
+
374
+ @property
375
+ def loading_icon(self) -> str | None:
376
+ """Get the loading icon."""
377
+ return self._get_button_trait().loading_icon
378
+
379
+ @loading_icon.setter
380
+ def loading_icon(self, value: str | None) -> None:
381
+ """Set the loading icon."""
382
+ self._get_button_trait().loading_icon = value
383
+ if value is None:
384
+ ui_options = self.ui_options.copy()
385
+ ui_options.pop("loading_icon", None)
386
+ self.ui_options = ui_options
387
+ else:
388
+ self.update_ui_options_key("loading_icon", value)
389
+
390
+ @property
391
+ def loading_icon_class(self) -> str | None:
392
+ """Get the loading icon class."""
393
+ return self._get_button_trait().loading_icon_class
394
+
395
+ @loading_icon_class.setter
396
+ def loading_icon_class(self, value: str | None) -> None:
397
+ """Set the loading icon class."""
398
+ self._get_button_trait().loading_icon_class = value
399
+ if value is None:
400
+ ui_options = self.ui_options.copy()
401
+ ui_options.pop("loading_icon_class", None)
402
+ self.ui_options = ui_options
403
+ else:
404
+ self.update_ui_options_key("loading_icon_class", value)
405
+
406
+ @property
407
+ def on_click_callback(self) -> Button.OnClickCallback | None:
408
+ """Get the on_click callback."""
409
+ return self._get_button_trait().on_click_callback
410
+
411
+ @on_click_callback.setter
412
+ def on_click_callback(self, value: Button.OnClickCallback | None) -> None:
413
+ """Set the on_click callback."""
414
+ self._get_button_trait().on_click_callback = value
415
+
416
+ @property
417
+ def get_button_state_callback(self) -> Button.GetButtonStateCallback | None:
418
+ """Get the get_button_state callback."""
419
+ return self._get_button_trait().get_button_state_callback
420
+
421
+ @get_button_state_callback.setter
422
+ def get_button_state_callback(self, value: Button.GetButtonStateCallback | None) -> None:
423
+ """Set the get_button_state callback."""
424
+ self._get_button_trait().get_button_state_callback = value
425
+
426
+ @property
427
+ def href(self) -> str | None:
428
+ """Get the href URL that will be opened when the button is clicked."""
429
+ return getattr(self, "_href", None)
430
+
431
+ @href.setter
432
+ def href(self, value: str | None) -> None:
433
+ """Set the href URL to open when button is clicked.
434
+
435
+ This will replace any existing on_click callback with a simple link-opening callback.
436
+ """
437
+ self._href = value
438
+ if value is not None:
439
+ # Replace on_click callback with href callback
440
+ self.on_click_callback = ParameterButton._create_href_callback(value)
441
+ else:
442
+ # Clear the callback if href is removed
443
+ self.on_click_callback = None