fastlib-py 0.3.8__tar.gz → 0.3.9__tar.gz

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 (80) hide show
  1. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/PKG-INFO +1 -3
  2. fastlib_py-0.3.9/fastlib/_logging/config.py +71 -0
  3. fastlib_py-0.3.9/fastlib/_logging/handlers.py +253 -0
  4. fastlib_py-0.3.9/fastlib/cache/_memony_cache.py +67 -0
  5. fastlib_py-0.3.9/fastlib/cache/_redis_cache.py +120 -0
  6. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/PKG-INFO +1 -3
  7. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/requires.txt +0 -2
  8. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/pyproject.toml +1 -3
  9. fastlib_py-0.3.8/fastlib/_logging/config.py +0 -109
  10. fastlib_py-0.3.8/fastlib/_logging/handlers.py +0 -171
  11. fastlib_py-0.3.8/fastlib/cache/_memony_cache.py +0 -35
  12. fastlib_py-0.3.8/fastlib/cache/_redis_cache.py +0 -58
  13. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/LICENSE +0 -0
  14. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/README.md +0 -0
  15. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/__init__.py +0 -0
  16. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/_logging/__init__.py +0 -0
  17. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/cache/__init__.py +0 -0
  18. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/cache/manager.py +0 -0
  19. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/__init__.py +0 -0
  20. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_database_config.py +0 -0
  21. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_security_config.py +0 -0
  22. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_server_config.py +0 -0
  23. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/base.py +0 -0
  24. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/loader.py +0 -0
  25. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/manager.py +0 -0
  26. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/registry.py +0 -0
  27. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/utils.py +0 -0
  28. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/constants.py +0 -0
  29. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/contextvars.py +0 -0
  30. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/db_engine.py +0 -0
  31. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/enums.py +0 -0
  32. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/__init__.py +0 -0
  33. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/base.py +0 -0
  34. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/exception_handler.py +0 -0
  35. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/exception_manager.py +0 -0
  36. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/__init__.py +0 -0
  37. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/manager.py +0 -0
  38. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/messages.py +0 -0
  39. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/types.py +0 -0
  40. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/utils.py +0 -0
  41. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/logging.py +0 -0
  42. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/__init__.py +0 -0
  43. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/base_mapper.py +0 -0
  44. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/impl/__init__.py +0 -0
  45. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
  46. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/__init__.py +0 -0
  47. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/db_session.py +0 -0
  48. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/i18n.py +0 -0
  49. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/jwt.py +0 -0
  50. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/migrate.py +0 -0
  51. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/openapi.py +0 -0
  52. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/py.typed +0 -0
  53. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/request.py +0 -0
  54. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/response.py +0 -0
  55. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/router.py +0 -0
  56. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/schema.py +0 -0
  57. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/security.py +0 -0
  58. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/__init__.py +0 -0
  59. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/base_service.py +0 -0
  60. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/impl/__init__.py +0 -0
  61. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/impl/base_service_impl.py +0 -0
  62. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/__init__.py +0 -0
  63. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/enums.py +0 -0
  64. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/event.py +0 -0
  65. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/handler.py +0 -0
  66. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/schema.py +0 -0
  67. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/sse.py +0 -0
  68. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/__init__.py +0 -0
  69. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/excel_util.py +0 -0
  70. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/file_util.py +0 -0
  71. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/http_util.py +0 -0
  72. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/ip_util.py +0 -0
  73. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/security_util.py +0 -0
  74. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/snowflake_util.py +0 -0
  75. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/str_util.py +0 -0
  76. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/validate_util.py +0 -0
  77. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/SOURCES.txt +0 -0
  78. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/dependency_links.txt +0 -0
  79. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/top_level.txt +0 -0
  80. {fastlib_py-0.3.8 → fastlib_py-0.3.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Fastlib, high performance, easy to use, fast to code, ready for production
5
5
  Author-email: tyvekZhang <tyvekzhang@gmail.com>
6
6
  Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
@@ -21,9 +21,7 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiosqlite>=0.21
23
23
  Requires-Dist: alembic>=1.15.2
24
- Requires-Dist: fakeredis>=2.32
25
24
  Requires-Dist: fastapi[standard]>=0.115.12
26
- Requires-Dist: loguru>=0.7.3
27
25
  Requires-Dist: openpyxl>=3.1.5
28
26
  Requires-Dist: pandas>=2.2.3
29
27
  Requires-Dist: pyjwt[crypto]>=2.10.1
@@ -0,0 +1,71 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Log configuration and logging utility for the application.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal, Optional
9
+ from fastlib.config.base import BaseConfig
10
+ from fastlib.config.manager import config_class
11
+ from fastlib.config.utils import ProjectInfo
12
+
13
+
14
+ LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
15
+
16
+
17
+ @config_class("log")
18
+ @dataclass
19
+ class LogConfig(BaseConfig):
20
+ """
21
+ Configuration for logging settings.
22
+ """
23
+
24
+ log_dir: str = "logs"
25
+ app_name: Optional[str] = None
26
+ log_level: LogLevel = "INFO"
27
+ log_rotation: str = "1 day"
28
+ log_retention: str = "90 days"
29
+ enable_console_log: bool = True
30
+ enable_json_logs: bool = False
31
+ error_log_rotation: str = "1 day"
32
+ error_log_retention: str = "90 days"
33
+ enable_async: bool = True
34
+ max_file_size: Optional[str] = "10 MB"
35
+ time_format: str = "%Y-%m-%d"
36
+
37
+ def __post_init__(self):
38
+ """
39
+ Post-initialization hook.
40
+ """
41
+ if not self.app_name:
42
+ project = ProjectInfo.from_pyproject()
43
+ self.app_name = getattr(project, "name", "app")
44
+
45
+ # ✅ ensure log directory exists
46
+ Path(self.log_dir).mkdir(parents=True, exist_ok=True)
47
+
48
+ @property
49
+ def default_format(self) -> str:
50
+ """Default text log format."""
51
+ return "%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s"
52
+
53
+ @property
54
+ def log_file_name(self) -> str:
55
+ """Main log filename."""
56
+ return f"{self.app_name}.log"
57
+
58
+ @property
59
+ def error_log_file_name(self) -> str:
60
+ """Error log filename."""
61
+ return f"{self.app_name}_error.log"
62
+
63
+ @property
64
+ def log_file_path(self) -> Path:
65
+ """Full path for the main log file."""
66
+ return Path(self.log_dir) / self.log_file_name
67
+
68
+ @property
69
+ def error_log_file_path(self) -> Path:
70
+ """Full path for the error log file."""
71
+ return Path(self.log_dir) / self.error_log_file_name
@@ -0,0 +1,253 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ A comprehensive logging tool for the application.
4
+ """
5
+
6
+ import sys
7
+ import logging
8
+ import logging.config
9
+ from pathlib import Path
10
+ from typing import Dict, Any, Optional
11
+ from logging.handlers import TimedRotatingFileHandler
12
+ import json
13
+ import gzip
14
+ import os
15
+ import re
16
+
17
+ from fastlib._logging.config import LogConfig
18
+ from fastlib.config.manager import ConfigManager
19
+
20
+
21
+ class JSONFormatter(logging.Formatter):
22
+ """Custom formatter for JSON logs."""
23
+
24
+ def format(self, record: logging.LogRecord) -> str:
25
+ """Format log record as JSON."""
26
+ log_data = {
27
+ "timestamp": self.formatTime(record, self.datefmt),
28
+ "level": record.levelname,
29
+ "logger": record.name,
30
+ "message": record.getMessage(),
31
+ "module": record.module,
32
+ "function": record.funcName,
33
+ "line": record.lineno,
34
+ }
35
+
36
+ # Safely merge user-provided data
37
+ extra_data = getattr(record, "extra_data", None)
38
+ if isinstance(extra_data, dict):
39
+ log_data.update(extra_data)
40
+
41
+ return json.dumps(log_data, ensure_ascii=False)
42
+
43
+
44
+ class Logger:
45
+ """
46
+ Enhanced logging tool supporting structured and JSON logs.
47
+ """
48
+
49
+ _instances: Dict[str, "Logger"] = {}
50
+ _config: Optional[LogConfig] = None
51
+ _is_configured: bool = False
52
+ _initialized_loggers: set[str] = set()
53
+
54
+ @classmethod
55
+ def initialize(cls) -> "Logger":
56
+ """Initialize the logging system."""
57
+ if not cls._is_configured:
58
+ cls._config = cls._get_config()
59
+ instance = cls.get_instance()
60
+ cls._is_configured = True
61
+ return instance
62
+ return cls.get_instance()
63
+
64
+ @classmethod
65
+ def _get_config(cls) -> LogConfig:
66
+ """Lazy load configuration."""
67
+ if cls._config is None:
68
+ cls._config = ConfigManager.get_config_instance("log")
69
+ return cls._config
70
+
71
+ @classmethod
72
+ def get_instance(cls, name: Optional[str] = None) -> "Logger":
73
+ """Get or create a named logger instance."""
74
+ if cls._config is None:
75
+ cls._config = cls._get_config()
76
+
77
+ name = name or cls._config.app_name
78
+
79
+ if name not in cls._instances:
80
+ cls._instances[name] = cls(name)
81
+ return cls._instances[name]
82
+
83
+ def __init__(self, name: str):
84
+ """Initialize the Logger."""
85
+ self.name = name
86
+ self._config = self._get_config()
87
+
88
+ # Only setup logging once globally
89
+ if not self.__class__._is_configured:
90
+ self._setup_logging()
91
+ self.__class__._is_configured = True
92
+
93
+ # Create or reuse named logger
94
+ self._logger = logging.getLogger(name)
95
+
96
+ # Avoid duplicate handler addition
97
+ if name not in self.__class__._initialized_loggers:
98
+ self.__class__._initialized_loggers.add(name)
99
+
100
+ # -----------------------------
101
+ # Setup and Handlers
102
+ # -----------------------------
103
+
104
+ def _setup_logging(self) -> None:
105
+ """Configure native logging with handlers."""
106
+ config = self._config
107
+
108
+ Path(config.log_dir).mkdir(parents=True, exist_ok=True)
109
+
110
+ root_logger = logging.getLogger()
111
+ root_logger.setLevel(getattr(logging, config.log_level, logging.INFO))
112
+
113
+ # Clear any stale handlers (important for tests)
114
+ for h in root_logger.handlers[:]:
115
+ root_logger.removeHandler(h)
116
+ h.close()
117
+
118
+ # Console handler
119
+ if config.enable_console_log:
120
+ console_handler = logging.StreamHandler(sys.stderr)
121
+ console_handler.setLevel(getattr(logging, config.log_level))
122
+ console_handler.setFormatter(self._get_formatter())
123
+ root_logger.addHandler(console_handler)
124
+
125
+ # Main rotating file handler
126
+ file_handler = self._create_file_handler(
127
+ str(config.log_file_path),
128
+ config.log_level,
129
+ config.log_rotation,
130
+ config.log_retention,
131
+ )
132
+ root_logger.addHandler(file_handler)
133
+
134
+ # Error log handler
135
+ if config.log_level not in ["ERROR", "CRITICAL"]:
136
+ error_handler = self._create_file_handler(
137
+ str(config.error_log_file_path),
138
+ "ERROR",
139
+ config.error_log_rotation,
140
+ config.error_log_retention,
141
+ )
142
+ root_logger.addHandler(error_handler)
143
+
144
+ def _create_file_handler(
145
+ self, filepath: str, level: str, rotation: str, retention: str
146
+ ) -> logging.Handler:
147
+ """Create a TimedRotatingFileHandler with compression."""
148
+
149
+ when, interval = self._parse_rotation(rotation)
150
+
151
+ handler = TimedRotatingFileHandler(
152
+ filepath,
153
+ when=when,
154
+ interval=interval,
155
+ backupCount=self._parse_retention(retention),
156
+ encoding="utf-8",
157
+ utc=False,
158
+ )
159
+
160
+ handler.setLevel(getattr(logging, level, logging.INFO))
161
+ handler.setFormatter(self._get_formatter())
162
+
163
+ # Patch doRollover to auto-compress old logs
164
+ original_doRollover = handler.doRollover
165
+
166
+ def compressed_doRollover(*args, **kwargs):
167
+ original_doRollover(*args, **kwargs)
168
+ try:
169
+ self._compress_old_logs(filepath)
170
+ except Exception:
171
+ # Prevent logging failure on compression errors
172
+ logging.getLogger(__name__).warning("Log compression failed", exc_info=True)
173
+
174
+ handler.doRollover = compressed_doRollover
175
+ return handler
176
+
177
+ def _parse_rotation(self, rotation: Optional[str]) -> tuple[str, int]:
178
+ """Parse rotation string like '1 day' or '6 hours'."""
179
+ if not rotation:
180
+ return "midnight", 1
181
+ rotation = rotation.lower()
182
+ num = int(re.findall(r"\d+", rotation)[0]) if re.search(r"\d+", rotation) else 1
183
+ if "hour" in rotation:
184
+ return "h", num
185
+ if "minute" in rotation:
186
+ return "m", num
187
+ return "midnight", num
188
+
189
+ def _parse_retention(self, retention: Optional[str]) -> int:
190
+ """Parse retention like '7 days'."""
191
+ if not retention:
192
+ return 7
193
+ if isinstance(retention, (int, float)):
194
+ return int(retention)
195
+ nums = re.findall(r"\d+", str(retention))
196
+ return int(nums[0]) if nums else 7
197
+
198
+ def _compress_old_logs(self, base_path: str) -> None:
199
+ """Compress all rotated logs except current."""
200
+ log_dir = os.path.dirname(base_path)
201
+ base_name = os.path.basename(base_path)
202
+
203
+ for f in os.listdir(log_dir):
204
+ if f.startswith(base_name) and not f.endswith(".gz"):
205
+ full_path = os.path.join(log_dir, f)
206
+ if f != base_name: # Skip active file
207
+ gz_path = f"{full_path}.gz"
208
+ if not os.path.exists(gz_path): # avoid double compress
209
+ with open(full_path, "rb") as f_in, gzip.open(gz_path, "wb") as f_out:
210
+ f_out.writelines(f_in)
211
+ os.remove(full_path)
212
+
213
+ def _get_formatter(self) -> logging.Formatter:
214
+ """Return JSON or plain formatter."""
215
+ if self._config.enable_json_logs:
216
+ return JSONFormatter()
217
+ return logging.Formatter(self._config.default_format)
218
+
219
+ # -----------------------------
220
+ # Logging Methods
221
+ # -----------------------------
222
+
223
+ def _log_with_extra(self, level: int, message: str, **kwargs) -> None:
224
+ """Log message with structured context."""
225
+ if not self._logger.isEnabledFor(level):
226
+ return
227
+
228
+ # Prevent passing non-serializable extra
229
+ safe_extra = {"extra_data": kwargs}
230
+ self._logger.log(level, message, extra=safe_extra)
231
+
232
+ def debug(self, message: str, **kwargs): self._log_with_extra(logging.DEBUG, message, **kwargs)
233
+ def info(self, message: str, **kwargs): self._log_with_extra(logging.INFO, message, **kwargs)
234
+ def warning(self, message: str, **kwargs): self._log_with_extra(logging.WARNING, message, **kwargs)
235
+ def error(self, message: str, **kwargs): self._log_with_extra(logging.ERROR, message, **kwargs)
236
+ def critical(self, message: str, **kwargs): self._log_with_extra(logging.CRITICAL, message, **kwargs)
237
+
238
+ def exception(self, message: str, **kwargs):
239
+ """Log exception with traceback."""
240
+ self._logger.exception(message, extra={"extra_data": kwargs})
241
+
242
+ @classmethod
243
+ def reset(cls) -> None:
244
+ """Reset all logger instances (for testing)."""
245
+ root_logger = logging.getLogger()
246
+ for h in root_logger.handlers[:]:
247
+ root_logger.removeHandler(h)
248
+ h.close()
249
+
250
+ cls._instances.clear()
251
+ cls._config = None
252
+ cls._is_configured = False
253
+ cls._initialized_loggers.clear()
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Memory (MemoryStorage) Cache Manager for in-memory caching"""
3
+
4
+ import asyncio
5
+
6
+
7
+ class MemoryStorage:
8
+ """In-memory storage implementation"""
9
+
10
+ def __init__(self):
11
+ self._data = {}
12
+
13
+ async def set(self, key, value):
14
+ self._data[key] = value
15
+ return key
16
+
17
+ async def get(self, key):
18
+ return self._data.get(key)
19
+
20
+ async def update(self, key, **updated):
21
+ if key not in self._data:
22
+ return None
23
+
24
+ value = self._data[key]
25
+ for key, value in updated.items():
26
+ if hasattr(value, key):
27
+ setattr(value, key, value)
28
+
29
+ return value
30
+
31
+ async def delete(self, key):
32
+ if key in self._data:
33
+ del self._data[key]
34
+ return True
35
+ return False
36
+
37
+ async def list(self):
38
+ return list(self._data.values())
39
+
40
+
41
+ class MemoryCacheManager:
42
+ """Singleton manager for in-memory (MemoryStorage) cache client."""
43
+
44
+ _instance = None
45
+ _lock = asyncio.Lock()
46
+
47
+ @classmethod
48
+ async def get_instance(cls):
49
+ """Get a singleton MemoryStorage instance."""
50
+ if cls._instance is None:
51
+ async with cls._lock:
52
+ if cls._instance is None:
53
+ cls._instance = MemoryStorage()
54
+ return cls._instance
55
+
56
+ @classmethod
57
+ async def close(cls):
58
+ """Close the MemoryStorage instance (for cleanup in tests)."""
59
+ if cls._instance is not None:
60
+ await cls._instance.close()
61
+ cls._instance = None
62
+
63
+ @classmethod
64
+ async def reset(cls):
65
+ """Clear all cached data without destroying the instance."""
66
+ if cls._instance is not None:
67
+ await cls._instance.flushall()
@@ -0,0 +1,120 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Redis cache implementation"""
3
+
4
+ import asyncio
5
+ from typing import Optional
6
+
7
+ from loguru import logger
8
+
9
+ import json
10
+ from datetime import datetime, date
11
+ from typing import Any, Type
12
+ from redis._parsers.encoders import Encoder
13
+ from pydantic import BaseModel
14
+
15
+ from fastlib import ConfigManager
16
+
17
+ try:
18
+ import redis.asyncio as redis
19
+ except ModuleNotFoundError:
20
+ logger.warning(
21
+ "Cannot find redis module, please install it via `uv add redis[hiredis]`"
22
+ )
23
+ redis = None
24
+ RedisError = Exception
25
+ except Exception:
26
+ logger.error("Error importing redis module")
27
+ redis = None
28
+ RedisError = Exception
29
+
30
+
31
+ class CustomJsonEncoder(Encoder):
32
+ def encode(self, value: Any):
33
+ """Extended encoding to support datetime and Pydantic models."""
34
+ if isinstance(value, (datetime, date)):
35
+ return json.dumps(
36
+ {
37
+ "__type__": "datetime",
38
+ "value": value.isoformat(),
39
+ }
40
+ ).encode("utf-8")
41
+
42
+ elif isinstance(value, BaseModel):
43
+ return json.dumps(
44
+ {
45
+ "__type__": "pydantic",
46
+ "model": value.__class__.__name__,
47
+ "module": value.__class__.__module__,
48
+ "data": value.model_dump(),
49
+ }
50
+ ).encode("utf-8")
51
+
52
+ elif isinstance(value, dict):
53
+ return json.dumps(value).encode("utf-8")
54
+
55
+ return super().encode(value)
56
+
57
+ def decode(self, value, force=False):
58
+ """Extended decoding to restore datetime and Pydantic models."""
59
+ if self.decode_responses or force:
60
+ if isinstance(value, bytes):
61
+ value = value.decode(self.encoding, self.encoding_errors)
62
+ try:
63
+ data = json.loads(value)
64
+ if isinstance(data, dict) and "__type__" in data:
65
+ if data["__type__"] == "datetime":
66
+ return datetime.fromisoformat(data["value"])
67
+
68
+ elif data["__type__"] == "pydantic":
69
+ module_name = data.get("module")
70
+ model_name = data.get("model")
71
+ payload = data.get("data")
72
+
73
+ # Dynamically import model class
74
+ module = __import__(module_name, fromlist=[model_name])
75
+ model_cls: Type[BaseModel] = getattr(module, model_name)
76
+ return model_cls(**payload)
77
+
78
+ return data
79
+ except json.JSONDecodeError:
80
+ raise
81
+ return value
82
+
83
+
84
+ class RedisCacheManager:
85
+ _instance: Optional["redis.Redis"] = None
86
+ _connection_pool: Optional["redis.ConnectionPool"] = None
87
+ _lock = asyncio.Lock()
88
+
89
+ @classmethod
90
+ async def get_instance(cls) -> "redis.Redis":
91
+ """
92
+ Get redis instance
93
+ """
94
+ if redis is None:
95
+ raise RuntimeError(
96
+ "Redis module is not available. Please install it via 'uv add redis[hiredis]'"
97
+ )
98
+
99
+ if cls._instance is None:
100
+ async with cls._lock:
101
+ if cls._connection_pool is None:
102
+ database_config = ConfigManager.get_database_config()
103
+ cls._connection_pool = redis.ConnectionPool.from_url(
104
+ f"redis://:{database_config.cache_pass}@{database_config.cache_host}:{database_config.cache_port}/{database_config.db_num}",
105
+ encoder_class=CustomJsonEncoder,
106
+ encoding="utf-8",
107
+ encoding_errors="strict",
108
+ decode_responses=True,
109
+ )
110
+ if cls._instance is None:
111
+ cls._instance = redis.Redis(connection_pool=cls._connection_pool)
112
+ return cls._instance
113
+
114
+ @classmethod
115
+ async def close_pool(cls):
116
+ """Close the connection pool."""
117
+ if cls._connection_pool:
118
+ await cls._connection_pool.disconnect()
119
+ cls._connection_pool = None
120
+ cls._instance = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Fastlib, high performance, easy to use, fast to code, ready for production
5
5
  Author-email: tyvekZhang <tyvekzhang@gmail.com>
6
6
  Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
@@ -21,9 +21,7 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiosqlite>=0.21
23
23
  Requires-Dist: alembic>=1.15.2
24
- Requires-Dist: fakeredis>=2.32
25
24
  Requires-Dist: fastapi[standard]>=0.115.12
26
- Requires-Dist: loguru>=0.7.3
27
25
  Requires-Dist: openpyxl>=3.1.5
28
26
  Requires-Dist: pandas>=2.2.3
29
27
  Requires-Dist: pyjwt[crypto]>=2.10.1
@@ -1,8 +1,6 @@
1
1
  aiosqlite>=0.21
2
2
  alembic>=1.15.2
3
- fakeredis>=2.32
4
3
  fastapi[standard]>=0.115.12
5
- loguru>=0.7.3
6
4
  openpyxl>=3.1.5
7
5
  pandas>=2.2.3
8
6
  pyjwt[crypto]>=2.10.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastlib-py"
3
- version = "0.3.8"
3
+ version = "0.3.9"
4
4
  description = "Fastlib, high performance, easy to use, fast to code, ready for production"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -31,9 +31,7 @@ classifiers = [
31
31
  dependencies = [
32
32
  "aiosqlite>=0.21",
33
33
  "alembic>=1.15.2",
34
- "fakeredis>=2.32",
35
34
  "fastapi[standard]>=0.115.12",
36
- "loguru>=0.7.3",
37
35
  "openpyxl>=3.1.5",
38
36
  "pandas>=2.2.3",
39
37
  "pyjwt[crypto]>=2.10.1",
@@ -1,109 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """
3
- Log configuration and logging utility for the application.
4
- """
5
-
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
- from typing import Literal
9
-
10
- from fastlib.config.base import BaseConfig
11
- from fastlib.config.manager import config_class
12
- from fastlib.config.utils import ProjectInfo
13
-
14
- LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
15
-
16
-
17
- @config_class("log")
18
- @dataclass
19
- class LogConfig(BaseConfig):
20
- """
21
- Configuration for logging settings.
22
-
23
- Attributes:
24
- log_dir (str): Directory where log files will be stored.
25
- app_name (str): Name of the application for log file naming.
26
- log_level (LogLevel): Minimum log level to record. Possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL.
27
- log_rotation (str): Frequency of log file rotation. Default is "1 day".
28
- log_retention (str): How long to keep log files. Default is "90 days".
29
- enable_console_log (bool): Whether to output logs to console. Default is True.
30
- enable_json_logs (bool): Whether to use JSON format for logs. Default is False.
31
- error_log_rotation (str): Frequency of error log rotation. Default is "1 day".
32
- error_log_retention (str): How long to keep error log files. Default is "90 days".
33
- enable_async (bool): Whether to use asynchronous logging. Default is True.
34
- max_file_size (Optional[str]): Maximum size for log files before rotation. Default is "10 MB".
35
- time_format (str): Format string for timestamps in log file names. Default is "YYYY-MM-DD".
36
- """
37
-
38
- log_dir: str = "/var/log"
39
- app_name: str | None = None
40
- log_level: LogLevel = "INFO"
41
- log_rotation: str = "1 day"
42
- log_retention: str = "90 days"
43
- enable_console_log: bool = True
44
- enable_json_logs: bool = False
45
- error_log_rotation: str = "1 day"
46
- error_log_retention: str = "90 days"
47
- enable_async: bool = True
48
- max_file_size: str | None = "10 MB"
49
- time_format: str = "YYYY-MM-DD"
50
-
51
- def __post_init__(self):
52
- """
53
- Set default app_name if not provided.
54
-
55
- If `app_name` is not provided, it will be set to the name of the project
56
- retrieved from the `pyproject.toml` file.
57
- """
58
- if self.app_name is None:
59
- self.app_name = ProjectInfo.from_pyproject().name
60
-
61
- @property
62
- def default_format(self) -> str:
63
- """
64
- Default log format string.
65
-
66
- Returns:
67
- str: The default format used for log messages.
68
- """
69
- return "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}"
70
-
71
- @property
72
- def log_file_pattern(self) -> str:
73
- """
74
- Returns the main log file pattern with timestamp.
75
-
76
- Returns:
77
- str: The pattern used to name the main log file.
78
- """
79
- return f"{{time:{self.time_format}}}_{self.app_name}.log"
80
-
81
- @property
82
- def error_log_file_pattern(self) -> str:
83
- """
84
- Returns the error log file pattern with timestamp.
85
-
86
- Returns:
87
- str: The pattern used to name the error log file.
88
- """
89
- return f"{{time:{self.time_format}}}_error_{self.app_name}.log"
90
-
91
- @property
92
- def log_file_path(self) -> Path:
93
- """
94
- Returns the full path for the main log file.
95
-
96
- Returns:
97
- Path: The full path to the main log file.
98
- """
99
- return Path(self.log_dir) / self.log_file_pattern
100
-
101
- @property
102
- def error_log_file_path(self) -> Path:
103
- """
104
- Returns the full path for the error log file.
105
-
106
- Returns:
107
- Path: The full path to the error log file.
108
- """
109
- return Path(self.log_dir) / self.error_log_file_pattern
@@ -1,171 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """
3
- A comprehensive logging tool for the application.
4
- """
5
-
6
- import sys
7
- from pathlib import Path
8
-
9
- from loguru import logger
10
-
11
- from fastlib._logging.config import LogConfig
12
- from fastlib.config.manager import ConfigManager
13
-
14
-
15
- class Logger:
16
- """
17
- Enhanced logging tool support structured logging.
18
- """
19
-
20
- _instances: dict[str, "Logger"] = {}
21
- _config: LogConfig | None = None
22
- _is_configured: bool = False
23
-
24
- @classmethod
25
- def initialize(cls) -> "Logger":
26
- """
27
- Initialize the logging system.
28
-
29
- Returns:
30
- Logger instance
31
- """
32
- if not cls._is_configured:
33
- # Load configuration first
34
- cls._config = cls._get_config()
35
- # Create and configure the logger
36
- instance = cls.get_instance()
37
- cls._is_configured = True
38
- return instance
39
- return cls.get_instance()
40
-
41
- @classmethod
42
- def _get_config(cls) -> LogConfig:
43
- """Lazy load configuration."""
44
- if cls._config is None:
45
- cls._config = ConfigManager.get_config_instance("log")
46
- return cls._config
47
-
48
- @classmethod
49
- def get_instance(
50
- cls,
51
- name: str | None = None,
52
- ) -> "Logger":
53
- """
54
- Get or create a named logger instance.
55
-
56
- Args:
57
- name: Logger instance name
58
-
59
- Returns:
60
- Logger instance
61
- """
62
- # Ensure config is loaded
63
- if cls._config is None:
64
- cls._config = cls._get_config()
65
-
66
- if not name:
67
- name = cls._config.app_name
68
-
69
- if name not in cls._instances:
70
- cls._instances[name] = cls(name)
71
- return cls._instances[name]
72
-
73
- def __init__(self, name: str):
74
- """Initialize the Logger with optional overrides."""
75
- self.name = name
76
-
77
- # Get configuration
78
- self._config = self._get_config()
79
-
80
- # Only setup logging once globally
81
- if not self.__class__._is_configured:
82
- self._setup_logging()
83
-
84
- def _setup_logging(self) -> None:
85
- """Configure loguru logger with handlers based on configuration."""
86
- # Remove default handler
87
- logger.remove()
88
-
89
- config = self._config
90
-
91
- # Create log directory
92
- Path(config.log_dir).mkdir(parents=True, exist_ok=True)
93
-
94
- # Console handler
95
- if config.enable_console_log:
96
- logger.add(
97
- sys.stderr,
98
- format=self._get_format_string(),
99
- level=config.log_level,
100
- colorize=True,
101
- )
102
-
103
- # Main file handler
104
- rotation = config.max_file_size or config.log_rotation
105
-
106
- logger.add(
107
- str(config.log_file_path),
108
- format=self._get_format_string(),
109
- level=config.log_level,
110
- rotation=rotation,
111
- retention=config.log_retention,
112
- compression="zip",
113
- serialize=config.enable_json_logs,
114
- enqueue=config.enable_async,
115
- )
116
-
117
- # Error log handler - only add if log level is not ERROR or CRITICAL
118
- if config.log_level not in ["ERROR", "CRITICAL"]:
119
- logger.add(
120
- str(config.error_log_file_path),
121
- format=self._get_format_string(),
122
- level="ERROR",
123
- rotation=config.error_log_rotation,
124
- retention=config.error_log_retention,
125
- compression="zip",
126
- serialize=config.enable_json_logs,
127
- enqueue=config.enable_async,
128
- )
129
-
130
- def _get_format_string(self) -> str:
131
- """Get the log format string."""
132
- config = self._config
133
-
134
- if config.enable_json_logs:
135
- # JSON logs don't need format string
136
- return "{message}"
137
-
138
- return config.default_format
139
-
140
- # Convenience methods for logging
141
- def debug(self, message: str, **kwargs) -> None:
142
- """Log debug message."""
143
- logger.bind(logger_name=self.name).debug(message, **kwargs)
144
-
145
- def info(self, message: str, **kwargs) -> None:
146
- """Log info message."""
147
- logger.bind(logger_name=self.name).info(message, **kwargs)
148
-
149
- def warning(self, message: str, **kwargs) -> None:
150
- """Log warning message."""
151
- logger.bind(logger_name=self.name).warning(message, **kwargs)
152
-
153
- def error(self, message: str, **kwargs) -> None:
154
- """Log error message."""
155
- logger.bind(logger_name=self.name).error(message, **kwargs)
156
-
157
- def critical(self, message: str, **kwargs) -> None:
158
- """Log critical message."""
159
- logger.bind(logger_name=self.name).critical(message, **kwargs)
160
-
161
- def exception(self, message: str, **kwargs) -> None:
162
- """Log exception with traceback."""
163
- logger.bind(logger_name=self.name).exception(message, **kwargs)
164
-
165
- @classmethod
166
- def reset(cls) -> None:
167
- """Reset all logger instances and configuration (useful for testing)."""
168
- cls._instances.clear()
169
- cls._config = None
170
- cls._is_configured = False
171
- logger.remove()
@@ -1,35 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Memory (FakeRedis) Cache Manager for in-memory caching"""
3
-
4
- import asyncio
5
-
6
- from fakeredis.aioredis import FakeRedis
7
-
8
-
9
- class MemoryCacheManager:
10
- """Singleton manager for in-memory (FakeRedis) cache client."""
11
-
12
- _instance: FakeRedis | None = None
13
- _lock = asyncio.Lock()
14
-
15
- @classmethod
16
- async def get_instance(cls) -> FakeRedis:
17
- """Get a singleton FakeRedis instance."""
18
- if cls._instance is None:
19
- async with cls._lock:
20
- if cls._instance is None:
21
- cls._instance = FakeRedis()
22
- return cls._instance
23
-
24
- @classmethod
25
- async def close(cls):
26
- """Close the FakeRedis instance (for cleanup in tests)."""
27
- if cls._instance is not None:
28
- await cls._instance.close()
29
- cls._instance = None
30
-
31
- @classmethod
32
- async def reset(cls):
33
- """Clear all cached data without destroying the instance."""
34
- if cls._instance is not None:
35
- await cls._instance.flushall()
@@ -1,58 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Redis cache implementation"""
3
-
4
- import asyncio
5
- from typing import Optional
6
-
7
- from loguru import logger
8
-
9
- from fastlib import ConfigManager
10
-
11
- try:
12
- import redis.asyncio as redis
13
- except ModuleNotFoundError:
14
- logger.warning(
15
- "Cannot find redis module, please install it via `uv add redis[hiredis]`"
16
- )
17
- redis = None
18
- RedisError = Exception
19
- except Exception:
20
- logger.error("Error importing redis module")
21
- redis = None
22
- RedisError = Exception
23
-
24
-
25
- class RedisCacheManager:
26
- _instance: Optional["redis.Redis"] = None
27
- _connection_pool: Optional["redis.ConnectionPool"] = None
28
- _lock = asyncio.Lock()
29
-
30
- @classmethod
31
- async def get_instance(cls) -> "redis.Redis":
32
- """
33
- Get redis instance
34
- """
35
- if redis is None:
36
- raise RuntimeError(
37
- "Redis module is not available. Please install it via 'uv add redis[hiredis]'"
38
- )
39
-
40
- if cls._instance is None:
41
- async with cls._lock:
42
- if cls._connection_pool is None:
43
- database_config = ConfigManager.get_database_config()
44
- cls._connection_pool = redis.ConnectionPool.from_url(
45
- f"redis://:{database_config.cache_pass}@{database_config.cache_host}:{database_config.cache_port}/{database_config.db_num}",
46
- decode_responses=True,
47
- )
48
- if cls._instance is None:
49
- cls._instance = redis.Redis(connection_pool=cls._connection_pool)
50
- return cls._instance
51
-
52
- @classmethod
53
- async def close_pool(cls):
54
- """Close the connection pool."""
55
- if cls._connection_pool:
56
- await cls._connection_pool.disconnect()
57
- cls._connection_pool = None
58
- cls._instance = None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes