data-designer 0.4.0rc3__py3-none-any.whl → 0.5.0rc1__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.
@@ -3,13 +3,53 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import re
7
+
6
8
  from rich.table import Table
7
9
 
10
+ from data_designer.cli.repositories.mcp_provider_repository import MCPProviderRepository
8
11
  from data_designer.cli.repositories.model_repository import ModelRepository
9
12
  from data_designer.cli.repositories.provider_repository import ProviderRepository
13
+ from data_designer.cli.repositories.tool_repository import ToolRepository
10
14
  from data_designer.cli.ui import console, print_error, print_header, print_info, print_warning
15
+ from data_designer.config.mcp import LocalStdioMCPProvider, MCPProvider
11
16
  from data_designer.config.utils.constants import DATA_DESIGNER_HOME, NordColor
12
17
 
18
+ # Pattern for valid environment variable names (uppercase letters, digits, underscores, not starting with digit)
19
+ _ENV_VAR_PATTERN = re.compile(r"^[A-Z_][A-Z0-9_]*$")
20
+
21
+
22
+ def _is_env_var_name(value: str) -> bool:
23
+ """Check if a string looks like an environment variable name.
24
+
25
+ Returns True only if the string matches the pattern for typical env var names:
26
+ - All uppercase letters, digits, and underscores only
27
+ - Does not start with a digit
28
+ - At least one character
29
+
30
+ This is intentionally conservative to avoid leaking secrets that happen
31
+ to be uppercase (e.g., base32-encoded API keys).
32
+ """
33
+ return bool(_ENV_VAR_PATTERN.match(value))
34
+
35
+
36
+ def _mask_api_key(api_key: str | None) -> str:
37
+ """Mask an API key for display, preserving environment variable names.
38
+
39
+ Args:
40
+ api_key: The API key value or None.
41
+
42
+ Returns:
43
+ A display string: "(not set)" if None, the original value if it looks
44
+ like an env var name, or a masked version showing only the last 4 chars.
45
+ """
46
+ if not api_key:
47
+ return "(not set)"
48
+ # Only show unmasked if it looks like a valid environment variable name
49
+ if _is_env_var_name(api_key):
50
+ return api_key
51
+ return "***" + api_key[-4:] if len(api_key) > 4 else "***"
52
+
13
53
 
14
54
  def list_command() -> None:
