dataquery-sdk 0.0.7__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.
@@ -0,0 +1,169 @@
1
+ """
2
+ Custom exceptions for the DATAQUERY SDK.
3
+ """
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class DataQueryError(Exception):
9
+ """Base exception for all DATAQUERY SDK errors."""
10
+
11
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
12
+ self.message = message
13
+ self.details = details or {}
14
+ super().__init__(self.message)
15
+
16
+ def __str__(self) -> str:
17
+ if self.details:
18
+ return f"{self.message} - Details: {self.details}"
19
+ return self.message
20
+
21
+
22
+ class AuthenticationError(DataQueryError):
23
+ """Raised when authentication fails."""
24
+
25
+ def __init__(
26
+ self,
27
+ message: str = "Authentication failed",
28
+ details: Optional[Dict[str, Any]] = None,
29
+ ):
30
+ super().__init__(message, details)
31
+
32
+
33
+ class ValidationError(DataQueryError):
34
+ """Raised when input validation fails."""
35
+
36
+ def __init__(
37
+ self,
38
+ message: str = "Validation failed",
39
+ details: Optional[Dict[str, Any]] = None,
40
+ ):
41
+ super().__init__(message, details)
42
+
43
+
44
+ class NotFoundError(DataQueryError):
45
+ """Raised when a requested resource is not found."""
46
+
47
+ def __init__(
48
+ self, resource_type: str, resource_id: str, message: Optional[str] = None
49
+ ):
50
+ if message is None:
51
+ message = f"{resource_type} not found: {resource_id}"
52
+ super().__init__(
53
+ message, {"resource_type": resource_type, "resource_id": resource_id}
54
+ )
55
+
56
+
57
+ class RateLimitError(DataQueryError):
58
+ """Raised when rate limits are exceeded."""
59
+
60
+ def __init__(
61
+ self, message: str = "Rate limit exceeded", retry_after: Optional[int] = None
62
+ ):
63
+ details = {"retry_after": retry_after} if retry_after else {}
64
+ super().__init__(message, details)
65
+
66
+
67
+ class NetworkError(DataQueryError):
68
+ """Raised when network-related errors occur."""
69
+
70
+ def __init__(
71
+ self, message: str = "Network error occurred", status_code: Optional[int] = None
72
+ ):
73
+ details = {"status_code": status_code} if status_code else {}
74
+ super().__init__(message, details)
75
+
76
+
77
+ class ConfigurationError(DataQueryError):
78
+ """Raised when configuration is invalid or missing."""
79
+
80
+ def __init__(
81
+ self,
82
+ message: str = "Configuration error",
83
+ details: Optional[Dict[str, Any]] = None,
84
+ ):
85
+ super().__init__(message, details)
86
+
87
+
88
+ class DownloadError(DataQueryError):
89
+ """Raised when file download fails."""
90
+
91
+ def __init__(
92
+ self,
93
+ file_group_id: str,
94
+ group_id: str,
95
+ message: str = "Download failed",
96
+ details: Optional[Dict[str, Any]] = None,
97
+ ):
98
+ if details is None:
99
+ details = {}
100
+ details.update({"file_group_id": file_group_id, "group_id": group_id})
101
+ super().__init__(message, details)
102
+
103
+
104
+ class AvailabilityError(DataQueryError):
105
+ """Raised when checking file availability fails."""
106
+
107
+ def __init__(
108
+ self,
109
+ file_group_id: str,
110
+ group_id: str,
111
+ message: str = "Availability check failed",
112
+ details: Optional[Dict[str, Any]] = None,
113
+ ):
114
+ if details is None:
115
+ details = {}
116
+ details.update({"file_group_id": file_group_id, "group_id": group_id})
117
+ super().__init__(message, details)
118
+
119
+
120
+ class GroupNotFoundError(NotFoundError):
121
+ """Raised when a group is not found."""
122
+
123
+ def __init__(self, group_id: str):
124
+ super().__init__("Group", group_id)
125
+
126
+
127
+ class FileNotFoundError(NotFoundError):
128
+ """Raised when a file is not found."""
129
+
130
+ def __init__(self, file_group_id: str, group_id: str):
131
+ super().__init__(
132
+ "File", file_group_id, f"File {file_group_id} not found in group {group_id}"
133
+ )
134
+
135
+
136
+ class DateRangeError(ValidationError):
137
+ """Raised when date range validation fails."""
138
+
139
+ def __init__(self, start_date: str, end_date: str, message: Optional[str] = None):
140
+ if message is None:
141
+ message = f"Invalid date range: {start_date} to {end_date}"
142
+ super().__init__(message, {"start_date": start_date, "end_date": end_date})
143
+
144
+
145
+ class FileTypeError(ValidationError):
146
+ """Raised when file type validation fails."""
147
+
148
+ def __init__(self, file_type: str, allowed_types: Optional[list] = None):
149
+ message = f"Invalid file type: {file_type}"
150
+ if allowed_types:
151
+ message += f". Allowed types: {', '.join(allowed_types)}"
152
+ super().__init__(
153
+ message, {"file_type": file_type, "allowed_types": allowed_types}
154
+ )
155
+
156
+
157
+ class WorkflowError(DataQueryError):
158
+ """Raised when workflow operations fail."""
159
+
160
+ def __init__(
161
+ self,
162
+ workflow_name: str,
163
+ message: str = "Workflow failed",
164
+ details: Optional[Dict[str, Any]] = None,
165
+ ):
166
+ if details is None:
167
+ details = {}
168
+ details["workflow_name"] = workflow_name
169
+ super().__init__(message, details)
@@ -0,0 +1,415 @@
1
+ """
2
+ Enhanced logging configuration for the DATAQUERY SDK.
3
+
4
+ Provides structured logging, performance metrics, request/response logging,
5
+ and configurable log levels and formats.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ import structlog
16
+
17
+ logger = structlog.get_logger(__name__)
18
+
19
+
20
+ class LogLevel(str, Enum):
21
+ """Log levels."""
22
+
23
+ DEBUG = "DEBUG"
24
+ INFO = "INFO"
25
+ WARNING = "WARNING"
26
+ ERROR = "ERROR"
27
+ CRITICAL = "CRITICAL"
28
+
29
+
30
+ class LogFormat(str, Enum):
31
+ """Log formats."""
32
+
33
+ JSON = "json"
34
+ CONSOLE = "console"
35
+ SIMPLE = "simple"
36
+
37
+
38
+ @dataclass
39
+ class LoggingConfig:
40
+ """Configuration for logging."""
41
+
42
+ level: LogLevel = LogLevel.INFO
43
+ format: LogFormat = LogFormat.JSON
44
+ enable_console: bool = True
45
+ enable_file: bool = False
46
+ log_file: Optional[Path] = None
47
+ max_file_size: int = 10 * 1024 * 1024 # 10MB
48
+ backup_count: int = 5
49
+ enable_request_logging: bool = False
50
+ enable_performance_logging: bool = True
51
+ enable_metrics: bool = True
52
+ include_timestamps: bool = True
53
+ include_process_info: bool = True
54
+ include_thread_info: bool = True
55
+ custom_processors: list = field(default_factory=list)
56
+ log_correlation_id: bool = True
57
+
58
+
59
+ class RequestResponseLogger:
60
+ """Log HTTP requests and responses."""
61
+
62
+ def __init__(self, config: LoggingConfig):
63
+ self.config = config
64
+ self.logger = structlog.get_logger(__name__)
65
+
66
+ def log_request(
67
+ self,
68
+ method: str,
69
+ url: str,
70
+ headers: Dict[str, str],
71
+ body: Optional[str] = None,
72
+ correlation_id: Optional[str] = None,
73
+ ):
74
+ """Log HTTP request."""
75
+ if not self.config.enable_request_logging:
76
+ return
77
+
78
+ log_data = {
79
+ "event_type": "http_request",
80
+ "method": method,
81
+ "url": url,
82
+ "headers": self._sanitize_headers(headers),
83
+ "timestamp": datetime.now().isoformat(),
84
+ }
85
+
86
+ if body:
87
+ log_data["body"] = self._truncate_body(body)
88
+
89
+ if correlation_id:
90
+ log_data["correlation_id"] = correlation_id
91
+
92
+ self.logger.info("HTTP Request", **log_data)
93
+
94
+ def log_response(
95
+ self,
96
+ status_code: int,
97
+ headers: Dict[str, str],
98
+ body: Optional[str] = None,
99
+ duration: float = 0.0,
100
+ correlation_id: Optional[str] = None,
101
+ ):
102
+ """Log HTTP response."""
103
+ if not self.config.enable_request_logging:
104
+ return
105
+
106
+ log_data = {
107
+ "event_type": "http_response",
108
+ "status_code": status_code,
109
+ "headers": self._sanitize_headers(headers),
110
+ "duration_ms": round(duration * 1000, 2),
111
+ "timestamp": datetime.now().isoformat(),
112
+ }
113
+
114
+ if body:
115
+ log_data["body"] = self._truncate_body(body)
116
+
117
+ if correlation_id:
118
+ log_data["correlation_id"] = correlation_id
119
+
120
+ # Use appropriate log level based on status code
121
+ if status_code >= 500:
122
+ self.logger.error("HTTP Response", **log_data)
123
+ elif status_code >= 400:
124
+ self.logger.warning("HTTP Response", **log_data)
125
+ else:
126
+ self.logger.info("HTTP Response", **log_data)
127
+
128
+ def _sanitize_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
129
+ """Sanitize headers for logging."""
130
+ sanitized = {}
131
+ sensitive_headers = {"authorization", "cookie", "x-api-key"}
132
+
133
+ for key, value in headers.items():
134
+ if key.lower() in sensitive_headers:
135
+ sanitized[key] = "***"
136
+ else:
137
+ sanitized[key] = value
138
+
139
+ return sanitized
140
+
141
+ def _truncate_body(self, body: str, max_length: int = 1000) -> str:
142
+ """Truncate body for logging."""
143
+ if len(body) <= max_length:
144
+ return body
145
+ return body[:max_length] + "... [truncated]"
146
+
147
+
148
+ class PerformanceLogger:
149
+ """Log performance metrics."""
150
+
151
+ def __init__(self, config: LoggingConfig):
152
+ self.config = config
153
+ self.logger = structlog.get_logger(__name__)
154
+ self.metrics: Dict[str, Any] = {}
155
+
156
+ def log_operation_start(self, operation: str, **kwargs):
157
+ """Log start of operation."""
158
+ if not self.config.enable_performance_logging:
159
+ return
160
+
161
+ self.metrics[operation] = {"start_time": datetime.now(), "kwargs": kwargs}
162
+
163
+ self.logger.debug("Operation started", operation=operation, **kwargs)
164
+
165
+ def log_operation_end(
166
+ self, operation: str, duration: float, success: bool = True, **kwargs
167
+ ):
168
+ """Log end of operation."""
169
+ if not self.config.enable_performance_logging:
170
+ return
171
+
172
+ if operation in self.metrics:
173
+ start_time = self.metrics[operation]["start_time"]
174
+ total_duration = (datetime.now() - start_time).total_seconds()
175
+
176
+ log_data = {
177
+ "operation": operation,
178
+ "duration_ms": round(duration * 1000, 2),
179
+ "total_duration_ms": round(total_duration * 1000, 2),
180
+ "success": success,
181
+ **kwargs,
182
+ }
183
+
184
+ if success:
185
+ self.logger.info("Operation completed", **log_data)
186
+ else:
187
+ self.logger.warning("Operation failed", **log_data)
188
+
189
+ # Clean up
190
+ del self.metrics[operation]
191
+
192
+ def log_metric(self, name: str, value: float, unit: str = "", **kwargs):
193
+ """Log a performance metric."""
194
+ if not self.config.enable_metrics:
195
+ return
196
+
197
+ self.logger.info(
198
+ "Performance metric", metric_name=name, value=value, unit=unit, **kwargs
199
+ )
200
+
201
+
202
+ class StructuredLogger:
203
+ """Enhanced structured logger with correlation IDs and context."""
204
+
205
+ def __init__(self, config: LoggingConfig):
206
+ self.config = config
207
+ self._setup_logging()
208
+
209
+ def _setup_logging(self):
210
+ """Setup structured logging configuration."""
211
+ processors = [
212
+ *self.config.custom_processors,
213
+ structlog.stdlib.filter_by_level,
214
+ structlog.stdlib.add_logger_name,
215
+ structlog.stdlib.add_log_level,
216
+ structlog.stdlib.PositionalArgumentsFormatter(),
217
+ ]
218
+
219
+ if self.config.include_timestamps:
220
+ processors.append(structlog.processors.TimeStamper(fmt="iso"))
221
+
222
+ # Minimal and safe error processors
223
+ processors.extend(
224
+ [
225
+ structlog.processors.StackInfoRenderer(),
226
+ structlog.processors.format_exc_info,
227
+ structlog.processors.UnicodeDecoder(),
228
+ ]
229
+ )
230
+
231
+ if self.config.log_correlation_id:
232
+ processors.append(self._add_correlation_id)
233
+
234
+ if self.config.format == LogFormat.JSON:
235
+ processors.append(structlog.processors.JSONRenderer())
236
+ elif self.config.format == LogFormat.CONSOLE:
237
+ processors.append(structlog.dev.ConsoleRenderer())
238
+ else:
239
+ processors.append(structlog.dev.ConsoleRenderer(colors=False))
240
+
241
+ structlog.configure(
242
+ processors=processors,
243
+ context_class=dict,
244
+ logger_factory=structlog.stdlib.LoggerFactory(),
245
+ wrapper_class=structlog.stdlib.BoundLogger,
246
+ cache_logger_on_first_use=True,
247
+ )
248
+
249
+ def _add_correlation_id(self, logger, method_name, event_dict):
250
+ """Add correlation ID to log entries."""
251
+ # This would be implemented to add correlation IDs
252
+ # from context or generate new ones
253
+ return event_dict
254
+
255
+ def get_logger(self, name: Optional[str] = None) -> structlog.BoundLogger:
256
+ """Get a structured logger."""
257
+ return structlog.get_logger(name)
258
+
259
+
260
+ class LoggingManager:
261
+ """Main logging manager for the DATAQUERY SDK."""
262
+
263
+ def __init__(self, config: LoggingConfig):
264
+ self.config = config
265
+ self.structured_logger = StructuredLogger(config)
266
+ self.request_logger = RequestResponseLogger(config)
267
+ self.performance_logger = PerformanceLogger(config)
268
+
269
+ # Setup file logging if enabled
270
+ if config.enable_file and config.log_file:
271
+ self._setup_file_logging()
272
+
273
+ logger.info(
274
+ "Logging manager initialized",
275
+ level=config.level.value,
276
+ format=config.format.value,
277
+ enable_request_logging=config.enable_request_logging,
278
+ )
279
+
280
+ def _setup_file_logging(self):
281
+ """Setup file logging with rotation."""
282
+ try:
283
+ from logging.handlers import RotatingFileHandler
284
+
285
+ # Check if log_file is set
286
+ if not self.config.log_file:
287
+ logger.warning("File logging enabled but no log file specified")
288
+ return
289
+
290
+ # Ensure log directory exists
291
+ self.config.log_file.parent.mkdir(parents=True, exist_ok=True)
292
+
293
+ # Create rotating file handler
294
+ handler = RotatingFileHandler(
295
+ str(self.config.log_file),
296
+ maxBytes=self.config.max_file_size,
297
+ backupCount=self.config.backup_count,
298
+ )
299
+
300
+ # Set formatter
301
+ if self.config.format == LogFormat.JSON:
302
+ formatter = logging.Formatter("%(message)s")
303
+ else:
304
+ formatter = logging.Formatter(
305
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
306
+ )
307
+
308
+ handler.setFormatter(formatter)
309
+
310
+ # Add to root logger
311
+ root_logger = logging.getLogger()
312
+ root_logger.addHandler(handler)
313
+ root_logger.setLevel(getattr(logging, self.config.level.value))
314
+
315
+ logger.info(
316
+ "File logging configured",
317
+ log_file=str(self.config.log_file),
318
+ max_size=self.config.max_file_size,
319
+ backup_count=self.config.backup_count,
320
+ )
321
+
322
+ except Exception as e:
323
+ logger.warning("Failed to setup file logging", error=str(e))
324
+
325
+ def get_logger(self, name: Optional[str] = None) -> structlog.BoundLogger:
326
+ """Get a logger instance."""
327
+ return self.structured_logger.get_logger(name)
328
+
329
+ def log_request(
330
+ self,
331
+ method: str,
332
+ url: str,
333
+ headers: Dict[str, str],
334
+ body: Optional[str] = None,
335
+ correlation_id: Optional[str] = None,
336
+ ):
337
+ """Log HTTP request."""
338
+ self.request_logger.log_request(method, url, headers, body, correlation_id)
339
+
340
+ def log_response(
341
+ self,
342
+ status_code: int,
343
+ headers: Dict[str, str],
344
+ body: Optional[str] = None,
345
+ duration: float = 0.0,
346
+ correlation_id: Optional[str] = None,
347
+ ):
348
+ """Log HTTP response."""
349
+ self.request_logger.log_response(
350
+ status_code, headers, body, duration, correlation_id
351
+ )
352
+
353
+ def log_operation_start(self, operation: str, **kwargs):
354
+ """Log operation start."""
355
+ self.performance_logger.log_operation_start(operation, **kwargs)
356
+
357
+ def log_operation_end(
358
+ self, operation: str, duration: float, success: bool = True, **kwargs
359
+ ):
360
+ """Log operation end."""
361
+ self.performance_logger.log_operation_end(
362
+ operation, duration, success, **kwargs
363
+ )
364
+
365
+ def log_metric(self, name: str, value: float, unit: str = "", **kwargs):
366
+ """Log performance metric."""
367
+ self.performance_logger.log_metric(name, value, unit, **kwargs)
368
+
369
+
370
+ def create_logging_config(
371
+ level: LogLevel = LogLevel.INFO,
372
+ format: LogFormat = LogFormat.JSON,
373
+ enable_console: bool = True,
374
+ enable_file: bool = False,
375
+ log_file: Optional[Path] = None,
376
+ enable_request_logging: bool = False,
377
+ enable_performance_logging: bool = True,
378
+ ) -> LoggingConfig:
379
+ """
380
+ Create logging configuration.
381
+
382
+ Args:
383
+ level: Log level
384
+ format: Log format
385
+ enable_console: Whether to enable console logging
386
+ enable_file: Whether to enable file logging
387
+ log_file: Path to log file
388
+ enable_request_logging: Whether to log HTTP requests/responses
389
+ enable_performance_logging: Whether to log performance metrics
390
+
391
+ Returns:
392
+ Logging configuration
393
+ """
394
+ return LoggingConfig(
395
+ level=level,
396
+ format=format,
397
+ enable_console=enable_console,
398
+ enable_file=enable_file,
399
+ log_file=log_file,
400
+ enable_request_logging=enable_request_logging,
401
+ enable_performance_logging=enable_performance_logging,
402
+ )
403
+
404
+
405
+ def create_logging_manager(config: LoggingConfig) -> LoggingManager:
406
+ """
407
+ Create a logging manager with the specified configuration.
408
+
409
+ Args:
410
+ config: Logging configuration
411
+
412
+ Returns:
413
+ Configured logging manager
414
+ """
415
+ return LoggingManager(config)