bustapi 0.1.0__cp311-cp311-win_amd64.whl → 0.1.5__cp311-cp311-win_amd64.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.

Potentially problematic release.


This version of bustapi might be problematic. Click here for more details.

bustapi/logging.py ADDED
@@ -0,0 +1,468 @@
1
+ """
2
+ BustAPI Smart Colorful Logging
3
+
4
+ FastAPI-style colorful logging with enhanced features for BustAPI.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ import time
10
+ from typing import Optional
11
+
12
+ try:
13
+ import colorama
14
+ from colorama import Back, Fore, Style
15
+
16
+ colorama.init(autoreset=True)
17
+ HAS_COLORAMA = True
18
+ except ImportError:
19
+ # Fallback without colors
20
+ class _MockColor:
21
+ def __getattr__(self, name):
22
+ return ""
23
+
24
+ Fore = Back = Style = _MockColor()
25
+ HAS_COLORAMA = False
26
+
27
+
28
+ class ColoredFormatter(logging.Formatter):
29
+ """Colorful log formatter similar to FastAPI."""
30
+
31
+ # Color mapping for different log levels
32
+ COLORS = {
33
+ "DEBUG": Fore.CYAN,
34
+ "INFO": Fore.GREEN,
35
+ "WARNING": Fore.YELLOW,
36
+ "ERROR": Fore.RED,
37
+ "CRITICAL": Fore.RED + Style.BRIGHT,
38
+ }
39
+
40
+ # HTTP status code colors
41
+ STATUS_COLORS = {
42
+ "1": Fore.CYAN, # 1xx Informational
43
+ "2": Fore.GREEN, # 2xx Success
44
+ "3": Fore.YELLOW, # 3xx Redirection
45
+ "4": Fore.RED, # 4xx Client Error
46
+ "5": Fore.MAGENTA, # 5xx Server Error
47
+ }
48
+
49
+ def __init__(self, use_colors: bool = True):
50
+ super().__init__()
51
+ self.use_colors = use_colors and HAS_COLORAMA
52
+
53
+ def format(self, record: logging.LogRecord) -> str:
54
+ if not self.use_colors:
55
+ return self._format_plain(record)
56
+
57
+ # Get color for log level
58
+ level_color = self.COLORS.get(record.levelname, "")
59
+
60
+ # Format timestamp
61
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
62
+ timestamp_colored = f"{Fore.BLUE}{timestamp}{Style.RESET_ALL}"
63
+
64
+ # Format log level
65
+ level_colored = f"{level_color}{record.levelname:8}{Style.RESET_ALL}"
66
+
67
+ # Format logger name
68
+ logger_name = record.name
69
+ if logger_name.startswith("bustapi"):
70
+ logger_name_colored = f"{Fore.MAGENTA}{logger_name}{Style.RESET_ALL}"
71
+ else:
72
+ logger_name_colored = f"{Fore.CYAN}{logger_name}{Style.RESET_ALL}"
73
+
74
+ # Format message
75
+ message = record.getMessage()
76
+
77
+ # Special formatting for HTTP requests
78
+ if hasattr(record, "method") and hasattr(record, "path"):
79
+ method = record.method
80
+ path = record.path
81
+ status_code = getattr(record, "status_code", "200")
82
+ duration_formatted = getattr(record, "duration_formatted", "N/A")
83
+ error = getattr(record, "error", None)
84
+
85
+ # Color method
86
+ method_colors = {
87
+ "GET": Fore.BLUE,
88
+ "POST": Fore.GREEN,
89
+ "PUT": Fore.YELLOW,
90
+ "DELETE": Fore.RED,
91
+ "PATCH": Fore.CYAN,
92
+ "HEAD": Fore.MAGENTA,
93
+ "OPTIONS": Fore.WHITE,
94
+ }
95
+ method_colored = f"{method_colors.get(method, '')}{method}{Style.RESET_ALL}"
96
+
97
+ # Color status code
98
+ status_first_digit = str(status_code)[0] if status_code else "2"
99
+ status_color = self.STATUS_COLORS.get(status_first_digit, Fore.WHITE)
100
+ status_colored = f"{status_color}{status_code}{Style.RESET_ALL}"
101
+
102
+ # Color path
103
+ path_colored = f"{Fore.WHITE}{path}{Style.RESET_ALL}"
104
+
105
+ # Color duration based on time
106
+ duration_color = Fore.GREEN # Default fast
107
+ if hasattr(record, "duration") and record.duration:
108
+ duration = record.duration
109
+ if duration >= 1.0:
110
+ duration_color = Fore.RED # >= 1s = Red (slow)
111
+ elif duration >= 0.5:
112
+ duration_color = Fore.YELLOW # >= 500ms = Yellow (medium)
113
+ elif duration >= 0.1:
114
+ duration_color = Fore.CYAN # >= 100ms = Cyan (ok)
115
+ else:
116
+ duration_color = Fore.GREEN # < 100ms = Green (fast)
117
+
118
+ duration_colored = f"{duration_color}{duration_formatted}{Style.RESET_ALL}"
119
+
120
+ # Build message
121
+ message = f"{method_colored} {path_colored} - {status_colored} - {duration_colored}"
122
+
123
+ # Add error if present
124
+ if error:
125
+ error_colored = f"{Fore.RED}ERROR: {error}{Style.RESET_ALL}"
126
+ message += f" - {error_colored}"
127
+
128
+ # Special formatting for startup messages
129
+ elif "starting" in message.lower() or "listening" in message.lower():
130
+ message = f"{Fore.GREEN}{Style.BRIGHT}{message}{Style.RESET_ALL}"
131
+
132
+ # Special formatting for error messages
133
+ elif record.levelname in ["ERROR", "CRITICAL"]:
134
+ message = f"{Fore.RED}{message}{Style.RESET_ALL}"
135
+
136
+ # Build final log line
137
+ log_line = (
138
+ f"{timestamp_colored} {level_colored} {logger_name_colored:20} {message}"
139
+ )
140
+
141
+ # Add exception info if present
142
+ if record.exc_info:
143
+ log_line += f"\n{self.formatException(record.exc_info)}"
144
+
145
+ return log_line
146
+
147
+ def _format_plain(self, record: logging.LogRecord) -> str:
148
+ """Plain formatting without colors."""
149
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
150
+ level = record.levelname.ljust(8)
151
+ logger_name = record.name.ljust(20)
152
+ message = record.getMessage()
153
+
154
+ log_line = f"{timestamp} {level} {logger_name} {message}"
155
+
156
+ if record.exc_info:
157
+ log_line += f"\n{self.formatException(record.exc_info)}"
158
+
159
+ return log_line
160
+
161
+
162
+ class BustAPILogger:
163
+ """BustAPI logger with smart colorful output."""
164
+
165
+ def __init__(self, name: str = "bustapi", use_colors: bool = True):
166
+ self.logger = logging.getLogger(name)
167
+ self.use_colors = use_colors
168
+ self._setup_logger()
169
+
170
+ def _setup_logger(self):
171
+ """Setup logger with colored formatter."""
172
+ if self.logger.handlers:
173
+ return # Already configured
174
+
175
+ # Create console handler
176
+ handler = logging.StreamHandler(sys.stdout)
177
+
178
+ # Set colored formatter
179
+ formatter = ColoredFormatter(use_colors=self.use_colors)
180
+ handler.setFormatter(formatter)
181
+
182
+ # Add handler to logger
183
+ self.logger.addHandler(handler)
184
+ self.logger.setLevel(logging.INFO)
185
+ self.logger.propagate = False
186
+
187
+ def info(self, message: str, **kwargs):
188
+ """Log info message with optional extra data."""
189
+ self.logger.info(message, extra=kwargs)
190
+
191
+ def debug(self, message: str, **kwargs):
192
+ """Log debug message with optional extra data."""
193
+ self.logger.debug(message, extra=kwargs)
194
+
195
+ def warning(self, message: str, **kwargs):
196
+ """Log warning message with optional extra data."""
197
+ self.logger.warning(message, extra=kwargs)
198
+
199
+ def error(self, message: str, **kwargs):
200
+ """Log error message with optional extra data."""
201
+ self.logger.error(message, extra=kwargs)
202
+
203
+ def critical(self, message: str, **kwargs):
204
+ """Log critical message with optional extra data."""
205
+ self.logger.critical(message, extra=kwargs)
206
+
207
+ def log_request(
208
+ self,
209
+ method: str,
210
+ path: str,
211
+ status_code: int,
212
+ duration: Optional[float] = None,
213
+ error: Optional[str] = None,
214
+ **kwargs,
215
+ ):
216
+ """Log HTTP request with colored formatting and smart time units."""
217
+ # Format duration with appropriate time unit
218
+ duration_str = (
219
+ self._format_duration(duration) if duration is not None else "N/A"
220
+ )
221
+
222
+ # Create log message
223
+ message = f"{method} {path} - {status_code} - {duration_str}"
224
+ if error:
225
+ message += f" - ERROR: {error}"
226
+
227
+ # Choose log level based on status code and error
228
+ if error or (status_code >= 500):
229
+ log_level = "error"
230
+ elif status_code >= 400:
231
+ log_level = "warning"
232
+ else:
233
+ log_level = "info"
234
+
235
+ # Log with appropriate level
236
+ getattr(self, log_level)(
237
+ message,
238
+ extra={
239
+ "method": method,
240
+ "path": path,
241
+ "status_code": status_code,
242
+ "duration": duration,
243
+ "duration_formatted": duration_str,
244
+ "error": error,
245
+ **kwargs,
246
+ },
247
+ )
248
+
249
+ def _format_duration(self, duration: float) -> str:
250
+ """Format duration with appropriate time unit (s, ms, μs, ns)."""
251
+ if duration >= 1.0:
252
+ return f"{duration:.3f}s"
253
+ elif duration >= 0.001:
254
+ return f"{duration * 1000:.3f}ms"
255
+ elif duration >= 0.000001:
256
+ return f"{duration * 1000000:.3f}μs"
257
+ else:
258
+ return f"{duration * 1000000000:.3f}ns"
259
+
260
+ def log_startup(self, message: str, **kwargs):
261
+ """Log startup message with special formatting."""
262
+ self.info(f"🚀 {message}", **kwargs)
263
+
264
+ def log_shutdown(self, message: str, **kwargs):
265
+ """Log shutdown message with special formatting."""
266
+ self.info(f"🛑 {message}", **kwargs)
267
+
268
+
269
+ # Global logger instance
270
+ logger = BustAPILogger()
271
+
272
+
273
+ # Simple interface - just import logging and use these
274
+ def setup(level: str = "INFO", use_colors: bool = True):
275
+ """Simple setup function."""
276
+ return setup_logging(level, use_colors)
277
+
278
+
279
+ def info(message: str, **kwargs):
280
+ """Simple info logging."""
281
+ logger.info(message, **kwargs)
282
+
283
+
284
+ def debug(message: str, **kwargs):
285
+ """Simple debug logging."""
286
+ logger.debug(message, **kwargs)
287
+
288
+
289
+ def warning(message: str, **kwargs):
290
+ """Simple warning logging."""
291
+ logger.warning(message, **kwargs)
292
+
293
+
294
+ def error(message: str, **kwargs):
295
+ """Simple error logging."""
296
+ logger.error(message, **kwargs)
297
+
298
+
299
+ def request(
300
+ method: str, path: str, status_code: int, duration: float = None, error: str = None
301
+ ):
302
+ """Simple request logging."""
303
+ logger.log_request(method, path, status_code, duration, error)
304
+
305
+
306
+ def setup_logging(
307
+ level: str = "INFO", use_colors: bool = True, logger_name: str = "bustapi"
308
+ ) -> BustAPILogger:
309
+ """
310
+ Setup BustAPI logging with colorful output.
311
+
312
+ Args:
313
+ level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
314
+ use_colors: Whether to use colored output
315
+ logger_name: Name of the logger
316
+
317
+ Returns:
318
+ Configured BustAPILogger instance
319
+ """
320
+ # Create logger
321
+ bustapi_logger = BustAPILogger(name=logger_name, use_colors=use_colors)
322
+
323
+ # Set log level
324
+ log_level = getattr(logging, level.upper(), logging.INFO)
325
+ bustapi_logger.logger.setLevel(log_level)
326
+
327
+ return bustapi_logger
328
+
329
+
330
+ def get_logger(name: str = "bustapi") -> BustAPILogger:
331
+ """Get a BustAPI logger instance."""
332
+ return BustAPILogger(name=name)
333
+
334
+
335
+ # Convenience functions
336
+ def log_info(message: str, **kwargs):
337
+ """Log info message using global logger."""
338
+ logger.info(message, **kwargs)
339
+
340
+
341
+ def log_debug(message: str, **kwargs):
342
+ """Log debug message using global logger."""
343
+ logger.debug(message, **kwargs)
344
+
345
+
346
+ def log_warning(message: str, **kwargs):
347
+ """Log warning message using global logger."""
348
+ logger.warning(message, **kwargs)
349
+
350
+
351
+ def log_error(message: str, **kwargs):
352
+ """Log error message using global logger."""
353
+ logger.error(message, **kwargs)
354
+
355
+
356
+ def log_request(
357
+ method: str,
358
+ path: str,
359
+ status_code: int,
360
+ duration: Optional[float] = None,
361
+ error: Optional[str] = None,
362
+ **kwargs,
363
+ ):
364
+ """Log HTTP request using global logger."""
365
+ logger.log_request(method, path, status_code, duration, error, **kwargs)
366
+
367
+
368
+ def log_startup(message: str, **kwargs):
369
+ """Log startup message using global logger."""
370
+ logger.log_startup(message, **kwargs)
371
+
372
+
373
+ def log_shutdown(message: str, **kwargs):
374
+ """Log shutdown message using global logger."""
375
+ logger.log_shutdown(message, **kwargs)
376
+
377
+
378
+ def request_logging_middleware(app_logger: Optional[BustAPILogger] = None):
379
+ """
380
+ Decorator to add automatic request logging to route handlers.
381
+
382
+ Args:
383
+ app_logger: Optional logger instance to use
384
+
385
+ Returns:
386
+ Decorator function
387
+ """
388
+
389
+ def decorator(func):
390
+ def wrapper(*args, **kwargs):
391
+ import time
392
+
393
+ from .request import request
394
+
395
+ # Use provided logger or global logger
396
+ req_logger = app_logger or logger
397
+
398
+ # Get request info
399
+ method = getattr(request, "method", "UNKNOWN")
400
+ path = getattr(request, "path", "/unknown")
401
+
402
+ # Start timing
403
+ start_time = time.perf_counter()
404
+
405
+ try:
406
+ # Execute the route handler
407
+ result = func(*args, **kwargs)
408
+
409
+ # Calculate duration
410
+ duration = time.perf_counter() - start_time
411
+
412
+ # Determine status code
413
+ if isinstance(result, tuple):
414
+ status_code = result[1] if len(result) > 1 else 200
415
+ else:
416
+ status_code = 200
417
+
418
+ # Log successful request
419
+ req_logger.log_request(
420
+ method=method, path=path, status_code=status_code, duration=duration
421
+ )
422
+
423
+ return result
424
+
425
+ except Exception as e:
426
+ # Calculate duration for error case
427
+ duration = time.perf_counter() - start_time
428
+
429
+ # Log error request
430
+ req_logger.log_request(
431
+ method=method,
432
+ path=path,
433
+ status_code=500,
434
+ duration=duration,
435
+ error=str(e),
436
+ )
437
+
438
+ # Re-raise the exception
439
+ raise
440
+
441
+ return wrapper
442
+
443
+ return decorator
444
+
445
+
446
+ # Example usage
447
+ if __name__ == "__main__":
448
+ # Demo the colorful logging
449
+ demo_logger = setup_logging(level="DEBUG", use_colors=True)
450
+
451
+ demo_logger.log_startup("BustAPI server starting...")
452
+ demo_logger.info("Server configuration loaded")
453
+ demo_logger.debug("Debug information")
454
+ demo_logger.warning("This is a warning")
455
+ demo_logger.error("This is an error")
456
+
457
+ # Demo HTTP request logging with different time units
458
+ demo_logger.log_request("GET", "/api/fast", 200, 0.000045) # 45μs - very fast
459
+ demo_logger.log_request("GET", "/api/quick", 200, 0.0023) # 2.3ms - quick
460
+ demo_logger.log_request("POST", "/api/users", 201, 0.123) # 123ms - ok
461
+ demo_logger.log_request("GET", "/api/medium", 200, 0.456) # 456ms - medium
462
+ demo_logger.log_request("GET", "/api/slow", 200, 1.234) # 1.234s - slow
463
+ demo_logger.log_request("GET", "/api/users/999", 404, 0.012) # 12ms - not found
464
+ demo_logger.log_request(
465
+ "POST", "/api/error", 500, 0.089, "Database connection failed"
466
+ ) # With error
467
+
468
+ demo_logger.log_shutdown("BustAPI server shutting down...")
@@ -0,0 +1,33 @@
1
+ """
2
+ BustAPI OpenAPI Module
3
+
4
+ OpenAPI 3.1.0 specification support for BustAPI.
5
+ """
6
+
7
+ from .models import (
8
+ OpenAPIInfo,
9
+ OpenAPIOperation,
10
+ OpenAPIPathItem,
11
+ OpenAPIResponse,
12
+ OpenAPIServer,
13
+ OpenAPISpec,
14
+ )
15
+ from .utils import (
16
+ create_operation_from_handler,
17
+ create_path_item_from_route,
18
+ extract_route_info,
19
+ get_openapi_spec,
20
+ )
21
+
22
+ __all__ = [
23
+ "OpenAPIInfo",
24
+ "OpenAPIServer",
25
+ "OpenAPIResponse",
26
+ "OpenAPIOperation",
27
+ "OpenAPIPathItem",
28
+ "OpenAPISpec",
29
+ "get_openapi_spec",
30
+ "create_path_item_from_route",
31
+ "create_operation_from_handler",
32
+ "extract_route_info",
33
+ ]
@@ -0,0 +1,3 @@
1
+ METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
2
+ REF_PREFIX = "#/components/schemas/"
3
+ REF_TEMPLATE = "#/components/schemas/{model}"