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.
Files changed (59) hide show
  1. analytics/__init__.py +1 -0
  2. analytics/reporter.py +497 -0
  3. cli/__init__.py +1 -0
  4. cli/app.py +147 -0
  5. cli/audio.py +129 -0
  6. cli/cli_analytics.py +320 -0
  7. cli/cli_utils.py +282 -0
  8. cli/error_handlers.py +122 -0
  9. cli/files.py +299 -0
  10. cli/update.py +325 -0
  11. cli/video.py +823 -0
  12. cli/worker.py +615 -0
  13. core/__init__.py +1 -0
  14. core/analytics_dashboard.py +368 -0
  15. core/base.py +303 -0
  16. core/base_service.py +69 -0
  17. core/config.py +345 -0
  18. core/database_service.py +116 -0
  19. core/decorators.py +263 -0
  20. core/error_handler.py +210 -0
  21. core/file_tracker.py +254 -0
  22. core/interactive_cli.py +366 -0
  23. core/interfaces.py +166 -0
  24. core/job_queue.py +437 -0
  25. core/logger.py +79 -0
  26. core/package_updater.py +469 -0
  27. core/progress.py +228 -0
  28. core/service_factory.py +295 -0
  29. core/streaming.py +299 -0
  30. core/worker.py +765 -0
  31. database/__init__.py +1 -0
  32. database/connection.py +265 -0
  33. database/metadata.py +516 -0
  34. database/models.py +288 -0
  35. database/repository.py +592 -0
  36. database/transcription_storage.py +219 -0
  37. modules/__init__.py +1 -0
  38. modules/audio/__init__.py +5 -0
  39. modules/audio/converter.py +197 -0
  40. modules/video/__init__.py +16 -0
  41. modules/video/converter.py +191 -0
  42. modules/video/fallback_extractor.py +334 -0
  43. modules/video/services/__init__.py +18 -0
  44. modules/video/services/audio_extraction_service.py +274 -0
  45. modules/video/services/download_service.py +852 -0
  46. modules/video/services/metadata_service.py +190 -0
  47. modules/video/services/playlist_service.py +445 -0
  48. modules/video/services/transcription_service.py +491 -0
  49. modules/video/transcription_service.py +385 -0
  50. modules/video/youtube_api.py +397 -0
  51. spatelier/__init__.py +33 -0
  52. spatelier-0.3.0.dist-info/METADATA +260 -0
  53. spatelier-0.3.0.dist-info/RECORD +59 -0
  54. spatelier-0.3.0.dist-info/WHEEL +5 -0
  55. spatelier-0.3.0.dist-info/entry_points.txt +2 -0
  56. spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
  57. spatelier-0.3.0.dist-info/top_level.txt +7 -0
  58. utils/__init__.py +1 -0
  59. utils/helpers.py +250 -0
