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.
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/PKG-INFO +1 -3
- fastlib_py-0.3.9/fastlib/_logging/config.py +71 -0
- fastlib_py-0.3.9/fastlib/_logging/handlers.py +253 -0
- fastlib_py-0.3.9/fastlib/cache/_memony_cache.py +67 -0
- fastlib_py-0.3.9/fastlib/cache/_redis_cache.py +120 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/PKG-INFO +1 -3
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/requires.txt +0 -2
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/pyproject.toml +1 -3
- fastlib_py-0.3.8/fastlib/_logging/config.py +0 -109
- fastlib_py-0.3.8/fastlib/_logging/handlers.py +0 -171
- fastlib_py-0.3.8/fastlib/cache/_memony_cache.py +0 -35
- fastlib_py-0.3.8/fastlib/cache/_redis_cache.py +0 -58
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/LICENSE +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/README.md +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/_logging/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/cache/manager.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_database_config.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_security_config.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/_server_config.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/base.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/config/utils.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/constants.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/contextvars.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/db_engine.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/enums.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/base.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/exception_handler.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/manager.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/messages.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/types.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/i18n/utils.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/logging.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/db_session.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/i18n.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/middleware/jwt.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/migrate.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/openapi.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/py.typed +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/request.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/response.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/router.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/schema.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/security.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/enums.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/event.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/handler.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/schema.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/stream/sse.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/security_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib/utils/validate_util.py +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/SOURCES.txt +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.3.8 → fastlib_py-0.3.9}/fastlib_py.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastlib-py"
|
|
3
|
-
version = "0.3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|