spatelier 0.3.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.
- analytics/__init__.py +1 -0
- analytics/reporter.py +497 -0
- cli/__init__.py +1 -0
- cli/app.py +147 -0
- cli/audio.py +129 -0
- cli/cli_analytics.py +320 -0
- cli/cli_utils.py +282 -0
- cli/error_handlers.py +122 -0
- cli/files.py +299 -0
- cli/update.py +325 -0
- cli/video.py +823 -0
- cli/worker.py +615 -0
- core/__init__.py +1 -0
- core/analytics_dashboard.py +368 -0
- core/base.py +303 -0
- core/base_service.py +69 -0
- core/config.py +345 -0
- core/database_service.py +116 -0
- core/decorators.py +263 -0
- core/error_handler.py +210 -0
- core/file_tracker.py +254 -0
- core/interactive_cli.py +366 -0
- core/interfaces.py +166 -0
- core/job_queue.py +437 -0
- core/logger.py +79 -0
- core/package_updater.py +469 -0
- core/progress.py +228 -0
- core/service_factory.py +295 -0
- core/streaming.py +299 -0
- core/worker.py +765 -0
- database/__init__.py +1 -0
- database/connection.py +265 -0
- database/metadata.py +516 -0
- database/models.py +288 -0
- database/repository.py +592 -0
- database/transcription_storage.py +219 -0
- modules/__init__.py +1 -0
- modules/audio/__init__.py +5 -0
- modules/audio/converter.py +197 -0
- modules/video/__init__.py +16 -0
- modules/video/converter.py +191 -0
- modules/video/fallback_extractor.py +334 -0
- modules/video/services/__init__.py +18 -0
- modules/video/services/audio_extraction_service.py +274 -0
- modules/video/services/download_service.py +852 -0
- modules/video/services/metadata_service.py +190 -0
- modules/video/services/playlist_service.py +445 -0
- modules/video/services/transcription_service.py +491 -0
- modules/video/transcription_service.py +385 -0
- modules/video/youtube_api.py +397 -0
- spatelier/__init__.py +33 -0
- spatelier-0.3.0.dist-info/METADATA +260 -0
- spatelier-0.3.0.dist-info/RECORD +59 -0
- spatelier-0.3.0.dist-info/WHEEL +5 -0
- spatelier-0.3.0.dist-info/entry_points.txt +2 -0
- spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
- spatelier-0.3.0.dist-info/top_level.txt +7 -0
- utils/__init__.py +1 -0
- utils/helpers.py +250 -0
cli/audio.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audio processing CLI commands.
|
|
3
|
+
|
|
4
|
+
This module provides command-line interfaces for audio processing operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from cli.error_handlers import handle_cli_error, handle_file_not_found
|
|
16
|
+
from core.config import Config
|
|
17
|
+
from core.decorators import handle_errors, time_operation
|
|
18
|
+
from core.logger import get_logger
|
|
19
|
+
from core.service_factory import ServiceFactory
|
|
20
|
+
from modules.audio.converter import AudioConverter
|
|
21
|
+
|
|
22
|
+
# Create the audio CLI app
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="audio",
|
|
25
|
+
help="Audio processing commands",
|
|
26
|
+
rich_markup_mode="rich",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
@handle_errors(context="audio convert", verbose=True)
|
|
34
|
+
@time_operation(verbose=True)
|
|
35
|
+
def convert(
|
|
36
|
+
input_file: Path = typer.Argument(..., help="Input audio file"),
|
|
37
|
+
output_file: Path = typer.Argument(..., help="Output audio file"),
|
|
38
|
+
bitrate: int = typer.Option(320, "--bitrate", "-b", help="Audio bitrate (kbps)"),
|
|
39
|
+
format: str = typer.Option("mp3", "--format", "-f", help="Output format"),
|
|
40
|
+
verbose: bool = typer.Option(
|
|
41
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
42
|
+
),
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Convert audio to different format.
|
|
46
|
+
|
|
47
|
+
Supports various input and output formats including MP3, FLAC, WAV, etc.
|
|
48
|
+
"""
|
|
49
|
+
config = Config()
|
|
50
|
+
logger = get_logger("audio-convert", verbose=verbose)
|
|
51
|
+
|
|
52
|
+
# Validate input file
|
|
53
|
+
if not input_file.exists():
|
|
54
|
+
handle_file_not_found(input_file, "convert")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with ServiceFactory(config, verbose=verbose) as services:
|
|
58
|
+
# Create audio converter
|
|
59
|
+
converter = AudioConverter(config, verbose=verbose)
|
|
60
|
+
|
|
61
|
+
# Perform conversion
|
|
62
|
+
result = converter.convert(
|
|
63
|
+
input_file=input_file,
|
|
64
|
+
output_file=output_file,
|
|
65
|
+
format=format,
|
|
66
|
+
bitrate=bitrate,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Display success message
|
|
70
|
+
console.print(
|
|
71
|
+
Panel(
|
|
72
|
+
f"[green]✓[/green] Conversion successful!\n"
|
|
73
|
+
f"Input: {input_file.name}\n"
|
|
74
|
+
f"Output: {output_file.name}\n"
|
|
75
|
+
f"Format: {format.upper()}\n"
|
|
76
|
+
f"Bitrate: {bitrate}kbps\n"
|
|
77
|
+
f"Size: {result.metadata.get('input_size', 0):,} -> {result.metadata.get('output_size', 0):,} bytes",
|
|
78
|
+
title="Conversion Complete",
|
|
79
|
+
border_style="green",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
handle_cli_error(e, "audio conversion")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
@handle_errors(context="audio info", verbose=True)
|
|
89
|
+
@time_operation(verbose=True)
|
|
90
|
+
def info(
|
|
91
|
+
file_path: Path = typer.Argument(..., help="Audio file to analyze"),
|
|
92
|
+
verbose: bool = typer.Option(
|
|
93
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
94
|
+
),
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Display detailed information about an audio file.
|
|
98
|
+
"""
|
|
99
|
+
config = Config()
|
|
100
|
+
logger = get_logger("audio-info", verbose=verbose)
|
|
101
|
+
|
|
102
|
+
with ServiceFactory(config, verbose=verbose) as services:
|
|
103
|
+
try:
|
|
104
|
+
if not file_path.exists():
|
|
105
|
+
handle_file_not_found(file_path, "analyze")
|
|
106
|
+
|
|
107
|
+
# Create audio converter to get detailed info
|
|
108
|
+
converter = AudioConverter(config, verbose=verbose)
|
|
109
|
+
audio_info = converter.get_audio_info(file_path)
|
|
110
|
+
|
|
111
|
+
# Create info table
|
|
112
|
+
table = Table(title=f"Audio Information: {file_path.name}")
|
|
113
|
+
table.add_column("Property", style="cyan")
|
|
114
|
+
table.add_column("Value", style="magenta")
|
|
115
|
+
|
|
116
|
+
table.add_row("File Path", str(file_path))
|
|
117
|
+
table.add_row("File Size", f"{file_path.stat().st_size:,} bytes")
|
|
118
|
+
table.add_row("Format", audio_info.get("format", "unknown"))
|
|
119
|
+
table.add_row("Codec", audio_info.get("codec", "unknown"))
|
|
120
|
+
table.add_row("Duration", f"{audio_info.get('duration', 0):.2f} seconds")
|
|
121
|
+
table.add_row("Bitrate", f"{audio_info.get('bitrate', 0):,} bps")
|
|
122
|
+
table.add_row("Sample Rate", f"{audio_info.get('sample_rate', 0):,} Hz")
|
|
123
|
+
table.add_row("Channels", str(audio_info.get("channels", 0)))
|
|
124
|
+
table.add_row("Channel Layout", audio_info.get("channel_layout", "unknown"))
|
|
125
|
+
|
|
126
|
+
console.print(table)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
handle_cli_error(e, "audio analysis")
|
cli/cli_analytics.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics CLI commands.
|
|
3
|
+
|
|
4
|
+
This module provides command-line interfaces for analytics and reporting operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from sqlalchemy import func
|
|
16
|
+
|
|
17
|
+
from analytics.reporter import AnalyticsReporter
|
|
18
|
+
from core.config import Config
|
|
19
|
+
from core.decorators import handle_errors, time_operation
|
|
20
|
+
from core.logger import get_logger
|
|
21
|
+
from core.service_factory import ServiceFactory
|
|
22
|
+
|
|
23
|
+
# Create the analytics CLI app
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
name="analytics",
|
|
26
|
+
help="Analytics and reporting commands",
|
|
27
|
+
rich_markup_mode="rich",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
@handle_errors(context="analytics report", verbose=True)
|
|
35
|
+
@time_operation(verbose=True)
|
|
36
|
+
def report(
|
|
37
|
+
days: int = typer.Option(30, "--days", "-d", help="Number of days to analyze"),
|
|
38
|
+
output: Optional[Path] = typer.Option(
|
|
39
|
+
None, "--output", "-o", help="Output file path"
|
|
40
|
+
),
|
|
41
|
+
format: str = typer.Option(
|
|
42
|
+
"json", "--format", "-f", help="Output format (json, csv, excel)"
|
|
43
|
+
),
|
|
44
|
+
verbose: bool = typer.Option(
|
|
45
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
46
|
+
),
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Generate comprehensive analytics report.
|
|
50
|
+
"""
|
|
51
|
+
config = Config()
|
|
52
|
+
logger = get_logger("analytics-report", verbose=verbose)
|
|
53
|
+
|
|
54
|
+
with ServiceFactory(config, verbose=verbose) as services:
|
|
55
|
+
try:
|
|
56
|
+
with Progress(
|
|
57
|
+
SpinnerColumn(),
|
|
58
|
+
TextColumn("[progress.description]{task.description}"),
|
|
59
|
+
console=console,
|
|
60
|
+
) as progress:
|
|
61
|
+
task = progress.add_task("Generating analytics report...", total=None)
|
|
62
|
+
|
|
63
|
+
# Create analytics reporter with database service
|
|
64
|
+
from analytics.reporter import AnalyticsReporter
|
|
65
|
+
|
|
66
|
+
reporter = AnalyticsReporter(
|
|
67
|
+
config, verbose=verbose, db_service=services.database
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Generate reports
|
|
71
|
+
progress.update(task, description="Generating media report...")
|
|
72
|
+
media_report = reporter.generate_media_report(days)
|
|
73
|
+
|
|
74
|
+
progress.update(task, description="Generating processing report...")
|
|
75
|
+
processing_report = reporter.generate_processing_report(days)
|
|
76
|
+
|
|
77
|
+
progress.update(task, description="Generating usage report...")
|
|
78
|
+
usage_report = reporter.generate_usage_report(days)
|
|
79
|
+
|
|
80
|
+
# Combine reports
|
|
81
|
+
combined_report = {
|
|
82
|
+
"period_days": days,
|
|
83
|
+
"generated_at": reporter.session.query(func.now()).scalar(),
|
|
84
|
+
"media_report": media_report,
|
|
85
|
+
"processing_report": processing_report,
|
|
86
|
+
"usage_report": usage_report,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Display summary
|
|
90
|
+
table = Table(title=f"Analytics Summary (Last {days} days)")
|
|
91
|
+
table.add_column("Metric", style="cyan")
|
|
92
|
+
table.add_column("Value", style="magenta")
|
|
93
|
+
|
|
94
|
+
table.add_row("Total Files", str(media_report["total_files"]))
|
|
95
|
+
table.add_row("Total Size (MB)", f"{media_report['total_size_mb']:.2f}")
|
|
96
|
+
table.add_row("Total Jobs", str(processing_report["total_jobs"]))
|
|
97
|
+
table.add_row("Success Rate", f"{processing_report['success_rate']:.2%}")
|
|
98
|
+
table.add_row(
|
|
99
|
+
"Avg Processing Time",
|
|
100
|
+
f"{processing_report['avg_processing_time_seconds']:.2f}s",
|
|
101
|
+
)
|
|
102
|
+
table.add_row("Total Events", str(usage_report["total_events"]))
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
|
|
106
|
+
# Save to file if requested
|
|
107
|
+
if output:
|
|
108
|
+
progress.update(task, description="Saving report...")
|
|
109
|
+
reporter.export_data(output, format)
|
|
110
|
+
console.print(
|
|
111
|
+
Panel(
|
|
112
|
+
f"[green]✓[/green] Report saved to: {output}",
|
|
113
|
+
title="Report Saved",
|
|
114
|
+
border_style="green",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Analytics report failed: {e}")
|
|
120
|
+
console.print(
|
|
121
|
+
Panel(
|
|
122
|
+
f"[red]✗[/red] Analytics report failed: {str(e)}",
|
|
123
|
+
title="Error",
|
|
124
|
+
border_style="red",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
raise typer.Exit(1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command()
|
|
131
|
+
def visualize(
|
|
132
|
+
output_dir: Path = typer.Argument(..., help="Output directory for visualizations"),
|
|
133
|
+
days: int = typer.Option(30, "--days", "-d", help="Number of days to analyze"),
|
|
134
|
+
verbose: bool = typer.Option(
|
|
135
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
136
|
+
),
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Create visualization charts and dashboards.
|
|
140
|
+
"""
|
|
141
|
+
config = Config()
|
|
142
|
+
logger = get_logger("analytics-visualize", verbose=verbose)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with Progress(
|
|
146
|
+
SpinnerColumn(),
|
|
147
|
+
TextColumn("[progress.description]{task.description}"),
|
|
148
|
+
console=console,
|
|
149
|
+
) as progress:
|
|
150
|
+
task = progress.add_task("Creating visualizations...", total=None)
|
|
151
|
+
|
|
152
|
+
reporter = AnalyticsReporter(config, verbose=verbose)
|
|
153
|
+
|
|
154
|
+
progress.update(task, description="Generating charts...")
|
|
155
|
+
created_files = reporter.create_visualizations(output_dir, days)
|
|
156
|
+
|
|
157
|
+
console.print(
|
|
158
|
+
Panel(
|
|
159
|
+
f"[green]✓[/green] Created {len(created_files)} visualization files\n"
|
|
160
|
+
f"Output directory: {output_dir}",
|
|
161
|
+
title="Visualizations Created",
|
|
162
|
+
border_style="green",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# List created files
|
|
167
|
+
if created_files:
|
|
168
|
+
table = Table(title="Created Files")
|
|
169
|
+
table.add_column("File", style="cyan")
|
|
170
|
+
table.add_column("Type", style="magenta")
|
|
171
|
+
|
|
172
|
+
for file_path in created_files:
|
|
173
|
+
file_type = "Chart" if file_path.suffix == ".png" else "Dashboard"
|
|
174
|
+
table.add_row(str(file_path.name), file_type)
|
|
175
|
+
|
|
176
|
+
console.print(table)
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Visualization creation failed: {e}")
|
|
180
|
+
console.print(
|
|
181
|
+
Panel(
|
|
182
|
+
f"[red]✗[/red] Visualization creation failed: {str(e)}",
|
|
183
|
+
title="Error",
|
|
184
|
+
border_style="red",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
raise typer.Exit(1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command()
|
|
191
|
+
def stats(
|
|
192
|
+
days: int = typer.Option(30, "--days", "-d", help="Number of days to analyze"),
|
|
193
|
+
verbose: bool = typer.Option(
|
|
194
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
195
|
+
),
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Display quick statistics overview.
|
|
199
|
+
"""
|
|
200
|
+
config = Config()
|
|
201
|
+
logger = get_logger("analytics-stats", verbose=verbose)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
reporter = AnalyticsReporter(config, verbose=verbose)
|
|
205
|
+
|
|
206
|
+
# Get quick stats
|
|
207
|
+
media_report = reporter.generate_media_report(days)
|
|
208
|
+
processing_report = reporter.generate_processing_report(days)
|
|
209
|
+
usage_report = reporter.generate_usage_report(days)
|
|
210
|
+
|
|
211
|
+
# Create stats table
|
|
212
|
+
table = Table(title=f"Quick Stats (Last {days} days)")
|
|
213
|
+
table.add_column("Category", style="cyan")
|
|
214
|
+
table.add_column("Metric", style="yellow")
|
|
215
|
+
table.add_column("Value", style="magenta")
|
|
216
|
+
|
|
217
|
+
# Media stats
|
|
218
|
+
table.add_row("Media", "Total Files", str(media_report["total_files"]))
|
|
219
|
+
table.add_row(
|
|
220
|
+
"Media", "Total Size (MB)", f"{media_report['total_size_mb']:.2f}"
|
|
221
|
+
)
|
|
222
|
+
table.add_row(
|
|
223
|
+
"Media",
|
|
224
|
+
"Avg File Size (MB)",
|
|
225
|
+
f"{media_report['avg_file_size_bytes'] / (1024 * 1024):.2f}",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Processing stats
|
|
229
|
+
table.add_row("Processing", "Total Jobs", str(processing_report["total_jobs"]))
|
|
230
|
+
table.add_row(
|
|
231
|
+
"Processing", "Success Rate", f"{processing_report['success_rate']:.2%}"
|
|
232
|
+
)
|
|
233
|
+
table.add_row(
|
|
234
|
+
"Processing",
|
|
235
|
+
"Avg Time (s)",
|
|
236
|
+
f"{processing_report['avg_processing_time_seconds']:.2f}",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Usage stats
|
|
240
|
+
table.add_row("Usage", "Total Events", str(usage_report["total_events"]))
|
|
241
|
+
table.add_row(
|
|
242
|
+
"Usage", "Most Active Day", usage_report.get("most_active_day", "N/A")
|
|
243
|
+
)
|
|
244
|
+
table.add_row(
|
|
245
|
+
"Usage", "Trend", usage_report.get("trend_analysis", {}).get("trend", "N/A")
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
console.print(table)
|
|
249
|
+
|
|
250
|
+
# Show files by type
|
|
251
|
+
if media_report["files_by_type"]:
|
|
252
|
+
type_table = Table(title="Files by Type")
|
|
253
|
+
type_table.add_column("Type", style="cyan")
|
|
254
|
+
type_table.add_column("Count", style="magenta")
|
|
255
|
+
|
|
256
|
+
for file_type, count in media_report["files_by_type"].items():
|
|
257
|
+
type_table.add_row(file_type, str(count))
|
|
258
|
+
|
|
259
|
+
console.print(type_table)
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Stats display failed: {e}")
|
|
263
|
+
console.print(
|
|
264
|
+
Panel(
|
|
265
|
+
f"[red]✗[/red] Stats display failed: {str(e)}",
|
|
266
|
+
title="Error",
|
|
267
|
+
border_style="red",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
raise typer.Exit(1)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@app.command()
|
|
274
|
+
def export(
|
|
275
|
+
output_path: Path = typer.Argument(..., help="Output file path"),
|
|
276
|
+
format: str = typer.Option(
|
|
277
|
+
"json", "--format", "-f", help="Export format (json, csv, excel)"
|
|
278
|
+
),
|
|
279
|
+
days: int = typer.Option(30, "--days", "-d", help="Number of days to export"),
|
|
280
|
+
verbose: bool = typer.Option(
|
|
281
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
282
|
+
),
|
|
283
|
+
):
|
|
284
|
+
"""
|
|
285
|
+
Export analytics data to file.
|
|
286
|
+
"""
|
|
287
|
+
config = Config()
|
|
288
|
+
logger = get_logger("analytics-export", verbose=verbose)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
with Progress(
|
|
292
|
+
SpinnerColumn(),
|
|
293
|
+
TextColumn("[progress.description]{task.description}"),
|
|
294
|
+
console=console,
|
|
295
|
+
) as progress:
|
|
296
|
+
task = progress.add_task("Exporting analytics data...", total=None)
|
|
297
|
+
|
|
298
|
+
reporter = AnalyticsReporter(config, verbose=verbose)
|
|
299
|
+
|
|
300
|
+
progress.update(task, description="Generating reports...")
|
|
301
|
+
exported_file = reporter.export_data(output_path, format)
|
|
302
|
+
|
|
303
|
+
console.print(
|
|
304
|
+
Panel(
|
|
305
|
+
f"[green]✓[/green] Data exported to: {exported_file}",
|
|
306
|
+
title="Export Complete",
|
|
307
|
+
border_style="green",
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Data export failed: {e}")
|
|
313
|
+
console.print(
|
|
314
|
+
Panel(
|
|
315
|
+
f"[red]✗[/red] Data export failed: {str(e)}",
|
|
316
|
+
title="Error",
|
|
317
|
+
border_style="red",
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
raise typer.Exit(1)
|