data-designer 0.1.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 (177) hide show
  1. data_designer/__init__.py +15 -0
  2. data_designer/_version.py +34 -0
  3. data_designer/cli/README.md +236 -0
  4. data_designer/cli/__init__.py +6 -0
  5. data_designer/cli/commands/__init__.py +2 -0
  6. data_designer/cli/commands/list.py +130 -0
  7. data_designer/cli/commands/models.py +10 -0
  8. data_designer/cli/commands/providers.py +11 -0
  9. data_designer/cli/commands/reset.py +100 -0
  10. data_designer/cli/controllers/__init__.py +7 -0
  11. data_designer/cli/controllers/model_controller.py +246 -0
  12. data_designer/cli/controllers/provider_controller.py +317 -0
  13. data_designer/cli/forms/__init__.py +20 -0
  14. data_designer/cli/forms/builder.py +51 -0
  15. data_designer/cli/forms/field.py +180 -0
  16. data_designer/cli/forms/form.py +59 -0
  17. data_designer/cli/forms/model_builder.py +125 -0
  18. data_designer/cli/forms/provider_builder.py +76 -0
  19. data_designer/cli/main.py +44 -0
  20. data_designer/cli/repositories/__init__.py +8 -0
  21. data_designer/cli/repositories/base.py +39 -0
  22. data_designer/cli/repositories/model_repository.py +42 -0
  23. data_designer/cli/repositories/provider_repository.py +43 -0
  24. data_designer/cli/services/__init__.py +7 -0
  25. data_designer/cli/services/model_service.py +116 -0
  26. data_designer/cli/services/provider_service.py +111 -0
  27. data_designer/cli/ui.py +448 -0
  28. data_designer/cli/utils.py +47 -0
  29. data_designer/config/__init__.py +2 -0
  30. data_designer/config/analysis/column_profilers.py +89 -0
  31. data_designer/config/analysis/column_statistics.py +274 -0
  32. data_designer/config/analysis/dataset_profiler.py +60 -0
  33. data_designer/config/analysis/utils/errors.py +8 -0
  34. data_designer/config/analysis/utils/reporting.py +188 -0
  35. data_designer/config/base.py +68 -0
  36. data_designer/config/column_configs.py +354 -0
  37. data_designer/config/column_types.py +168 -0
  38. data_designer/config/config_builder.py +660 -0
  39. data_designer/config/data_designer_config.py +40 -0
  40. data_designer/config/dataset_builders.py +11 -0
  41. data_designer/config/datastore.py +151 -0
  42. data_designer/config/default_model_settings.py +123 -0
  43. data_designer/config/errors.py +19 -0
  44. data_designer/config/interface.py +54 -0
  45. data_designer/config/models.py +231 -0
  46. data_designer/config/preview_results.py +32 -0
  47. data_designer/config/processors.py +41 -0
  48. data_designer/config/sampler_constraints.py +51 -0
  49. data_designer/config/sampler_params.py +604 -0
  50. data_designer/config/seed.py +145 -0
  51. data_designer/config/utils/code_lang.py +83 -0
  52. data_designer/config/utils/constants.py +313 -0
  53. data_designer/config/utils/errors.py +19 -0
  54. data_designer/config/utils/info.py +88 -0
  55. data_designer/config/utils/io_helpers.py +273 -0
  56. data_designer/config/utils/misc.py +81 -0
  57. data_designer/config/utils/numerical_helpers.py +28 -0
  58. data_designer/config/utils/type_helpers.py +100 -0
  59. data_designer/config/utils/validation.py +336 -0
  60. data_designer/config/utils/visualization.py +427 -0
  61. data_designer/config/validator_params.py +96 -0
  62. data_designer/engine/__init__.py +2 -0
  63. data_designer/engine/analysis/column_profilers/base.py +55 -0
  64. data_designer/engine/analysis/column_profilers/judge_score_profiler.py +160 -0
  65. data_designer/engine/analysis/column_profilers/registry.py +20 -0
  66. data_designer/engine/analysis/column_statistics.py +142 -0
  67. data_designer/engine/analysis/dataset_profiler.py +125 -0
  68. data_designer/engine/analysis/errors.py +7 -0
  69. data_designer/engine/analysis/utils/column_statistics_calculations.py +209 -0
  70. data_designer/engine/analysis/utils/judge_score_processing.py +128 -0
  71. data_designer/engine/column_generators/__init__.py +2 -0
  72. data_designer/engine/column_generators/generators/__init__.py +2 -0
  73. data_designer/engine/column_generators/generators/base.py +61 -0
  74. data_designer/engine/column_generators/generators/expression.py +63 -0
  75. data_designer/engine/column_generators/generators/llm_generators.py +172 -0
  76. data_designer/engine/column_generators/generators/samplers.py +75 -0
  77. data_designer/engine/column_generators/generators/seed_dataset.py +149 -0
  78. data_designer/engine/column_generators/generators/validation.py +147 -0
  79. data_designer/engine/column_generators/registry.py +56 -0
  80. data_designer/engine/column_generators/utils/errors.py +13 -0
  81. data_designer/engine/column_generators/utils/judge_score_factory.py +57 -0
  82. data_designer/engine/column_generators/utils/prompt_renderer.py +98 -0
  83. data_designer/engine/configurable_task.py +82 -0
  84. data_designer/engine/dataset_builders/artifact_storage.py +181 -0
  85. data_designer/engine/dataset_builders/column_wise_builder.py +287 -0
  86. data_designer/engine/dataset_builders/errors.py +13 -0
  87. data_designer/engine/dataset_builders/multi_column_configs.py +44 -0
  88. data_designer/engine/dataset_builders/utils/__init__.py +2 -0
  89. data_designer/engine/dataset_builders/utils/concurrency.py +184 -0
  90. data_designer/engine/dataset_builders/utils/config_compiler.py +60 -0
  91. data_designer/engine/dataset_builders/utils/dag.py +56 -0
  92. data_designer/engine/dataset_builders/utils/dataset_batch_manager.py +190 -0
  93. data_designer/engine/dataset_builders/utils/errors.py +13 -0
  94. data_designer/engine/errors.py +49 -0
  95. data_designer/engine/model_provider.py +75 -0
  96. data_designer/engine/models/__init__.py +2 -0
  97. data_designer/engine/models/errors.py +308 -0
  98. data_designer/engine/models/facade.py +225 -0
  99. data_designer/engine/models/litellm_overrides.py +162 -0
  100. data_designer/engine/models/parsers/__init__.py +2 -0
  101. data_designer/engine/models/parsers/errors.py +34 -0
  102. data_designer/engine/models/parsers/parser.py +236 -0
  103. data_designer/engine/models/parsers/postprocessors.py +93 -0
  104. data_designer/engine/models/parsers/tag_parsers.py +60 -0
  105. data_designer/engine/models/parsers/types.py +82 -0
  106. data_designer/engine/models/recipes/base.py +79 -0
  107. data_designer/engine/models/recipes/response_recipes.py +291 -0
  108. data_designer/engine/models/registry.py +118 -0
  109. data_designer/engine/models/usage.py +75 -0
  110. data_designer/engine/models/utils.py +38 -0
  111. data_designer/engine/processing/ginja/__init__.py +2 -0
  112. data_designer/engine/processing/ginja/ast.py +64 -0
  113. data_designer/engine/processing/ginja/environment.py +461 -0
  114. data_designer/engine/processing/ginja/exceptions.py +54 -0
  115. data_designer/engine/processing/ginja/record.py +30 -0
  116. data_designer/engine/processing/gsonschema/__init__.py +2 -0
  117. data_designer/engine/processing/gsonschema/exceptions.py +8 -0
  118. data_designer/engine/processing/gsonschema/schema_transformers.py +81 -0
  119. data_designer/engine/processing/gsonschema/types.py +8 -0
  120. data_designer/engine/processing/gsonschema/validators.py +143 -0
  121. data_designer/engine/processing/processors/base.py +15 -0
  122. data_designer/engine/processing/processors/drop_columns.py +46 -0
  123. data_designer/engine/processing/processors/registry.py +20 -0
  124. data_designer/engine/processing/utils.py +120 -0
  125. data_designer/engine/registry/base.py +97 -0
  126. data_designer/engine/registry/data_designer_registry.py +37 -0
  127. data_designer/engine/registry/errors.py +10 -0
  128. data_designer/engine/resources/managed_dataset_generator.py +35 -0
  129. data_designer/engine/resources/managed_dataset_repository.py +194 -0
  130. data_designer/engine/resources/managed_storage.py +63 -0
  131. data_designer/engine/resources/resource_provider.py +46 -0
  132. data_designer/engine/resources/seed_dataset_data_store.py +66 -0
  133. data_designer/engine/sampling_gen/column.py +89 -0
  134. data_designer/engine/sampling_gen/constraints.py +95 -0
  135. data_designer/engine/sampling_gen/data_sources/base.py +214 -0
  136. data_designer/engine/sampling_gen/data_sources/errors.py +10 -0
  137. data_designer/engine/sampling_gen/data_sources/sources.py +342 -0
  138. data_designer/engine/sampling_gen/entities/__init__.py +2 -0
  139. data_designer/engine/sampling_gen/entities/assets/zip_area_code_map.parquet +0 -0
  140. data_designer/engine/sampling_gen/entities/dataset_based_person_fields.py +64 -0
  141. data_designer/engine/sampling_gen/entities/email_address_utils.py +169 -0
  142. data_designer/engine/sampling_gen/entities/errors.py +8 -0
  143. data_designer/engine/sampling_gen/entities/national_id_utils.py +100 -0
  144. data_designer/engine/sampling_gen/entities/person.py +142 -0
  145. data_designer/engine/sampling_gen/entities/phone_number.py +122 -0
  146. data_designer/engine/sampling_gen/errors.py +24 -0
  147. data_designer/engine/sampling_gen/generator.py +121 -0
  148. data_designer/engine/sampling_gen/jinja_utils.py +60 -0
  149. data_designer/engine/sampling_gen/people_gen.py +203 -0
  150. data_designer/engine/sampling_gen/person_constants.py +54 -0
  151. data_designer/engine/sampling_gen/schema.py +143 -0
  152. data_designer/engine/sampling_gen/schema_builder.py +59 -0
  153. data_designer/engine/sampling_gen/utils.py +40 -0
  154. data_designer/engine/secret_resolver.py +80 -0
  155. data_designer/engine/validators/__init__.py +17 -0
  156. data_designer/engine/validators/base.py +36 -0
  157. data_designer/engine/validators/local_callable.py +34 -0
  158. data_designer/engine/validators/python.py +245 -0
  159. data_designer/engine/validators/remote.py +83 -0
  160. data_designer/engine/validators/sql.py +60 -0
  161. data_designer/errors.py +5 -0
  162. data_designer/essentials/__init__.py +137 -0
  163. data_designer/interface/__init__.py +2 -0
  164. data_designer/interface/data_designer.py +351 -0
  165. data_designer/interface/errors.py +16 -0
  166. data_designer/interface/results.py +55 -0
  167. data_designer/logging.py +161 -0
  168. data_designer/plugin_manager.py +83 -0
  169. data_designer/plugins/__init__.py +6 -0
  170. data_designer/plugins/errors.py +10 -0
  171. data_designer/plugins/plugin.py +69 -0
  172. data_designer/plugins/registry.py +86 -0
  173. data_designer-0.1.0.dist-info/METADATA +173 -0
  174. data_designer-0.1.0.dist-info/RECORD +177 -0
  175. data_designer-0.1.0.dist-info/WHEEL +4 -0
  176. data_designer-0.1.0.dist-info/entry_points.txt +2 -0
  177. data_designer-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,116 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_designer.cli.repositories.model_repository import ModelConfigRegistry, ModelRepository