core/decorators.py ADDED
@@ -0,0 +1,263 @@
1
+ """
2
+ Decorators for Spatelier modules.
3
+
4
+ This module provides decorators for common patterns like
5
+ error handling, timing, and validation.
6
+ """
7
+
8
+ import functools
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Optional, Union
12
+
13
+ from core.base import ProcessingResult
14
+ from core.error_handler import get_error_handler
15
+
16
+
17
+ def format_duration(seconds: float) -> str:
18
+ """Format duration in seconds to human-readable format."""
19
+ if seconds < 60:
20
+ return f"{seconds:.1f} seconds"
21
+ elif seconds < 3600:
22
+ minutes = int(seconds // 60)
23
+ remaining_seconds = seconds % 60
24
+ return f"{minutes}m {remaining_seconds:.1f}s"
25
+ else:
26
+ hours = int(seconds // 3600)
27
+ remaining_minutes = int((seconds % 3600) // 60)
28
+ remaining_seconds = seconds % 60
29
+ return f"{hours}h {remaining_minutes}m {remaining_seconds:.1f}s"
30
+
31
+
32
+ def handle_errors(context: str = "", return_result: bool = True, verbose: bool = False):
33
+ """
34
+ Decorator for automatic error handling.
35
+
36
+ Args:
37
+ context: Context for error reporting
38
+ return_result: Whether to return ProcessingResult on error
39
+ verbose: Enable verbose logging
40
+ """
41
+
42
+ def decorator(func: Callable) -> Callable:
43
+ @functools.wraps(func)
44
+ def wrapper(*args, **kwargs) -> Union[Any, ProcessingResult]:
45
+ try:
46
+ return func(*args, **kwargs)
47
+ except Exception as e:
48
+ handler = get_error_handler(verbose=verbose)
49
+ error_result = handler.handle_error(
50
+ e, context, return_result=return_result
51
+ )
52
+ if return_result:
53
+ return error_result
54
+ raise
55
+
56
+ return wrapper
57
+
58
+ return decorator
59
+
60
+
61
+ def time_operation(verbose: bool = False):
62
+ """
63
+ Decorator for timing operations.
64
+
65
+ Args:
66
+ verbose: Enable verbose logging
67
+ """
68
+
69
+ def decorator(func: Callable) -> Callable:
70
+ @functools.wraps(func)
71
+ def wrapper(*args, **kwargs) -> Any:
72
+ start_time = time.time()
73
+ try:
74
+ result = func(*args, **kwargs)
75
+ duration = time.time() - start_time
76
+
77
+ if verbose:
78
+ print(f"{func.__name__} completed in {format_duration(duration)}")
79
+
80
+ # Add timing to result if it's a ProcessingResult
81
+ if isinstance(result, ProcessingResult):
82
+ result.duration_seconds = duration
83
+
84
+ return result
85
+ except Exception as e:
86
+ duration = time.time() - start_time
87
+ if verbose:
88
+ print(
89
+ f"{func.__name__} failed after {format_duration(duration)}: {e}"
90
+ )
91
+ raise
92
+
93
+ return wrapper
94
+
95
+ return decorator
96
+
97
+
98
+ def validate_input(
99
+ input_validator: Optional[Callable] = None,
100
+ output_validator: Optional[Callable] = None,
101
+ ):
102
+ """
103
+ Decorator for input/output validation.
104
+
105
+ Args:
106
+ input_validator: Function to validate inputs
107
+ output_validator: Function to validate outputs
108
+ """
109
+
110
+ def decorator(func: Callable) -> Callable:
111
+ @functools.wraps(func)
112
+ def wrapper(*args, **kwargs) -> Any:
113
+ # Validate inputs
114
+ if input_validator:
115
+ try:
116
+ input_validator(*args, **kwargs)
117
+ except Exception as e:
118
+ raise ValueError(f"Input validation failed: {e}")
119
+
120
+ result = func(*args, **kwargs)
121
+
122
+ # Validate outputs
123
+ if output_validator:
124
+ try:
125
+ output_validator(result)
126
+ except Exception as e:
127
+ raise ValueError(f"Output validation failed: {e}")
128
+
129
+ return result
130
+
131
+ return wrapper
132
+
133
+ return decorator
134
+
135
+
136
+ def retry_on_failure(
137
+ max_retries: int = 3,
138
+ delay: float = 1.0,
139
+ backoff_factor: float = 2.0,
140
+ exceptions: tuple = (Exception,),
141
+ ):
142
+ """
143
+ Decorator for retrying operations on failure.
144
+
145
+ Args:
146
+ max_retries: Maximum number of retries
147
+ delay: Initial delay between retries
148
+ backoff_factor: Factor to multiply delay by after each retry
149
+ exceptions: Tuple of exceptions to retry on
150
+ """
151
+
152
+ def decorator(func: Callable) -> Callable:
153
+ @functools.wraps(func)
154
+ def wrapper(*args, **kwargs) -> Any:
155
+ current_delay = delay
156
+ last_exception = None
157
+
158
+ for attempt in range(max_retries + 1):
159
+ try:
160
+ return func(*args, **kwargs)
161
+ except exceptions as e:
162
+ last_exception = e
163
+ if attempt < max_retries:
164
+ time.sleep(current_delay)
165
+ current_delay *= backoff_factor
166
+ else:
167
+ raise last_exception
168
+
169
+ return None # Should never reach here
170
+
171
+ return wrapper
172
+
173
+ return decorator
174
+
175
+
176
+ def log_operation(
177
+ level: str = "INFO", include_args: bool = False, include_result: bool = False
178
+ ):
179
+ """
180
+ Decorator for logging operations.
181
+
182
+ Args:
183
+ level: Log level
184
+ include_args: Whether to include function arguments in log
185
+ include_result: Whether to include result in log
186
+ """
187
+
188
+ def decorator(func: Callable) -> Callable:
189
+ @functools.wraps(func)
190
+ def wrapper(*args, **kwargs) -> Any:
191
+ from core.logger import get_logger
192
+
193
+ logger = get_logger(func.__module__)
194
+
195
+ # Log function start
196
+ log_msg = f"Starting {func.__name__}"
197
+ if include_args:
198
+ log_msg += f" with args={args}, kwargs={kwargs}"
199
+
200
+ if level.upper() == "DEBUG":
201
+ logger.debug(log_msg)
202
+ elif level.upper() == "INFO":
203
+ logger.info(log_msg)
204
+ elif level.upper() == "WARNING":
205
+ logger.warning(log_msg)
206
+ elif level.upper() == "ERROR":
207
+ logger.error(log_msg)
208
+
209
+ try:
210
+ result = func(*args, **kwargs)
211
+
212
+ # Log function completion
213
+ log_msg = f"Completed {func.__name__}"
214
+ if include_result:
215
+ log_msg += f" with result={result}"
216
+
217
+ if level.upper() == "DEBUG":
218
+ logger.debug(log_msg)
219
+ elif level.upper() == "INFO":
220
+ logger.info(log_msg)
221
+ elif level.upper() == "WARNING":
222
+ logger.warning(log_msg)
223
+ elif level.upper() == "ERROR":
224
+ logger.error(log_msg)
225
+
226
+ return result
227
+ except Exception as e:
228
+ logger.error(f"Failed {func.__name__}: {e}")
229
+ raise
230
+
231
+ return wrapper
232
+
233
+ return decorator
234
+
235
+
236
+ def ensure_path_exists(path_arg: str = "path"):
237
+ """
238
+ Decorator to ensure a path argument exists.
239
+
240
+ Args:
241
+ path_arg: Name of the path argument to validate
242
+ """
243
+
244
+ def decorator(func: Callable) -> Callable:
245
+ @functools.wraps(func)
246
+ def wrapper(*args, **kwargs) -> Any:
247
+ # Get the path argument
248
+ if path_arg in kwargs:
249
+ path = kwargs[path_arg]
250
+ else:
251
+ # Try to get from positional args (this is a simplified approach)
252
+ path = args[0] if args else None
253
+
254
+ if path:
255
+ path_obj = Path(path)
256
+ if not path_obj.exists():
257
+ raise FileNotFoundError(f"Path does not exist: {path}")
258
+
259
+ return func(*args, **kwargs)
260
+
261
+ return wrapper
262
+
263
+ return decorator
core/error_handler.py ADDED
@@ -0,0 +1,210 @@
1
+ """
2
+ Centralized error handling for Spatelier.
3
+
4
+ This module provides consistent error handling patterns
5
+ across all services and modules.
6
+ """
7
+
8
+ import traceback
9
+ from pathlib import Path
10
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
11
+
12
+ from core.base import ProcessingResult
13
+ from core.logger import get_logger
14
+
15
+
16
+ class ErrorHandler:
17
+ """Centralized error handler for consistent error management."""
18
+
19
+ def __init__(self, logger_name: str = "ErrorHandler", verbose: bool = False):
20
+ """
21
+ Initialize error handler.
22
+
23
+ Args:
24
+ logger_name: Name for logger
25
+ verbose: Enable verbose logging
26
+ """
27
+ self.verbose = verbose
28
+ self.logger = get_logger(logger_name, verbose=verbose)
29
+ self.error_handlers: Dict[Type[Exception], Callable] = {}
30
+ self._register_default_handlers()
31
+
32
+ def _register_default_handlers(self):
33
+ """Register default error handlers."""
34
+ self.error_handlers[FileNotFoundError] = self._handle_file_not_found
35
+ self.error_handlers[PermissionError] = self._handle_permission_error
36
+ self.error_handlers[OSError] = self._handle_os_error
37
+ self.error_handlers[ValueError] = self._handle_value_error
38
+ self.error_handlers[KeyError] = self._handle_key_error
39
+ self.error_handlers[ImportError] = self._handle_import_error
40
+
41
+ def handle_error(
42
+ self, error: Exception, context: str = "", return_result: bool = True, **kwargs
43
+ ) -> Optional[ProcessingResult]:
44
+ """
45
+ Handle an error with appropriate response.
46
+
47
+ Args:
48
+ error: Exception to handle
49
+ context: Context where error occurred
50
+ return_result: Whether to return ProcessingResult
51
+ **kwargs: Additional context for error handling
52
+
53
+ Returns:
54
+ ProcessingResult if return_result=True, None otherwise
55
+ """
56
+ error_type = type(error)
57
+
58
+ # Log the error
59
+ self.logger.error(f"Error in {context}: {error}")
60
+ if self.verbose:
61
+ self.logger.debug(f"Error traceback: {traceback.format_exc()}")
62
+
63
+ # Get specific handler or use generic handler
64
+ handler = self.error_handlers.get(error_type, self._handle_generic_error)
65
+ result = handler(error, context, **kwargs)
66
+
67
+ if return_result:
68
+ return result
69
+ return None
70
+
71
+ def _handle_file_not_found(
72
+ self, error: FileNotFoundError, context: str, **kwargs
73
+ ) -> ProcessingResult:
74
+ """Handle FileNotFoundError."""
75
+ file_path = getattr(error, "filename", "unknown file")
76
+ return ProcessingResult.error_result(
77
+ message=f"File not found: {file_path}",
78
+ errors=[f"FileNotFoundError in {context}: {str(error)}"],
79
+ )
80
+
81
+ def _handle_permission_error(
82
+ self, error: PermissionError, context: str, **kwargs
83
+ ) -> ProcessingResult:
84
+ """Handle PermissionError."""
85
+ return ProcessingResult.error_result(
86
+ message="Permission denied",
87
+ errors=[f"PermissionError in {context}: {str(error)}"],
88
+ )
89
+
90
+ def _handle_os_error(
91
+ self, error: OSError, context: str, **kwargs
92
+ ) -> ProcessingResult:
93
+ """Handle OSError."""
94
+ return ProcessingResult.error_result(
95
+ message=f"System error: {error.strerror}",
96
+ errors=[f"OSError in {context}: {str(error)}"],
97
+ )
98
+
99
+ def _handle_value_error(
100
+ self, error: ValueError, context: str, **kwargs
101
+ ) -> ProcessingResult:
102
+ """Handle ValueError."""
103
+ return ProcessingResult.error_result(
104
+ message=f"Invalid value: {str(error)}",
105
+ errors=[f"ValueError in {context}: {str(error)}"],
106
+ )
107
+
108
+ def _handle_key_error(
109
+ self, error: KeyError, context: str, **kwargs
110
+ ) -> ProcessingResult:
111
+ """Handle KeyError."""
112
+ key = str(error).strip("'\"")
113
+ return ProcessingResult.error_result(
114
+ message=f"Missing key: {key}",
115
+ errors=[f"KeyError in {context}: Missing key '{key}'"],
116
+ )
117
+
118
+ def _handle_import_error(
119
+ self, error: ImportError, context: str, **kwargs
120
+ ) -> ProcessingResult:
121
+ """Handle ImportError."""
122
+ module = getattr(error, "name", "unknown module")
123
+ return ProcessingResult.error_result(
124
+ message=f"Import error: {module}",
125
+ errors=[f"ImportError in {context}: Cannot import {module}"],
126
+ )
127
+
128
+ def _handle_generic_error(
129
+ self, error: Exception, context: str, **kwargs
130
+ ) -> ProcessingResult:
131
+ """Handle generic errors."""
132
+ return ProcessingResult.error_result(
133
+ message=f"Unexpected error: {type(error).__name__}",
134
+ errors=[f"{type(error).__name__} in {context}: {str(error)}"],
135
+ )
136
+
137
+ def register_handler(self, exception_type: Type[Exception], handler: Callable):
138
+ """Register a custom error handler."""
139
+ self.error_handlers[exception_type] = handler
140
+
141
+ def safe_execute(
142
+ self,
143
+ func: Callable,
144
+ context: str = "",
145
+ default_result: Optional[ProcessingResult] = None,
146
+ *args,
147
+ **kwargs,
148
+ ) -> ProcessingResult:
149
+ """
150
+ Safely execute a function with error handling.
151
+
152
+ Args:
153
+ func: Function to execute
154
+ context: Context for error reporting
155
+ default_result: Default result if function fails
156
+ *args: Function arguments
157
+ **kwargs: Function keyword arguments
158
+
159
+ Returns:
160
+ ProcessingResult from function or error result
161
+ """
162
+ try:
163
+ result = func(*args, **kwargs)
164
+ if isinstance(result, ProcessingResult):
165
+ return result
166
+ else:
167
+ return ProcessingResult.success_result(
168
+ message="Operation completed successfully",
169
+ metadata={"result": result},
170
+ )
171
+ except Exception as e:
172
+ error_result = self.handle_error(e, context, return_result=True)
173
+ if default_result:
174
+ return default_result
175
+ return error_result or ProcessingResult.error_result(
176
+ message="Operation failed", errors=[f"Unexpected error in {context}"]
177
+ )
178
+
179
+
180
+ # Global error handler instance
181
+ _error_handler: Optional[ErrorHandler] = None
182
+
183
+
184
+ def get_error_handler(verbose: bool = False) -> ErrorHandler:
185
+ """Get global error handler instance."""
186
+ global _error_handler
187
+ if _error_handler is None:
188
+ _error_handler = ErrorHandler(verbose=verbose)
189
+ return _error_handler
190
+
191
+
192
+ def handle_error(
193
+ error: Exception, context: str = "", verbose: bool = False, **kwargs
194
+ ) -> ProcessingResult:
195
+ """Convenience function for handling errors."""
196
+ handler = get_error_handler(verbose=verbose)
197
+ return handler.handle_error(error, context, **kwargs)
198
+
199
+
200
+ def safe_execute(
201
+ func: Callable,
202
+ context: str = "",
203
+ verbose: bool = False,
204
+ default_result: Optional[ProcessingResult] = None,
205
+ *args,
206
+ **kwargs,
207
+ ) -> ProcessingResult:
208
+ """Convenience function for safe execution."""
209
+ handler = get_error_handler(verbose=verbose)
210
+ return handler.safe_execute(func, context, default_result, *args, **kwargs)