15
55
  """List current Data Designer configurations.
@@ -22,9 +62,11 @@ def list_command() -> None:
22
62
  print_info(f"Configuration directory: {DATA_DESIGNER_HOME}")
23
63
  console.print()
24
64
 
25
- # Display providers
65
+ # Display all configuration types
26
66
  display_providers(ProviderRepository(DATA_DESIGNER_HOME))
27
67
  display_models(ModelRepository(DATA_DESIGNER_HOME))
68
+ display_mcp_providers(MCPProviderRepository(DATA_DESIGNER_HOME))
69
+ display_tool_configs(ToolRepository(DATA_DESIGNER_HOME))
28
70
 
29
71
 
30
72
  def display_providers(provider_repo: ProviderRepository) -> None:
@@ -56,11 +98,7 @@ def display_providers(provider_repo: ProviderRepository) -> None:
56
98
 
57
99
  for provider in provider_registry.providers:
58
100
  is_default = "✓" if provider.name == default_name else ""
59
- api_key_display = provider.api_key or "(not set)"
60
-
61
- # Mask actual API keys (keep env var names visible)
62
- if provider.api_key and not provider.api_key.isupper():
63
- api_key_display = "***" + provider.api_key[-4:] if len(provider.api_key) > 4 else "***"
101
+ api_key_display = _mask_api_key(provider.api_key)
64
102
 
65
103
  table.add_row(
66
104
  provider.name,
@@ -116,3 +154,102 @@ def display_models(model_repo: ModelRepository) -> None:
116
154
  except Exception as e:
117
155
  print_error(f"Error loading model configuration: {e}")
118
156
  console.print()
157
+
158
+
159
+ def display_mcp_providers(mcp_provider_repo: MCPProviderRepository) -> None:
160
+ """Load and display MCP provider configurations.
161
+
162
+ Handles both MCPProvider (remote SSE) and LocalStdioMCPProvider (subprocess).
163
+
164
+ Args:
165
+ mcp_provider_repo: MCP provider repository
166
+
167
+ Returns:
168
+ None
169
+ """
170
+ try:
171
+ registry = mcp_provider_repo.load()
172
+
173
+ if not registry:
174
+ print_warning("MCP providers have not been configured. Run 'data-designer config mcp' to configure them.")
175
+ console.print()
176
+ return
177
+
178
+ # Display as table
179
+ table = Table(title="MCP Providers", border_style=NordColor.NORD8.value)
180
+ table.add_column("Name", style=NordColor.NORD14.value, no_wrap=True)
181
+ table.add_column("Endpoint / Command", style=NordColor.NORD4.value)
182
+ table.add_column("Type", style=NordColor.NORD9.value, no_wrap=True)
183
+ table.add_column("API Key / Env", style=NordColor.NORD7.value)
184
+
185
+ for provider in registry.providers:
186
+ if isinstance(provider, MCPProvider):
187
+ endpoint_display = provider.endpoint
188
+ api_key_display = _mask_api_key(provider.api_key)
189
+ elif isinstance(provider, LocalStdioMCPProvider):
190
+ # Display command + args for stdio provider
191
+ args_str = " ".join(provider.args) if provider.args else ""
192
+ endpoint_display = f"{provider.command} {args_str}".strip()
193
+ # Show env vars count for stdio provider
194
+ api_key_display = f"{len(provider.env)} env vars" if provider.env else "(none)"
195
+ else:
196
+ endpoint_display = "(unknown)"
197
+ api_key_display = "(unknown)"
198
+
199
+ table.add_row(
200
+ provider.name,
201
+ endpoint_display,
202
+ provider.provider_type,
203
+ api_key_display,
204
+ )
205
+
206
+ console.print(table)
207
+ console.print()
208
+ except Exception as e:
209
+ print_error(f"Error loading MCP provider configuration: {e}")
210
+ console.print()
211
+
212
+
213
+ def display_tool_configs(tool_repo: ToolRepository) -> None:
214
+ """Load and display tool configurations.
215
+
216
+ Args:
217
+ tool_repo: Tool repository
218
+
219
+ Returns:
220
+ None
221
+ """
222
+ try:
223
+ registry = tool_repo.load()
224
+
225
+ if not registry:
226
+ print_warning("Tool configs have not been configured. Run 'data-designer config tools' to configure them.")
227
+ console.print()
228
+ return
229
+
230
+ # Display as table
231
+ table = Table(title="Tool Configurations", border_style=NordColor.NORD8.value)
232
+ table.add_column("Alias", style=NordColor.NORD14.value, no_wrap=True)
233
+ table.add_column("Providers", style=NordColor.NORD4.value)
234
+ table.add_column("Allowed Tools", style=NordColor.NORD9.value)
235
+ table.add_column("Max Turns", style=NordColor.NORD7.value, justify="center")
236
+ table.add_column("Timeout", style=NordColor.NORD15.value, justify="center")
237
+
238
+ for tc in registry.tool_configs:
239
+ providers_display = ", ".join(tc.providers)
240
+ allow_tools_display = ", ".join(tc.allow_tools) if tc.allow_tools else "(all)"
241
+ timeout_display = f"{tc.timeout_sec}s" if tc.timeout_sec else "(none)"
242
+
243
+ table.add_row(
244
+ tc.tool_alias,
245
+ providers_display,
246
+ allow_tools_display,
247
+ str(tc.max_tool_call_turns),
248
+ timeout_display,
249
+ )
250
+
251
+ console.print(table)
252
+ console.print()
253
+ except Exception as e:
254
+ print_error(f"Error loading tool configuration: {e}")
255
+ console.print()
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from data_designer.cli.controllers.mcp_provider_controller import MCPProviderController
7
+ from data_designer.config.utils.constants import DATA_DESIGNER_HOME
8
+
9
+
10
+ def mcp_command() -> None:
11
+ """Configure MCP providers interactively."""
12
+ controller = MCPProviderController(DATA_DESIGNER_HOME)
13
+ controller.run()
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from data_designer.cli.controllers.tool_controller import ToolController
7
+ from data_designer.config.utils.constants import DATA_DESIGNER_HOME
8
+
9
+
10
+ def tools_command() -> None:
11
+ """Configure tool configs interactively."""
12
+ controller = ToolController(DATA_DESIGNER_HOME)
13
+ controller.run()
@@ -0,0 +1,241 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ import re
8
+ from pathlib import Path
9
+
10
+ # Pattern for valid environment variable names (uppercase letters, digits, underscores, not starting with digit)
11
+ _ENV_VAR_PATTERN = re.compile(r"^[A-Z_][A-Z0-9_]*$")
12
+
13
+ from data_designer.cli.forms.mcp_provider_builder import MCPProviderFormBuilder, MCPProviderT
14
+ from data_designer.cli.repositories.mcp_provider_repository import MCPProviderRepository
15
+ from data_designer.cli.services.mcp_provider_service import MCPProviderService
16
+ from data_designer.cli.ui import (
17
+ confirm_action,
18
+ console,
19
+ display_config_preview,
20
+ print_error,
21
+ print_header,
22
+ print_info,
23
+ print_success,
24
+ select_with_arrows,
25
+ )
26
+ from data_designer.config.mcp import LocalStdioMCPProvider, MCPProvider
27
+
28
+
29
+ class MCPProviderController:
30
+ """Controller for MCP provider configuration workflows."""
31
+
32
+ def __init__(self, config_dir: Path):
33
+ self.config_dir = config_dir
34
+ self.repository = MCPProviderRepository(config_dir)
35
+ self.service = MCPProviderService(self.repository)
36
+
37
+ def run(self) -> None:
38
+ """Main entry point for MCP provider configuration."""
39
+ print_header("Configure MCP Providers")
40
+ print_info(f"Configuration directory: {self.config_dir}")
41
+ console.print()
42
+
43
+ # Check for existing configuration
44
+ providers = self.service.list_all()
45
+
46
+ if providers:
47
+ self._show_existing_config()
48
+ mode = self._select_mode()
49
+ else:
50
+ print_info("No MCP providers configured yet")
51
+ console.print()
52
+ mode = "add"
53
+
54
+ if mode is None:
55
+ print_info("No changes made")
56
+ return
57
+
58
+ # Execute selected mode
59
+ mode_handlers = {
60
+ "add": self._handle_add,
61
+ "update": self._handle_update,
62
+ "delete": self._handle_delete,
63
+ "delete_all": self._handle_delete_all,
64
+ }
65
+
66
+ handler = mode_handlers.get(mode)
67
+ if handler:
68
+ handler()
69
+
70
+ def _show_existing_config(self) -> None:
71
+ """Display current configuration."""
72
+ registry = self.repository.load()
73
+ if not registry:
74
+ return
75
+
76
+ print_info(f"Found {len(registry.providers)} configured MCP provider(s)")
77
+ console.print()
78
+
79
+ # Display configuration (with masked API keys)
80
+ config_dict = registry.model_dump(mode="json", exclude_none=True)
81
+ masked_config = self._mask_api_keys(config_dict)
82
+ display_config_preview(masked_config, "Current Configuration")
83
+ console.print()
84
+
85
+ def _mask_api_keys(self, config: dict) -> dict:
86
+ """Mask API keys in configuration for display."""
87
+ masked = copy.deepcopy(config)
88
+
89
+ if "providers" in masked:
90
+ for provider in masked["providers"]:
91
+ if "api_key" in provider and provider["api_key"]:
92
+ api_key = provider["api_key"]
93
+ # Only show unmasked if it looks like a valid environment variable name
94
+ if not _ENV_VAR_PATTERN.match(api_key):
95
+ provider["api_key"] = "***" + api_key[-4:] if len(api_key) > 4 else "***"
96
+
97
+ return masked
98
+
99
+ def _select_mode(self) -> str | None:
100
+ """Prompt user to select operation mode."""
101
+ options = {
102
+ "add": "Add a new MCP provider",
103
+ "update": "Update an existing MCP provider",
104
+ "delete": "Delete an MCP provider",
105
+ "delete_all": "Delete all MCP providers",
106
+ "exit": "Exit without changes",
107
+ }
108
+
109
+ result = select_with_arrows(
110
+ options,
111
+ "What would you like to do?",
112
+ default_key="add",
113
+ allow_back=False,
114
+ )
115
+
116
+ return None if result == "exit" or result is None else result
117
+
118
+ def _handle_add(self) -> None:
119
+ """Handle adding new MCP providers."""
120
+ existing_names = {p.name for p in self.service.list_all()}
121
+
122
+ while True:
123
+ # Create builder with current existing names
124
+ builder = MCPProviderFormBuilder(existing_names)
125
+ provider = builder.run()
126
+
127
+ if provider is None:
128
+ break
129
+
130
+ # Attempt to add
131
+ try:
132
+ self.service.add(provider)
133
+ print_success(f"MCP provider '{provider.name}' added successfully")
134
+ existing_names.add(provider.name)
135
+ except ValueError as e:
136
+ print_error(f"Failed to add MCP provider: {e}")
137
+ break
138
+
139
+ # Ask if they want to add more
140
+ if not self._confirm_add_another():
141
+ break
142
+
143
+ def _handle_update(self) -> None:
144
+ """Handle updating an existing MCP provider."""
145
+ providers = self.service.list_all()
146
+ if not providers:
147
+ print_error("No MCP providers to update")
148
+ return
149
+
150
+ # Select provider to update
151
+ selected_name = self._select_provider(providers, "Select MCP provider to update")
152
+ if selected_name is None:
153
+ return
154
+
155
+ provider = self.service.get_by_name(selected_name)
156
+ if not provider:
157
+ print_error(f"MCP provider '{selected_name}' not found")
158
+ return
159
+
160
+ # Run builder with existing data
161
+ existing_names = {p.name for p in providers if p.name != selected_name}
162
+ builder = MCPProviderFormBuilder(existing_names)
163
+ initial_data = provider.model_dump(mode="json", exclude_none=True)
164
+ updated_provider = builder.run(initial_data)
165
+
166
+ if updated_provider:
167
+ try:
168
+ self.service.update(selected_name, updated_provider)
169
+ print_success(f"MCP provider '{updated_provider.name}' updated successfully")
170
+ except ValueError as e:
171
+ print_error(f"Failed to update MCP provider: {e}")
172
+
173
+ def _handle_delete(self) -> None:
174
+ """Handle deleting an MCP provider."""
175
+ providers = self.service.list_all()
176
+ if not providers:
177
+ print_error("No MCP providers to delete")
178
+ return
179
+
180
+ # Select provider to delete
181
+ selected_name = self._select_provider(providers, "Select MCP provider to delete")
182
+ if selected_name is None:
183
+ return
184
+
185
+ # Confirm deletion
186
+ console.print()
187
+ if confirm_action(f"Delete MCP provider '{selected_name}'?", default=False):
188
+ try:
189
+ self.service.delete(selected_name)
190
+ print_success(f"MCP provider '{selected_name}' deleted successfully")
191
+ except ValueError as e:
192
+ print_error(f"Failed to delete MCP provider: {e}")
193
+
194
+ def _handle_delete_all(self) -> None:
195
+ """Handle deleting all MCP providers."""
196
+ providers = self.service.list_all()
197
+ if not providers:
198
+ print_error("No MCP providers to delete")
199
+ return
200
+
201
+ # List providers to be deleted
202
+ console.print()
203
+ provider_count = len(providers)
204
+ provider_names = ", ".join([f"'{p.name}'" for p in providers])
205
+
206
+ if confirm_action(
207
+ f"Delete ALL ({provider_count}) MCP provider(s): {provider_names}?\n This action cannot be undone.",
208
+ default=False,
209
+ ):
210
+ try:
211
+ self.repository.delete()
212
+ print_success(f"All ({provider_count}) MCP provider(s) deleted successfully")
213
+ except Exception as e:
214
+ print_error(f"Failed to delete all MCP providers: {e}")
215
+
216
+ def _select_provider(self, providers: list[MCPProviderT], prompt: str, default: str | None = None) -> str | None:
217
+ """Helper to select an MCP provider from list."""
218
+ options = {}
219
+ for p in providers:
220
+ if isinstance(p, MCPProvider):
221
+ options[p.name] = f"{p.name} (SSE: {p.endpoint})"
222
+ elif isinstance(p, LocalStdioMCPProvider):
223
+ options[p.name] = f"{p.name} (stdio: {p.command})"
224
+ else:
225
+ options[p.name] = p.name
226
+ return select_with_arrows(
227
+ options,
228
+ prompt,
229
+ default_key=default or providers[0].name,
230
+ allow_back=False,
231
+ )
232
+
233
+ def _confirm_add_another(self) -> bool:
234
+ """Ask if user wants to add another MCP provider."""
235
+ result = select_with_arrows(
236
+ {"yes": "Add another MCP provider", "no": "Finish"},
237
+ "Add another MCP provider?",
238
+ default_key="no",
239
+ allow_back=False,
240
+ )
241
+ return result == "yes"
@@ -4,10 +4,14 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import copy
7
+ import re
7
8
  from pathlib import Path
8
9
  from typing import TYPE_CHECKING
9
10
 
10
11
  from data_designer.cli.forms.provider_builder import ProviderFormBuilder
12
+
13
+ # Pattern for valid environment variable names (uppercase letters, digits, underscores, not starting with digit)
14
+ _ENV_VAR_PATTERN = re.compile(r"^[A-Z_][A-Z0-9_]*$")
11
15
  from data_designer.cli.repositories.model_repository import ModelRepository
12
16
  from data_designer.cli.repositories.provider_repository import ProviderRepository
13
17
  from data_designer.cli.services.model_service import ModelService
@@ -95,8 +99,8 @@ class ProviderController:
95
99
  for provider in masked["providers"]:
96
100
  if "api_key" in provider and provider["api_key"]:
97
101
  api_key = provider["api_key"]
98
- # Keep environment variable names visible
99
- if not api_key.isupper():
102
+ # Only show unmasked if it looks like a valid environment variable name
103
+ if not _ENV_VAR_PATTERN.match(api_key):
100
104
  provider["api_key"] = "***" + api_key[-4:] if len(api_key) > 4 else "***"
101
105
 
102
106
  return masked