ai-coding-assistant 0.5.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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Secure API key storage for cloud LLM providers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, List, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KeyStore:
|
|
10
|
+
"""
|
|
11
|
+
Secure storage for API keys.
|
|
12
|
+
|
|
13
|
+
Stores API keys in a JSON file with restricted permissions (600).
|
|
14
|
+
Keys are stored at: ~/.coding_assistant/api_keys.json
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Valid provider names (allowlist for security)
|
|
18
|
+
VALID_PROVIDERS = ["groq", "together", "gemini", "openai", "claude", "anthropic"]
|
|
19
|
+
|
|
20
|
+
def __init__(self, storage_dir: Optional[Path] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize keystore.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
storage_dir: Directory to store keys (default: ~/.coding_assistant)
|
|
26
|
+
"""
|
|
27
|
+
if storage_dir is None:
|
|
28
|
+
storage_dir = Path.home() / ".coding_assistant"
|
|
29
|
+
|
|
30
|
+
self.storage_dir = Path(storage_dir)
|
|
31
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
self.keys_file = self.storage_dir / "api_keys.json"
|
|
33
|
+
|
|
34
|
+
# Ensure file has secure permissions if it exists
|
|
35
|
+
if self.keys_file.exists():
|
|
36
|
+
self._ensure_secure_permissions()
|
|
37
|
+
|
|
38
|
+
def set_key(self, provider: str, api_key: str) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Store API key for a provider.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
provider: Provider name (groq, together, openai, claude)
|
|
44
|
+
api_key: API key to store
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if key was stored successfully
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If provider is not in allowlist
|
|
51
|
+
"""
|
|
52
|
+
# Validate provider
|
|
53
|
+
if provider.lower() not in self.VALID_PROVIDERS:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Invalid provider: {provider}. "
|
|
56
|
+
f"Must be one of: {', '.join(self.VALID_PROVIDERS)}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Load existing keys
|
|
60
|
+
keys = self._load_keys()
|
|
61
|
+
|
|
62
|
+
# Update key
|
|
63
|
+
keys[provider.lower()] = api_key
|
|
64
|
+
|
|
65
|
+
# Save with secure permissions
|
|
66
|
+
self._save_keys(keys)
|
|
67
|
+
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def get_key(self, provider: str) -> Optional[str]:
|
|
71
|
+
"""
|
|
72
|
+
Get API key for a provider.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
provider: Provider name
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
API key if found, None otherwise
|
|
79
|
+
"""
|
|
80
|
+
keys = self._load_keys()
|
|
81
|
+
return keys.get(provider.lower())
|
|
82
|
+
|
|
83
|
+
def remove_key(self, provider: str) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Remove API key for a provider.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
provider: Provider name
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if key was removed, False if key didn't exist
|
|
92
|
+
"""
|
|
93
|
+
keys = self._load_keys()
|
|
94
|
+
|
|
95
|
+
if provider.lower() in keys:
|
|
96
|
+
del keys[provider.lower()]
|
|
97
|
+
self._save_keys(keys)
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def list_providers(self) -> List[str]:
|
|
103
|
+
"""
|
|
104
|
+
List all providers with stored API keys.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of provider names
|
|
108
|
+
"""
|
|
109
|
+
keys = self._load_keys()
|
|
110
|
+
return list(keys.keys())
|
|
111
|
+
|
|
112
|
+
def get_all_keys(self) -> Dict[str, str]:
|
|
113
|
+
"""
|
|
114
|
+
Get all stored API keys.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary of provider -> API key
|
|
118
|
+
"""
|
|
119
|
+
return self._load_keys()
|
|
120
|
+
|
|
121
|
+
def clear_all(self) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Remove all stored API keys.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if keys were cleared
|
|
127
|
+
"""
|
|
128
|
+
self._save_keys({})
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
def _load_keys(self) -> Dict[str, str]:
|
|
132
|
+
"""
|
|
133
|
+
Load API keys from storage.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary of provider -> API key
|
|
137
|
+
"""
|
|
138
|
+
if not self.keys_file.exists():
|
|
139
|
+
return {}
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
with open(self.keys_file, 'r') as f:
|
|
143
|
+
keys = json.load(f)
|
|
144
|
+
# Ensure it's a dict
|
|
145
|
+
if not isinstance(keys, dict):
|
|
146
|
+
return {}
|
|
147
|
+
return keys
|
|
148
|
+
except (json.JSONDecodeError, IOError):
|
|
149
|
+
# If file is corrupted, return empty dict
|
|
150
|
+
return {}
|
|
151
|
+
|
|
152
|
+
def _save_keys(self, keys: Dict[str, str]) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Save API keys to storage with secure permissions.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
keys: Dictionary of provider -> API key
|
|
158
|
+
"""
|
|
159
|
+
# Write to file
|
|
160
|
+
with open(self.keys_file, 'w') as f:
|
|
161
|
+
json.dump(keys, f, indent=2)
|
|
162
|
+
|
|
163
|
+
# Set secure permissions (owner read/write only)
|
|
164
|
+
self._ensure_secure_permissions()
|
|
165
|
+
|
|
166
|
+
def _ensure_secure_permissions(self) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Ensure keys file has secure permissions (600).
|
|
169
|
+
|
|
170
|
+
Only the owner should be able to read/write the file.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
# chmod 600 (owner read/write only)
|
|
174
|
+
os.chmod(self.keys_file, 0o600)
|
|
175
|
+
except Exception:
|
|
176
|
+
# If chmod fails (e.g., on Windows), continue anyway
|
|
177
|
+
# Windows doesn't use UNIX permissions
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
def mask_key(self, api_key: str) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Mask an API key for display (show only first 8 and last 4 characters).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
api_key: API key to mask
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Masked API key (e.g., "gsk_1234...xyz9")
|
|
189
|
+
"""
|
|
190
|
+
if not api_key or len(api_key) < 12:
|
|
191
|
+
return "***"
|
|
192
|
+
|
|
193
|
+
return f"{api_key[:8]}...{api_key[-4:]}"
|
|
194
|
+
|
|
195
|
+
def get_masked_keys(self) -> Dict[str, str]:
|
|
196
|
+
"""
|
|
197
|
+
Get all keys with masking for safe display.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dictionary of provider -> masked API key
|
|
201
|
+
"""
|
|
202
|
+
keys = self._load_keys()
|
|
203
|
+
return {
|
|
204
|
+
provider: self.mask_key(key)
|
|
205
|
+
for provider, key in keys.items()
|
|
206
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Simple logging utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_logger(name: str, level: Optional[int] = None) -> logging.Logger:
|
|
8
|
+
"""
|
|
9
|
+
Get a configured logger instance.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
name: Logger name (usually __name__)
|
|
13
|
+
level: Optional logging level (defaults to INFO)
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Configured logger instance
|
|
17
|
+
"""
|
|
18
|
+
logger = logging.getLogger(name)
|
|
19
|
+
|
|
20
|
+
if not logger.handlers:
|
|
21
|
+
# Configure handler only if not already configured
|
|
22
|
+
handler = logging.StreamHandler()
|
|
23
|
+
formatter = logging.Formatter(
|
|
24
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
25
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
26
|
+
)
|
|
27
|
+
handler.setFormatter(formatter)
|
|
28
|
+
logger.addHandler(handler)
|
|
29
|
+
|
|
30
|
+
logger.setLevel(level or logging.INFO)
|
|
31
|
+
|
|
32
|
+
return logger
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Progress indicators and status utilities for consistent UX."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Callable, Any
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import (
|
|
7
|
+
Progress,
|
|
8
|
+
SpinnerColumn,
|
|
9
|
+
TextColumn,
|
|
10
|
+
BarColumn,
|
|
11
|
+
TaskProgressColumn,
|
|
12
|
+
TimeRemainingColumn
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProgressManager:
|
|
17
|
+
"""
|
|
18
|
+
Centralized progress management for consistent UX.
|
|
19
|
+
|
|
20
|
+
Provides:
|
|
21
|
+
- Simple status spinners
|
|
22
|
+
- Detailed progress bars
|
|
23
|
+
- Consistent styling
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, console: Optional[Console] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize progress manager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
console: Rich console (creates new one if not provided)
|
|
32
|
+
"""
|
|
33
|
+
self.console = console or Console()
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def status(self, message: str, spinner: str = "dots"):
|
|
37
|
+
"""
|
|
38
|
+
Show a simple status spinner.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message: Status message to display
|
|
42
|
+
spinner: Spinner style (dots, line, arc, etc.)
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
with progress_manager.status("Processing..."):
|
|
46
|
+
# Long operation
|
|
47
|
+
pass
|
|
48
|
+
"""
|
|
49
|
+
with self.console.status(f"[bold green]{message}[/bold green]", spinner=spinner):
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
@contextmanager
|
|
53
|
+
def progress_bar(
|
|
54
|
+
self,
|
|
55
|
+
description: str,
|
|
56
|
+
total: Optional[int] = None,
|
|
57
|
+
show_time: bool = False
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Show a detailed progress bar.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
description: Task description
|
|
64
|
+
total: Total steps (None for indeterminate)
|
|
65
|
+
show_time: Show time remaining
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
with progress_manager.progress_bar("Indexing", total=100) as progress:
|
|
69
|
+
for i in range(100):
|
|
70
|
+
progress.update(i + 1)
|
|
71
|
+
|
|
72
|
+
Yields:
|
|
73
|
+
Progress updater function
|
|
74
|
+
"""
|
|
75
|
+
columns = [
|
|
76
|
+
SpinnerColumn(),
|
|
77
|
+
TextColumn("[progress.description]{task.description}"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
if total is not None:
|
|
81
|
+
columns.append(BarColumn())
|
|
82
|
+
columns.append(TaskProgressColumn())
|
|
83
|
+
|
|
84
|
+
if show_time:
|
|
85
|
+
columns.append(TimeRemainingColumn())
|
|
86
|
+
|
|
87
|
+
with Progress(*columns, console=self.console, transient=False) as progress:
|
|
88
|
+
task = progress.add_task(f"[cyan]{description}", total=total)
|
|
89
|
+
|
|
90
|
+
class Updater:
|
|
91
|
+
"""Simple updater interface."""
|
|
92
|
+
|
|
93
|
+
def update(self, completed: Optional[int] = None, advance: Optional[int] = None):
|
|
94
|
+
"""Update progress."""
|
|
95
|
+
if completed is not None:
|
|
96
|
+
progress.update(task, completed=completed)
|
|
97
|
+
elif advance is not None:
|
|
98
|
+
progress.advance(task, advance)
|
|
99
|
+
|
|
100
|
+
def set_description(self, description: str):
|
|
101
|
+
"""Update description."""
|
|
102
|
+
progress.update(task, description=f"[cyan]{description}")
|
|
103
|
+
|
|
104
|
+
yield Updater()
|
|
105
|
+
|
|
106
|
+
def show_operation(
|
|
107
|
+
self,
|
|
108
|
+
operation_name: str,
|
|
109
|
+
operation_fn: Callable,
|
|
110
|
+
*args,
|
|
111
|
+
**kwargs
|
|
112
|
+
) -> Any:
|
|
113
|
+
"""
|
|
114
|
+
Execute an operation with automatic progress indication.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
operation_name: Name of the operation
|
|
118
|
+
operation_fn: Function to execute
|
|
119
|
+
*args, **kwargs: Arguments for operation_fn
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Result of operation_fn
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
result = progress_manager.show_operation(
|
|
126
|
+
"Indexing codebase",
|
|
127
|
+
retriever.index_codebase,
|
|
128
|
+
max_files=100
|
|
129
|
+
)
|
|
130
|
+
"""
|
|
131
|
+
with self.status(operation_name):
|
|
132
|
+
return operation_fn(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
def show_steps(self, steps: list[tuple[str, Callable]]) -> list[Any]:
|
|
135
|
+
"""
|
|
136
|
+
Execute multiple steps with progress tracking.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
steps: List of (step_name, step_function) tuples
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of results from each step
|
|
143
|
+
|
|
144
|
+
Usage:
|
|
145
|
+
results = progress_manager.show_steps([
|
|
146
|
+
("Parsing code", parser.parse),
|
|
147
|
+
("Generating embeddings", embedder.embed),
|
|
148
|
+
("Storing results", storage.save)
|
|
149
|
+
])
|
|
150
|
+
"""
|
|
151
|
+
results = []
|
|
152
|
+
|
|
153
|
+
with self.progress_bar("Executing steps", total=len(steps)) as progress:
|
|
154
|
+
for i, (step_name, step_fn) in enumerate(steps, 1):
|
|
155
|
+
progress.set_description(f"{step_name}...")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
result = step_fn()
|
|
159
|
+
results.append(result)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.console.print(f"[red]✗ {step_name} failed: {e}[/red]")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
progress.update(i)
|
|
165
|
+
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Global instance for convenience
|
|
170
|
+
_default_progress_manager: Optional[ProgressManager] = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_progress_manager(console: Optional[Console] = None) -> ProgressManager:
|
|
174
|
+
"""
|
|
175
|
+
Get global progress manager instance.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
console: Optional console to use
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ProgressManager instance
|
|
182
|
+
"""
|
|
183
|
+
global _default_progress_manager
|
|
184
|
+
|
|
185
|
+
if _default_progress_manager is None or console is not None:
|
|
186
|
+
_default_progress_manager = ProgressManager(console)
|
|
187
|
+
|
|
188
|
+
return _default_progress_manager
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Convenience functions
|
|
192
|
+
def status(message: str, console: Optional[Console] = None):
|
|
193
|
+
"""Context manager for simple status spinner."""
|
|
194
|
+
return get_progress_manager(console).status(message)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def progress_bar(
|
|
198
|
+
description: str,
|
|
199
|
+
total: Optional[int] = None,
|
|
200
|
+
show_time: bool = False,
|
|
201
|
+
console: Optional[Console] = None
|
|
202
|
+
):
|
|
203
|
+
"""Context manager for progress bar."""
|
|
204
|
+
return get_progress_manager(console).progress_bar(description, total, show_time)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def show_operation(
|
|
208
|
+
operation_name: str,
|
|
209
|
+
operation_fn: Callable,
|
|
210
|
+
*args,
|
|
211
|
+
console: Optional[Console] = None,
|
|
212
|
+
**kwargs
|
|
213
|
+
) -> Any:
|
|
214
|
+
"""Execute operation with progress."""
|
|
215
|
+
return get_progress_manager(console).show_operation(
|
|
216
|
+
operation_name, operation_fn, *args, **kwargs
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Progress callback helpers
|
|
221
|
+
class ProgressCallback:
|
|
222
|
+
"""
|
|
223
|
+
Progress callback for long-running operations.
|
|
224
|
+
|
|
225
|
+
Usage:
|
|
226
|
+
callback = ProgressCallback(total=100)
|
|
227
|
+
for i in range(100):
|
|
228
|
+
# ... work ...
|
|
229
|
+
callback.update(i + 1)
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
total: int,
|
|
235
|
+
description: str = "Processing",
|
|
236
|
+
console: Optional[Console] = None
|
|
237
|
+
):
|
|
238
|
+
"""
|
|
239
|
+
Initialize progress callback.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
total: Total number of items
|
|
243
|
+
description: Progress description
|
|
244
|
+
console: Rich console
|
|
245
|
+
"""
|
|
246
|
+
self.total = total
|
|
247
|
+
self.description = description
|
|
248
|
+
self.console = console or Console()
|
|
249
|
+
self.current = 0
|
|
250
|
+
|
|
251
|
+
self.progress = Progress(
|
|
252
|
+
SpinnerColumn(),
|
|
253
|
+
TextColumn("[progress.description]{task.description}"),
|
|
254
|
+
BarColumn(),
|
|
255
|
+
TaskProgressColumn(),
|
|
256
|
+
console=self.console
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
self.task = None
|
|
260
|
+
self._started = False
|
|
261
|
+
|
|
262
|
+
def __enter__(self):
|
|
263
|
+
"""Start progress display."""
|
|
264
|
+
self.progress.__enter__()
|
|
265
|
+
self.task = self.progress.add_task(
|
|
266
|
+
f"[cyan]{self.description}",
|
|
267
|
+
total=self.total
|
|
268
|
+
)
|
|
269
|
+
self._started = True
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def __exit__(self, *args):
|
|
273
|
+
"""Stop progress display."""
|
|
274
|
+
self.progress.__exit__(*args)
|
|
275
|
+
self._started = False
|
|
276
|
+
|
|
277
|
+
def update(self, current: int, description: Optional[str] = None):
|
|
278
|
+
"""
|
|
279
|
+
Update progress.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
current: Current progress value
|
|
283
|
+
description: Optional new description
|
|
284
|
+
"""
|
|
285
|
+
if not self._started:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
self.current = current
|
|
289
|
+
|
|
290
|
+
if description:
|
|
291
|
+
self.progress.update(
|
|
292
|
+
self.task,
|
|
293
|
+
completed=current,
|
|
294
|
+
description=f"[cyan]{description}"
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
self.progress.update(self.task, completed=current)
|
|
298
|
+
|
|
299
|
+
def advance(self, amount: int = 1):
|
|
300
|
+
"""Advance progress by amount."""
|
|
301
|
+
if self._started:
|
|
302
|
+
self.current += amount
|
|
303
|
+
self.progress.advance(self.task, amount)
|
|
304
|
+
|
|
305
|
+
def set_description(self, description: str):
|
|
306
|
+
"""Update description."""
|
|
307
|
+
if self._started:
|
|
308
|
+
self.progress.update(
|
|
309
|
+
self.task,
|
|
310
|
+
description=f"[cyan]{description}"
|
|
311
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Input validation utilities."""
|
|
2
|
+
|
|
3
|
+
from coding_assistant.validation.inputs import InputValidator
|
|
4
|
+
from coding_assistant.validation.files import FileValidator
|
|
5
|
+
from coding_assistant.validation.params import ParameterValidator
|
|
6
|
+
from coding_assistant.validation.sanitizers import InputSanitizer
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'InputValidator',
|
|
10
|
+
'FileValidator',
|
|
11
|
+
'ParameterValidator',
|
|
12
|
+
'InputSanitizer',
|
|
13
|
+
]
|