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.
- data_designer/__init__.py +15 -0
- data_designer/_version.py +34 -0
- data_designer/cli/README.md +236 -0
- data_designer/cli/__init__.py +6 -0
- data_designer/cli/commands/__init__.py +2 -0
- data_designer/cli/commands/list.py +130 -0
- data_designer/cli/commands/models.py +10 -0
- data_designer/cli/commands/providers.py +11 -0
- data_designer/cli/commands/reset.py +100 -0
- data_designer/cli/controllers/__init__.py +7 -0
- data_designer/cli/controllers/model_controller.py +246 -0
- data_designer/cli/controllers/provider_controller.py +317 -0
- data_designer/cli/forms/__init__.py +20 -0
- data_designer/cli/forms/builder.py +51 -0
- data_designer/cli/forms/field.py +180 -0
- data_designer/cli/forms/form.py +59 -0
- data_designer/cli/forms/model_builder.py +125 -0
- data_designer/cli/forms/provider_builder.py +76 -0
- data_designer/cli/main.py +44 -0
- data_designer/cli/repositories/__init__.py +8 -0
- data_designer/cli/repositories/base.py +39 -0
- data_designer/cli/repositories/model_repository.py +42 -0
- data_designer/cli/repositories/provider_repository.py +43 -0
- data_designer/cli/services/__init__.py +7 -0
- data_designer/cli/services/model_service.py +116 -0
- data_designer/cli/services/provider_service.py +111 -0
- data_designer/cli/ui.py +448 -0
- data_designer/cli/utils.py +47 -0
- data_designer/config/__init__.py +2 -0
- data_designer/config/analysis/column_profilers.py +89 -0
- data_designer/config/analysis/column_statistics.py +274 -0
- data_designer/config/analysis/dataset_profiler.py +60 -0
- data_designer/config/analysis/utils/errors.py +8 -0
- data_designer/config/analysis/utils/reporting.py +188 -0
- data_designer/config/base.py +68 -0
- data_designer/config/column_configs.py +354 -0
- data_designer/config/column_types.py +168 -0
- data_designer/config/config_builder.py +660 -0
- data_designer/config/data_designer_config.py +40 -0
- data_designer/config/dataset_builders.py +11 -0
- data_designer/config/datastore.py +151 -0
- data_designer/config/default_model_settings.py +123 -0
- data_designer/config/errors.py +19 -0
- data_designer/config/interface.py +54 -0
- data_designer/config/models.py +231 -0
- data_designer/config/preview_results.py +32 -0
- data_designer/config/processors.py +41 -0
- data_designer/config/sampler_constraints.py +51 -0
- data_designer/config/sampler_params.py +604 -0
- data_designer/config/seed.py +145 -0
- data_designer/config/utils/code_lang.py +83 -0
- data_designer/config/utils/constants.py +313 -0
- data_designer/config/utils/errors.py +19 -0
- data_designer/config/utils/info.py +88 -0
- data_designer/config/utils/io_helpers.py +273 -0
- data_designer/config/utils/misc.py +81 -0
- data_designer/config/utils/numerical_helpers.py +28 -0
- data_designer/config/utils/type_helpers.py +100 -0
- data_designer/config/utils/validation.py +336 -0
- data_designer/config/utils/visualization.py +427 -0
- data_designer/config/validator_params.py +96 -0
- data_designer/engine/__init__.py +2 -0
- data_designer/engine/analysis/column_profilers/base.py +55 -0
- data_designer/engine/analysis/column_profilers/judge_score_profiler.py +160 -0
- data_designer/engine/analysis/column_profilers/registry.py +20 -0
- data_designer/engine/analysis/column_statistics.py +142 -0
- data_designer/engine/analysis/dataset_profiler.py +125 -0
- data_designer/engine/analysis/errors.py +7 -0
- data_designer/engine/analysis/utils/column_statistics_calculations.py +209 -0
- data_designer/engine/analysis/utils/judge_score_processing.py +128 -0
- data_designer/engine/column_generators/__init__.py +2 -0
- data_designer/engine/column_generators/generators/__init__.py +2 -0
- data_designer/engine/column_generators/generators/base.py +61 -0
- data_designer/engine/column_generators/generators/expression.py +63 -0
- data_designer/engine/column_generators/generators/llm_generators.py +172 -0
- data_designer/engine/column_generators/generators/samplers.py +75 -0
- data_designer/engine/column_generators/generators/seed_dataset.py +149 -0
- data_designer/engine/column_generators/generators/validation.py +147 -0
- data_designer/engine/column_generators/registry.py +56 -0
- data_designer/engine/column_generators/utils/errors.py +13 -0
- data_designer/engine/column_generators/utils/judge_score_factory.py +57 -0
- data_designer/engine/column_generators/utils/prompt_renderer.py +98 -0
- data_designer/engine/configurable_task.py +82 -0
- data_designer/engine/dataset_builders/artifact_storage.py +181 -0
- data_designer/engine/dataset_builders/column_wise_builder.py +287 -0
- data_designer/engine/dataset_builders/errors.py +13 -0
- data_designer/engine/dataset_builders/multi_column_configs.py +44 -0
- data_designer/engine/dataset_builders/utils/__init__.py +2 -0
- data_designer/engine/dataset_builders/utils/concurrency.py +184 -0
- data_designer/engine/dataset_builders/utils/config_compiler.py +60 -0
- data_designer/engine/dataset_builders/utils/dag.py +56 -0
- data_designer/engine/dataset_builders/utils/dataset_batch_manager.py +190 -0
- data_designer/engine/dataset_builders/utils/errors.py +13 -0
- data_designer/engine/errors.py +49 -0
- data_designer/engine/model_provider.py +75 -0
- data_designer/engine/models/__init__.py +2 -0
- data_designer/engine/models/errors.py +308 -0
- data_designer/engine/models/facade.py +225 -0
- data_designer/engine/models/litellm_overrides.py +162 -0
- data_designer/engine/models/parsers/__init__.py +2 -0
- data_designer/engine/models/parsers/errors.py +34 -0
- data_designer/engine/models/parsers/parser.py +236 -0
- data_designer/engine/models/parsers/postprocessors.py +93 -0
- data_designer/engine/models/parsers/tag_parsers.py +60 -0
- data_designer/engine/models/parsers/types.py +82 -0
- data_designer/engine/models/recipes/base.py +79 -0
- data_designer/engine/models/recipes/response_recipes.py +291 -0
- data_designer/engine/models/registry.py +118 -0
- data_designer/engine/models/usage.py +75 -0
- data_designer/engine/models/utils.py +38 -0
- data_designer/engine/processing/ginja/__init__.py +2 -0
- data_designer/engine/processing/ginja/ast.py +64 -0
- data_designer/engine/processing/ginja/environment.py +461 -0
- data_designer/engine/processing/ginja/exceptions.py +54 -0
- data_designer/engine/processing/ginja/record.py +30 -0
- data_designer/engine/processing/gsonschema/__init__.py +2 -0
- data_designer/engine/processing/gsonschema/exceptions.py +8 -0
- data_designer/engine/processing/gsonschema/schema_transformers.py +81 -0
- data_designer/engine/processing/gsonschema/types.py +8 -0
- data_designer/engine/processing/gsonschema/validators.py +143 -0
- data_designer/engine/processing/processors/base.py +15 -0
- data_designer/engine/processing/processors/drop_columns.py +46 -0
- data_designer/engine/processing/processors/registry.py +20 -0
- data_designer/engine/processing/utils.py +120 -0
- data_designer/engine/registry/base.py +97 -0
- data_designer/engine/registry/data_designer_registry.py +37 -0
- data_designer/engine/registry/errors.py +10 -0
- data_designer/engine/resources/managed_dataset_generator.py +35 -0
- data_designer/engine/resources/managed_dataset_repository.py +194 -0
- data_designer/engine/resources/managed_storage.py +63 -0
- data_designer/engine/resources/resource_provider.py +46 -0
- data_designer/engine/resources/seed_dataset_data_store.py +66 -0
- data_designer/engine/sampling_gen/column.py +89 -0
- data_designer/engine/sampling_gen/constraints.py +95 -0
- data_designer/engine/sampling_gen/data_sources/base.py +214 -0
- data_designer/engine/sampling_gen/data_sources/errors.py +10 -0
- data_designer/engine/sampling_gen/data_sources/sources.py +342 -0
- data_designer/engine/sampling_gen/entities/__init__.py +2 -0
- data_designer/engine/sampling_gen/entities/assets/zip_area_code_map.parquet +0 -0
- data_designer/engine/sampling_gen/entities/dataset_based_person_fields.py +64 -0
- data_designer/engine/sampling_gen/entities/email_address_utils.py +169 -0
- data_designer/engine/sampling_gen/entities/errors.py +8 -0
- data_designer/engine/sampling_gen/entities/national_id_utils.py +100 -0
- data_designer/engine/sampling_gen/entities/person.py +142 -0
- data_designer/engine/sampling_gen/entities/phone_number.py +122 -0
- data_designer/engine/sampling_gen/errors.py +24 -0
- data_designer/engine/sampling_gen/generator.py +121 -0
- data_designer/engine/sampling_gen/jinja_utils.py +60 -0
- data_designer/engine/sampling_gen/people_gen.py +203 -0
- data_designer/engine/sampling_gen/person_constants.py +54 -0
- data_designer/engine/sampling_gen/schema.py +143 -0
- data_designer/engine/sampling_gen/schema_builder.py +59 -0
- data_designer/engine/sampling_gen/utils.py +40 -0
- data_designer/engine/secret_resolver.py +80 -0
- data_designer/engine/validators/__init__.py +17 -0
- data_designer/engine/validators/base.py +36 -0
- data_designer/engine/validators/local_callable.py +34 -0
- data_designer/engine/validators/python.py +245 -0
- data_designer/engine/validators/remote.py +83 -0
- data_designer/engine/validators/sql.py +60 -0
- data_designer/errors.py +5 -0
- data_designer/essentials/__init__.py +137 -0
- data_designer/interface/__init__.py +2 -0
- data_designer/interface/data_designer.py +351 -0
- data_designer/interface/errors.py +16 -0
- data_designer/interface/results.py +55 -0
- data_designer/logging.py +161 -0
- data_designer/plugin_manager.py +83 -0
- data_designer/plugins/__init__.py +6 -0
- data_designer/plugins/errors.py +10 -0
- data_designer/plugins/plugin.py +69 -0
- data_designer/plugins/registry.py +86 -0
- data_designer-0.1.0.dist-info/METADATA +173 -0
- data_designer-0.1.0.dist-info/RECORD +177 -0
- data_designer-0.1.0.dist-info/WHEEL +4 -0
- data_designer-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|
data_designer/cli/ui.py
ADDED
|
@@ -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))
|