5
+ from data_designer.config.models import ModelConfig
6
+
7
+
8
+ class ModelService:
9
+ """Business logic for model management."""
10
+
11
+ def __init__(self, repository: ModelRepository):
12
+ self.repository = repository
13
+
14
+ def list_all(self) -> list[ModelConfig]:
15
+ """Get all configured models."""
16
+ registry = self.repository.load()
17
+ return list(registry.model_configs) if registry else []
18
+
19
+ def get_by_alias(self, alias: str) -> ModelConfig | None:
20
+ """Get a model by alias."""
21
+ models = self.list_all()
22
+ return next((m for m in models if m.alias == alias), None)
23
+
24
+ def find_by_provider(self, provider_name: str) -> list[ModelConfig]:
25
+ """Find all models associated with a provider.
26
+
27
+ Args:
28
+ provider_name: Name of the provider to search for
29
+
30
+ Returns:
31
+ List of models associated with the provider
32
+ """
33
+ models = self.list_all()
34
+ return [m for m in models if m.provider == provider_name]
35
+
36
+ def add(self, model: ModelConfig) -> None:
37
+ """Add a new model.
38
+
39
+ Raises:
40
+ ValueError: If model alias already exists
41
+ """
42
+ registry = self.repository.load() or ModelConfigRegistry(model_configs=[])
43
+
44
+ if any(m.alias == model.alias for m in registry.model_configs):
45
+ raise ValueError(f"Model alias '{model.alias}' already exists")
46
+
47
+ registry.model_configs.append(model)
48
+ self.repository.save(registry)
49
+
50
+ def update(self, original_alias: str, updated_model: ModelConfig) -> None:
51
+ """Update an existing model.
52
+
53
+ Raises:
54
+ ValueError: If model not found or new alias already exists
55
+ """
56
+ registry = self.repository.load()
57
+ if not registry:
58
+ raise ValueError("No models configured")
59
+
60
+ # Find model index
61
+ index = next(
62
+ (i for i, m in enumerate(registry.model_configs) if m.alias == original_alias),
63
+ None,
64
+ )
65
+ if index is None:
66
+ raise ValueError(f"Model '{original_alias}' not found")
67
+
68
+ if updated_model.alias != original_alias:
69
+ if any(m.alias == updated_model.alias for m in registry.model_configs):
70
+ raise ValueError(f"Model alias '{updated_model.alias}' already exists")
71
+
72
+ registry.model_configs[index] = updated_model
73
+ self.repository.save(registry)
74
+
75
+ def delete(self, alias: str) -> None:
76
+ """Delete a model.
77
+
78
+ Raises:
79
+ ValueError: If model not found
80
+ """
81
+ registry = self.repository.load()
82
+ if not registry:
83
+ raise ValueError("No models configured")
84
+
85
+ if not any(m.alias == alias for m in registry.model_configs):
86
+ raise ValueError(f"Model '{alias}' not found")
87
+
88
+ registry.model_configs = [m for m in registry.model_configs if m.alias != alias]
89
+
90
+ if registry.model_configs:
91
+ self.repository.save(registry)
92
+ else:
93
+ self.repository.delete()
94
+
95
+ def delete_by_aliases(self, aliases: list[str]) -> None:
96
+ """Delete multiple models by their aliases.
97
+
98
+ Args:
99
+ aliases: List of model aliases to delete
100
+
101
+ Raises:
102
+ ValueError: If no models configured
103
+ """
104
+ if not aliases:
105
+ return
106
+
107
+ registry = self.repository.load()
108
+ if not registry:
109
+ raise ValueError("No models configured")
110
+
111
+ registry.model_configs = [m for m in registry.model_configs if m.alias not in aliases]
112
+
113
+ if registry.model_configs:
114
+ self.repository.save(registry)
115
+ else:
116
+ self.repository.delete()
@@ -0,0 +1,111 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_designer.cli.repositories.provider_repository import ModelProviderRegistry, ProviderRepository
5
+ from data_designer.config.models import ModelProvider
6
+
7
+
8
+ class ProviderService:
9
+ """Business logic for provider management."""
10
+
11
+ def __init__(self, repository: ProviderRepository):
12
+ self.repository = repository
13
+
14
+ def list_all(self) -> list[ModelProvider]:
15
+ """Get all configured providers."""
16
+ registry = self.repository.load()
17
+ return list(registry.providers) if registry else []
18
+
19
+ def get_by_name(self, name: str) -> ModelProvider | None:
20
+ """Get a provider by name."""
21
+ providers = self.list_all()
22
+ return next((p for p in providers if p.name == name), None)
23
+
24
+ def add(self, provider: ModelProvider) -> None:
25
+ """Add a new provider.
26
+
27
+ Raises:
28
+ ValueError: If provider name already exists
29
+ """
30
+ registry = self.repository.load() or ModelProviderRegistry(providers=[], default=None)
31
+
32
+ if any(p.name == provider.name for p in registry.providers):
33
+ raise ValueError(f"Provider '{provider.name}' already exists")
34
+
35
+ registry.providers.append(provider)
36
+ self.repository.save(registry)
37
+
38
+ def update(self, original_name: str, updated_provider: ModelProvider) -> None:
39
+ """Update an existing provider.
40
+
41
+ Raises:
42
+ ValueError: If provider not found or new name already exists
43
+ """
44
+ registry = self.repository.load()
45
+ if not registry:
46
+ raise ValueError("No providers configured")
47
+
48
+ # Find provider index
49
+ index = next(
50
+ (i for i, p in enumerate(registry.providers) if p.name == original_name),
51
+ None,
52
+ )
53
+ if index is None:
54
+ raise ValueError(f"Provider '{original_name}' not found")
55
+
56
+ if updated_provider.name != original_name:
57
+ if any(p.name == updated_provider.name for p in registry.providers):
58
+ raise ValueError(f"Provider name '{updated_provider.name}' already exists")
59
+
60
+ registry.providers[index] = updated_provider
61
+
62
+ if registry.default == original_name and updated_provider.name != original_name:
63
+ registry.default = updated_provider.name
64
+
65
+ self.repository.save(registry)
66
+
67
+ def delete(self, name: str) -> None:
68
+ """Delete a provider.
69
+
70
+ Raises:
71
+ ValueError: If provider not found
72
+ """
73
+ registry = self.repository.load()
74
+ if not registry:
75
+ raise ValueError("No providers configured")
76
+
77
+ if not any(p.name == name for p in registry.providers):
78
+ raise ValueError(f"Provider '{name}' not found")
79
+
80
+ registry.providers = [p for p in registry.providers if p.name != name]
81
+
82
+ if registry.default == name:
83
+ registry.default = registry.providers[0].name if registry.providers else None
84
+
85
+ if registry.providers:
86
+ self.repository.save(registry)
87
+ else:
88
+ self.repository.delete()
89
+
90
+ def set_default(self, name: str) -> None:
91
+ """Set the default provider.
92
+
93
+ Raises:
94
+ ValueError: If provider not found
95
+ """
96
+ registry = self.repository.load()
97
+ if not registry:
98
+ raise ValueError("No providers configured")
99
+
100
+ if not any(p.name == name for p in registry.providers):
101
+ raise ValueError(f"Provider '{name}' not found")
102
+
103
+ registry.default = name
104
+ self.repository.save(registry)
105
+
106
+ def get_default(self) -> str | None:
107
+ """Get the default provider name."""
108
+ registry = self.repository.load()
109
+ if not registry:
110
+ return None
111
+ return registry.default or (registry.providers[0].name if registry.providers else None)
@@ -0,0 +1,448 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from collections.abc import Callable
5
+
6
+ from prompt_toolkit import Application, prompt
7
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
8
+ from prompt_toolkit.completion import FuzzyCompleter, WordCompleter
9
+ from prompt_toolkit.formatted_text import HTML
10
+ from prompt_toolkit.history import InMemoryHistory
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.layout import Layout
13
+ from prompt_toolkit.layout.containers import HSplit, Window
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
15
+ from prompt_toolkit.styles import Style
16
+ from rich.console import Console
17
+ from rich.padding import Padding
18
+ from rich.panel import Panel
19
+
20
+ from data_designer.config.utils.constants import RICH_CONSOLE_THEME, NordColor
21
+
22
+ # Global padding configuration
23
+ LEFT_PADDING = 2
24
+ RIGHT_PADDING = 2
25
+
26
+ # Private console instance - all output goes through this
27
+ _console = Console(theme=RICH_CONSOLE_THEME)
28
+
29
+ # Public console alias for external imports
30
+ console = _console
31
+
32
+ # Command/input history for prompt_toolkit
33
+ _input_history = InMemoryHistory()
34
+
35
+ # Custom style for prompt_toolkit
36
+ _PROMPT_STYLE = Style.from_dict(
37
+ {
38
+ # Default text style
39
+ "": "#ffffff",
40
+ # Auto-suggestions in gray (from history)
41
+ "auto-suggest": "fg:#666666",
42
+ # Dim text for defaults and hints (Nord3 - lighter dark gray)
43
+ "dim": "fg:#4c566a",
44
+ # Selected completion in menu
45
+ "completion-menu.completion.current": "bg:#88c0d0 #2e3440",
46
+ "completion-menu.completion": "bg:#4c566a #d8dee9",
47
+ }
48
+ )
49
+
50
+
51
+ # Sentinel value to indicate user wants to go back
52
+ class _BackSentinel:
53
+ """Sentinel class to indicate user wants to go back to previous prompt."""
54
+
55
+ def __repr__(self) -> str:
56
+ return "BACK"
57
+
58
+
59
+ BACK = _BackSentinel()
60
+
61
+
62
+ def select_with_arrows(
63
+ options: dict[str, str],
64
+ prompt_text: str,
65
+ default_key: str | None = None,
66
+ allow_back: bool = False,
67
+ ) -> str | None | _BackSentinel:
68
+ """Interactive selection with arrow key navigation using inline menu.
69
+
70
+ Uses prompt_toolkit's Application for an inline dropdown-style menu experience.
71
+
72
+ Args:
73
+ options: Dictionary of {key: display_text} options
74
+ prompt_text: Prompt to display above options
75
+ default_key: Default selected key (if None, first option is selected)
76
+ allow_back: If True, adds a "Go back" option to return to previous prompt
77
+
78
+ Returns:
79
+ Selected key, None if cancelled, or BACK sentinel if user wants to go back
80
+ """
81
+ if not options:
82
+ return None
83
+
84
+ # Build list of keys and values
85
+ keys = list(options.keys())
86
+ back_key = "__back__"
87
+
88
+ if allow_back:
89
+ keys.append(back_key)
90
+ options = {**options, back_key: "← Go back"}
91
+
92
+ # Find default index
93
+ if default_key and default_key in keys:
94
+ selected_index = keys.index(default_key)
95
+ else:
96
+ selected_index = 0
97
+
98
+ # Store result
99
+ result = {"value": None, "cancelled": False}
100
+
101
+ def get_formatted_text() -> list[tuple[str, str]]:
102
+ """Generate the formatted text for the menu."""
103
+ text = []
104
+ # Add prompt with padding
105
+ padding = " " * LEFT_PADDING
106
+ text.append(("", f"{padding}{prompt_text}\n"))
107
+
108
+ # Add options
109
+ for i, key in enumerate(keys):
110
+ display = options[key]
111
+ if i == selected_index:
112
+ # Selected item with Nord8 color
113
+ text.append((f"fg:{NordColor.NORD8.value} bold", f"{padding} → {display}\n"))
114
+ else:
115
+ # Unselected item
116
+ text.append(("", f"{padding} {display}\n"))
117
+
118
+ # Add hint
119
+ text.append(("fg:#666666", f"{padding} (↑/↓: navigate, Enter: select, Esc: cancel)\n"))
120
+ return text
121
+
122
+ # Create key bindings
123
+ kb = KeyBindings()
124
+
125
+ @kb.add("up")
126
+ @kb.add("c-p") # Ctrl+P
127
+ def _move_up(event) -> None:
128
+ nonlocal selected_index
129
+ selected_index = (selected_index - 1) % len(keys)
130
+
131
+ @kb.add("down")
132
+ @kb.add("c-n") # Ctrl+N
133
+ def _move_down(event) -> None:
134
+ nonlocal selected_index
135
+ selected_index = (selected_index + 1) % len(keys)
136
+
137
+ @kb.add("enter")
138
+ def _select(event) -> None:
139
+ result["value"] = keys[selected_index]
140
+ event.app.exit()
141
+
142
+ @kb.add("escape")
143
+ @kb.add("c-c") # Ctrl+C
144
+ def _cancel(event) -> None:
145
+ result["cancelled"] = True
146
+ event.app.exit()
147
+
148
+ # Create the application
149
+ app = Application(
150
+ layout=Layout(
151
+ HSplit(
152
+ [
153
+ Window(
154
+ content=FormattedTextControl(get_formatted_text),
155
+ dont_extend_height=True,
156
+ always_hide_cursor=True, # Hide cursor during menu selection
157
+ )
158
+ ]
159
+ )
160
+ ),
161
+ key_bindings=kb,
162
+ full_screen=False,
163
+ mouse_support=False,
164
+ )
165
+
166
+ try:
167
+ # Run the application
168
+ app.run()
169
+
170
+ # Handle the result
171
+ if result["cancelled"]:
172
+ print_warning("Cancelled")
173
+ return None
174
+ elif result["value"] == back_key:
175
+ print_info("Going back...")
176
+ return BACK
177
+ else:
178
+ return result["value"]
179
+
180
+ except (KeyboardInterrupt, EOFError):
181
+ print_warning("Cancelled")
182
+ return None
183
+
184
+
185
+ def prompt_text_input(
186
+ prompt_msg: str,
187
+ default: str | None = None,
188
+ validator: Callable[[str], tuple[bool, str | None]] | None = None,
189
+ mask: bool = False,
190
+ completions: list[str] | None = None,
191
+ allow_back: bool = False,
192
+ ) -> str | None | _BackSentinel:
193
+ """Prompt for text input with full line editing support.
194
+
195
+ Supports standard keyboard shortcuts:
196
+ - Ctrl+A: Move to beginning of line
197
+ - Ctrl+E: Move to end of line
198
+ - Ctrl+K: Delete to end of line
199
+ - Ctrl+U: Delete entire line
200
+ - Ctrl+W: Delete previous word
201
+ - Arrow keys: Navigate character by character
202
+ - Up/Down: Navigate through input history
203
+ - Tab: Trigger completions (if provided)
204
+ - Ctrl+C / Ctrl+D: Cancel
205
+
206
+ Special inputs:
207
+ - Type 'back' or 'b' to go back to previous prompt (if allow_back=True)
208
+
209
+ Features:
210
+ - Auto-suggestions from history (shown in gray)
211
+ - Fuzzy completion from provided options
212
+ - Persistent history across prompts
213
+
214
+ Args:
215
+ prompt_msg: Prompt message to display
216
+ default: Default value if user presses Enter without input
217
+ validator: Optional function that returns (is_valid, error_message)
218
+ mask: If True, input is masked (for passwords/secrets)
219
+ completions: Optional list of completion suggestions
220
+ allow_back: If True, user can type 'back' to return to previous prompt
221
+
222
+ Returns:
223
+ User input string, None if cancelled, or BACK sentinel if user wants to go back
224
+ """
225
+ # Build the prompt text with padding
226
+ # Use prompt-toolkit's HTML-like markup for colors
227
+ padded_prompt = " " * LEFT_PADDING + prompt_msg
228
+ if default:
229
+ padded_prompt += f" <dim>(default: {default})</dim>"
230
+ padded_prompt += ": "
231
+
232
+ # Create completer if completions provided or if back is enabled
233
+ completer = None
234
+ if completions or allow_back:
235
+ # Start with provided completions or empty list
236
+ completion_list = list(completions) if completions else []
237
+ # Add 'back' to completions if allowed
238
+ if allow_back and "back" not in completion_list:
239
+ completion_list.append("back")
240
+ if completion_list: # Only create completer if we have items
241
+ word_completer = WordCompleter(completion_list, ignore_case=True)
242
+ completer = FuzzyCompleter(word_completer)
243
+
244
+ try:
245
+ # Use prompt-toolkit for line editing with enhanced features
246
+ # Wrap in HTML() to enable styled markup
247
+ value = prompt(
248
+ HTML(padded_prompt),
249
+ default="", # Empty input field
250
+ is_password=mask,
251
+ completer=completer,
252
+ style=_PROMPT_STYLE,
253
+ complete_while_typing=True, # Show completions as you type
254
+ auto_suggest=AutoSuggestFromHistory(), # Show gray suggestions from history
255
+ history=_input_history, # Enable history with up/down arrows
256
+ enable_history_search=False, # Disable Ctrl+R search (optional)
257
+ ).strip()
258
+
259
+ except (KeyboardInterrupt, EOFError):
260
+ print_warning("Cancelled")
261
+ return None
262
+
263
+ # Check if user wants to go back
264
+ if allow_back and value.lower() in ("back", "b"):
265
+ print_info("Going back...")
266
+ return BACK
267
+
268
+ # Use default if no input provided (user just pressed Enter)
269
+ if not value and default:
270
+ value = default
271
+
272
+ # Skip validation if no input and no default
273
+ if not value:
274
+ return value
275
+
276
+ # Validate if validator provided
277
+ if validator:
278
+ is_valid, error_msg = validator(value)
279
+ if not is_valid:
280
+ print_error(f"Error: {error_msg}")
281
+ return prompt_text_input(prompt_msg, default, validator, mask, completions, allow_back)
282
+
283
+ return value
284
+
285
+
286
+ def confirm_action(prompt_msg: str, default: bool = False) -> bool:
287
+ """Prompt for yes/no confirmation with line editing support.
288
+
289
+ Args:
290
+ prompt_msg: Question to ask
291
+ default: Default choice if user just presses Enter
292
+
293
+ Returns:
294
+ True for yes, False for no
295
+ """
296
+ default_text = "Y/n" if default else "y/N"
297
+ padded_prompt = " " * LEFT_PADDING + f"{prompt_msg} <dim>[{default_text}]</dim>: "
298
+
299
+ try:
300
+ response = (
301
+ prompt(
302
+ HTML(padded_prompt),
303
+ style=_PROMPT_STYLE,
304
+ history=_input_history,
305
+ )
306
+ .strip()
307
+ .lower()
308
+ )
309
+ except (KeyboardInterrupt, EOFError):
310
+ print_warning("Cancelled")
311
+ return False
312
+
313
+ if not response:
314
+ return default
315
+
316
+ return response in ("y", "yes")
317
+
318
+
319
+ def display_config_preview(config: dict, title: str = "Configuration Preview") -> None:
320
+ """Display a configuration dictionary as formatted YAML.
321
+
322
+ Args:
323
+ config: Configuration dictionary to display
324
+ title: Title for the preview panel
325
+ """
326
+ import yaml
327
+
328
+ yaml_str = yaml.safe_dump(
329
+ config,
330
+ default_flow_style=False,
331
+ sort_keys=False,
332
+ indent=2,
333
+ allow_unicode=True,
334
+ )
335
+
336
+ # Calculate panel width to account for padding
337
+ panel_width = _console.width - LEFT_PADDING - RIGHT_PADDING
338
+
339
+ panel = Panel(
340
+ yaml_str,
341
+ title=title,
342
+ title_align="left",
343
+ border_style=NordColor.NORD14.value,
344
+ width=panel_width,
345
+ )
346
+ _print_with_padding(panel)
347
+
348
+
349
+ def print_success(message: str) -> None:
350
+ """Print a success message with green styling.
351
+
352
+ Args:
353
+ message: Success message to display
354
+ """
355
+ _print_with_padding(f"✅ {message}")
356
+
357
+
358
+ def print_error(message: str) -> None:
359
+ """Print an error message with red styling.
360
+
361
+ Args:
362
+ message: Error message to display
363
+ """
364
+ _print_with_padding(f"❌ {message}")
365
+
366
+
367
+ def print_warning(message: str) -> None:
368
+ """Print a warning message with yellow styling.
369
+
370
+ Args:
371
+ message: Warning message to display
372
+ """
373
+ _print_with_padding(f"⚠️ {message}")
374
+
375
+
376
+ def print_info(message: str) -> None:
377
+ """Print an info message with blue styling.
378
+
379
+ Args:
380
+ message: Info message to display
381
+ """
382
+ _print_with_padding(f"💡 {message}")
383
+
384
+
385
+ def print_text(message: str) -> None:
386
+ """Print a text message.
387
+
388
+ Args:
389
+ message: Text message to display
390
+ """
391
+ _print_with_padding(message)
392
+
393
+
394
+ def print_header(text: str) -> None:
395
+ """Print a styled header.
396
+
397
+ Args:
398
+ text: Header text
399
+ """
400
+ _console.print()
401
+ # Create a manual rule that respects padding
402
+ padding_str = " " * LEFT_PADDING
403
+ available_width = _console.width - LEFT_PADDING - RIGHT_PADDING
404
+
405
+ # Format the title text
406
+ title_text = f" {text} "
407
+ title_length = len(title_text)
408
+
409
+ # Calculate how many rule characters on each side
410
+ # (available_width - title_length) / 2
411
+ rule_chars = max(0, (available_width - title_length) // 2)
412
+ remaining = max(0, available_width - title_length - (rule_chars * 2))
413
+
414
+ # Build the rule string
415
+ rule_line = "─" * rule_chars + title_text + "─" * (rule_chars + remaining)
416
+
417
+ # Print with padding and styling
418
+ _console.print(f"{padding_str}[bold {NordColor.NORD8.value}]{rule_line}[/bold {NordColor.NORD8.value}]")
419
+ _console.print()
420
+
421
+
422
+ def print_navigation_tip() -> None:
423
+ """Display a concise navigation tip for interactive prompts."""
424
+ tip = "[dim]Tip: Use arrow keys to navigate menus, type [bold]'back'[/bold] to edit previous entries, press [bold]Tab[/bold] for completions[/dim]"
425
+ _print_with_padding(tip)
426
+ _console.print()
427
+
428
+
429
+ def _print_with_padding(content: str | Panel) -> None:
430
+ """Internal helper to print with left padding.
431
+
432
+ Args:
433
+ content: Content to print (string or Panel)
434
+ """
435
+
436
+ padding = " " * LEFT_PADDING
437
+ if isinstance(content, Panel):
438
+ # For panels, wrap in Rich's Padding to properly handle alignment
439
+ padded_content = Padding(content, (0, 0, 0, LEFT_PADDING))
440
+ _console.print(padded_content)
441
+ else:
442
+ # For strings, handle multi-line text
443
+ if "\n" in str(content):
444
+ lines = str(content).split("\n")
445
+ for line in lines:
446
+ _console.print(padding + line)
447
+ else:
448
+ _console.print(padding + str(content))