fastlib-py 0.3.4__tar.gz → 0.3.6__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.4 → fastlib_py-0.3.6}/PKG-INFO +2 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/_logging/config.py +2 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/_logging/handlers.py +3 -4
- fastlib_py-0.3.6/fastlib/cache/_memony_cache.py +267 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/cache/_redis_cache.py +5 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/cache/base.py +7 -7
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/cache/manager.py +3 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/loader.py +3 -3
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/manager.py +7 -7
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/registry.py +3 -3
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/utils.py +3 -3
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/contextvars.py +2 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/exception/base.py +5 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/exception/exception_handler.py +2 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/i18n/manager.py +2 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/i18n/messages.py +2 -6
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/i18n/types.py +0 -20
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/i18n/utils.py +3 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/logging.py +2 -1
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/mapper/base_mapper.py +15 -15
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/mapper/impl/base_mapper_impl.py +46 -48
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/middleware/db_session.py +4 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/migrate.py +3 -4
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/request.py +1 -3
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/response.py +6 -6
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/security/security.py +5 -4
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/security/utils.py +17 -22
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/service/base_service.py +35 -34
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/service/impl/base_service_impl.py +38 -36
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/event.py +9 -9
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/handler.py +12 -6
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/schema.py +39 -34
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/sse.py +27 -25
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/excel_util.py +3 -4
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/file_util.py +5 -9
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/http_util.py +8 -8
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/snowflake_util.py +1 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/validate_util.py +7 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib_py.egg-info/PKG-INFO +2 -5
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib_py.egg-info/SOURCES.txt +1 -1
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib_py.egg-info/requires.txt +0 -2
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/pyproject.toml +2 -5
- fastlib_py-0.3.4/fastlib/cache/_disk_cache.py +0 -219
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/LICENSE +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/README.md +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/_logging/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/_database_config.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/_security_config.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/_server_config.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/config/base.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/constants.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/db_engine.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/enums.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/i18n/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/middleware/i18n.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/middleware/jwt.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/openapi.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/py.typed +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/router.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/schema.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/security/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/stream/enums.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.3.4 → fastlib_py-0.3.6}/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.6
|
|
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>
|
|
@@ -10,21 +10,18 @@ Keywords: fastapi,library,sqlmodel,web
|
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.10
|
|
21
20
|
Description-Content-Type: text/markdown
|
|
22
21
|
License-File: LICENSE
|
|
23
22
|
Requires-Dist: aiosqlite>=0.21
|
|
24
23
|
Requires-Dist: alembic>=1.15.2
|
|
25
|
-
Requires-Dist: asgi-correlation-id>=4.3.4
|
|
26
24
|
Requires-Dist: cryptography>=45.0.6
|
|
27
|
-
Requires-Dist: diskcache>=5.6.3
|
|
28
25
|
Requires-Dist: fastapi[standard]>=0.115.12
|
|
29
26
|
Requires-Dist: loguru>=0.7.3
|
|
30
27
|
Requires-Dist: openpyxl>=3.1.5
|
|
@@ -4,7 +4,7 @@ Log configuration and logging utility for the application.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Literal
|
|
8
8
|
|
|
9
9
|
from fastlib.config.base import BaseConfig
|
|
10
10
|
from fastlib.config.manager import config_class
|
|
@@ -29,7 +29,7 @@ class LogConfig(BaseConfig):
|
|
|
29
29
|
error_log_rotation: str = "1 day",
|
|
30
30
|
error_log_retention: str = "90 days",
|
|
31
31
|
enable_async: bool = True,
|
|
32
|
-
max_file_size:
|
|
32
|
+
max_file_size: str | None = "10 MB",
|
|
33
33
|
time_format: str = "YYYY-MM-DD",
|
|
34
34
|
**kwargs,
|
|
35
35
|
):
|
|
@@ -5,12 +5,11 @@ A comprehensive logging tool for the application.
|
|
|
5
5
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
9
8
|
|
|
10
9
|
from loguru import logger
|
|
11
10
|
|
|
12
|
-
from fastlib.config.manager import ConfigManager
|
|
13
11
|
from fastlib._logging.config import LogConfig
|
|
12
|
+
from fastlib.config.manager import ConfigManager
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class Logger:
|
|
@@ -19,7 +18,7 @@ class Logger:
|
|
|
19
18
|
"""
|
|
20
19
|
|
|
21
20
|
_instances: dict[str, "Logger"] = {}
|
|
22
|
-
_config:
|
|
21
|
+
_config: LogConfig | None = None
|
|
23
22
|
_is_configured: bool = False
|
|
24
23
|
|
|
25
24
|
@classmethod
|
|
@@ -49,7 +48,7 @@ class Logger:
|
|
|
49
48
|
@classmethod
|
|
50
49
|
def get_instance(
|
|
51
50
|
cls,
|
|
52
|
-
name:
|
|
51
|
+
name: str | None = None,
|
|
53
52
|
) -> "Logger":
|
|
54
53
|
"""
|
|
55
54
|
Get or create a named logger instance.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Memory-based cache implementation compatible with Cache interface."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import Any, Dict, Optional, Set
|
|
8
|
+
|
|
9
|
+
from fastlib.cache.base import Cache, CacheError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryCache(Cache):
|
|
13
|
+
"""
|
|
14
|
+
Memory-based cache implementation
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, namespace: str = "default"):
|
|
18
|
+
self.namespace = namespace
|
|
19
|
+
self._data: Dict[str, Any] = {}
|
|
20
|
+
self._expiry: Dict[str, float] = {}
|
|
21
|
+
self._lists: Dict[str, list] = {}
|
|
22
|
+
self._sets: Dict[str, Set] = {}
|
|
23
|
+
self._hashes: Dict[str, Dict[str, Any]] = {}
|
|
24
|
+
self._locks: Dict[str, asyncio.Lock] = {}
|
|
25
|
+
self._global_lock = asyncio.Lock()
|
|
26
|
+
|
|
27
|
+
def _is_expired(self, key: str) -> bool:
|
|
28
|
+
"""Check if a key has expired"""
|
|
29
|
+
if key not in self._expiry:
|
|
30
|
+
return False
|
|
31
|
+
return time.time() > self._expiry[key]
|
|
32
|
+
|
|
33
|
+
def _clean_expired(self):
|
|
34
|
+
"""Clean up expired keys"""
|
|
35
|
+
expired_keys = [k for k in self._expiry if self._is_expired(k)]
|
|
36
|
+
for key in expired_keys:
|
|
37
|
+
self._data.pop(key, None)
|
|
38
|
+
self._expiry.pop(key, None)
|
|
39
|
+
self._lists.pop(key, None)
|
|
40
|
+
self._sets.pop(key, None)
|
|
41
|
+
self._hashes.pop(key, None)
|
|
42
|
+
|
|
43
|
+
async def ping(self) -> bool:
|
|
44
|
+
try:
|
|
45
|
+
await self.set("__ping__", "pong", ex=1)
|
|
46
|
+
return True
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise CacheError("Ping failed", e) from e
|
|
49
|
+
|
|
50
|
+
async def set(
|
|
51
|
+
self,
|
|
52
|
+
key: str,
|
|
53
|
+
value: Any,
|
|
54
|
+
ex: Optional[int] = None,
|
|
55
|
+
nx: bool = False, # only set if not exists
|
|
56
|
+
xx: bool = False, # only set if exists
|
|
57
|
+
) -> bool:
|
|
58
|
+
async with self._global_lock:
|
|
59
|
+
self._clean_expired()
|
|
60
|
+
|
|
61
|
+
if nx and key in self._data:
|
|
62
|
+
return False
|
|
63
|
+
if xx and key not in self._data:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
self._data[key] = value
|
|
67
|
+
if ex is not None:
|
|
68
|
+
self._expiry[key] = time.time() + ex
|
|
69
|
+
elif key in self._expiry:
|
|
70
|
+
del self._expiry[key] # Remove expiration time
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
async def get(self, key: str) -> Any | None:
|
|
75
|
+
async with self._global_lock:
|
|
76
|
+
self._clean_expired()
|
|
77
|
+
|
|
78
|
+
if key in self._data and not self._is_expired(key):
|
|
79
|
+
return self._data[key]
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
async def delete(self, *keys: str) -> int:
|
|
83
|
+
async with self._global_lock:
|
|
84
|
+
count = 0
|
|
85
|
+
for key in keys:
|
|
86
|
+
if key in self._data:
|
|
87
|
+
del self._data[key]
|
|
88
|
+
self._expiry.pop(key, None)
|
|
89
|
+
self._lists.pop(key, None)
|
|
90
|
+
self._sets.pop(key, None)
|
|
91
|
+
self._hashes.pop(key, None)
|
|
92
|
+
count += 1
|
|
93
|
+
return count
|
|
94
|
+
|
|
95
|
+
async def exists(self, *keys: str) -> int:
|
|
96
|
+
async with self._global_lock:
|
|
97
|
+
self._clean_expired()
|
|
98
|
+
return sum(1 for k in keys if k in self._data and not self._is_expired(k))
|
|
99
|
+
|
|
100
|
+
async def expire(self, key: str, seconds: int) -> bool:
|
|
101
|
+
async with self._global_lock:
|
|
102
|
+
if key not in self._data or self._is_expired(key):
|
|
103
|
+
return False
|
|
104
|
+
self._expiry[key] = time.time() + seconds
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
async def ttl(self, key: str) -> int:
|
|
108
|
+
async with self._global_lock:
|
|
109
|
+
if key not in self._data or key not in self._expiry:
|
|
110
|
+
return -1 # No expiration time set
|
|
111
|
+
if self._is_expired(key):
|
|
112
|
+
return -2 # Key does not exist (expired)
|
|
113
|
+
return int(self._expiry[key] - time.time())
|
|
114
|
+
|
|
115
|
+
async def incr(self, key: str, amount: int = 1) -> int:
|
|
116
|
+
async with self._global_lock:
|
|
117
|
+
self._clean_expired()
|
|
118
|
+
|
|
119
|
+
current = self._data.get(key, 0)
|
|
120
|
+
if not isinstance(current, int):
|
|
121
|
+
raise CacheError("Value is not integer")
|
|
122
|
+
|
|
123
|
+
new_value = current + amount
|
|
124
|
+
self._data[key] = new_value
|
|
125
|
+
return new_value
|
|
126
|
+
|
|
127
|
+
async def decr(self, key: str, amount: int = 1) -> int:
|
|
128
|
+
return await self.incr(key, -amount)
|
|
129
|
+
|
|
130
|
+
# ---------------- Hash (dict) ----------------
|
|
131
|
+
async def hset(self, name: str, key: str, value: Any) -> int:
|
|
132
|
+
async with self._global_lock:
|
|
133
|
+
if name not in self._hashes:
|
|
134
|
+
self._hashes[name] = {}
|
|
135
|
+
self._hashes[name][key] = value
|
|
136
|
+
return 1
|
|
137
|
+
|
|
138
|
+
async def hget(self, name: str, key: str) -> Any | None:
|
|
139
|
+
async with self._global_lock:
|
|
140
|
+
if name in self._hashes:
|
|
141
|
+
return self._hashes[name].get(key)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
async def hgetall(self, name: str) -> dict[str, Any]:
|
|
145
|
+
async with self._global_lock:
|
|
146
|
+
return self._hashes.get(name, {}).copy()
|
|
147
|
+
|
|
148
|
+
async def hdel(self, name: str, *keys: str) -> int:
|
|
149
|
+
async with self._global_lock:
|
|
150
|
+
if name not in self._hashes:
|
|
151
|
+
return 0
|
|
152
|
+
removed = 0
|
|
153
|
+
for k in keys:
|
|
154
|
+
if k in self._hashes[name]:
|
|
155
|
+
del self._hashes[name][k]
|
|
156
|
+
removed += 1
|
|
157
|
+
return removed
|
|
158
|
+
|
|
159
|
+
# ---------------- List ----------------
|
|
160
|
+
async def lpush(self, name: str, *values: Any) -> int:
|
|
161
|
+
async with self._global_lock:
|
|
162
|
+
if name not in self._lists:
|
|
163
|
+
self._lists[name] = []
|
|
164
|
+
self._lists[name][:0] = values # Insert at the beginning
|
|
165
|
+
return len(self._lists[name])
|
|
166
|
+
|
|
167
|
+
async def rpush(self, name: str, *values: Any) -> int:
|
|
168
|
+
async with self._global_lock:
|
|
169
|
+
if name not in self._lists:
|
|
170
|
+
self._lists[name] = []
|
|
171
|
+
self._lists[name].extend(values)
|
|
172
|
+
return len(self._lists[name])
|
|
173
|
+
|
|
174
|
+
async def lpop(self, name: str) -> Any | None:
|
|
175
|
+
async with self._global_lock:
|
|
176
|
+
if name not in self._lists or not self._lists[name]:
|
|
177
|
+
return None
|
|
178
|
+
return self._lists[name].pop(0)
|
|
179
|
+
|
|
180
|
+
async def rpop(self, name: str) -> Any | None:
|
|
181
|
+
async with self._global_lock:
|
|
182
|
+
if name not in self._lists or not self._lists[name]:
|
|
183
|
+
return None
|
|
184
|
+
return self._lists[name].pop()
|
|
185
|
+
|
|
186
|
+
async def lrange(self, name: str, start: int = 0, end: int = -1) -> list[Any]:
|
|
187
|
+
async with self._global_lock:
|
|
188
|
+
if name not in self._lists:
|
|
189
|
+
return []
|
|
190
|
+
lst = self._lists[name]
|
|
191
|
+
if end == -1:
|
|
192
|
+
end = len(lst)
|
|
193
|
+
return lst[start:end]
|
|
194
|
+
|
|
195
|
+
# ---------------- Set ----------------
|
|
196
|
+
async def sadd(self, name: str, *values: Any) -> int:
|
|
197
|
+
async with self._global_lock:
|
|
198
|
+
if name not in self._sets:
|
|
199
|
+
self._sets[name] = set()
|
|
200
|
+
before = len(self._sets[name])
|
|
201
|
+
self._sets[name].update(values)
|
|
202
|
+
return len(self._sets[name]) - before
|
|
203
|
+
|
|
204
|
+
async def smembers(self, name: str) -> list[Any]:
|
|
205
|
+
async with self._global_lock:
|
|
206
|
+
if name not in self._sets:
|
|
207
|
+
return []
|
|
208
|
+
return list(self._sets[name])
|
|
209
|
+
|
|
210
|
+
async def srem(self, name: str, *values: Any) -> int:
|
|
211
|
+
async with self._global_lock:
|
|
212
|
+
if name not in self._sets:
|
|
213
|
+
return 0
|
|
214
|
+
before = len(self._sets[name])
|
|
215
|
+
for v in values:
|
|
216
|
+
self._sets[name].discard(v)
|
|
217
|
+
return before - len(self._sets[name])
|
|
218
|
+
|
|
219
|
+
# ---------------- Lock (async-based) ----------------
|
|
220
|
+
async def acquire_lock(
|
|
221
|
+
self,
|
|
222
|
+
lock_name: str,
|
|
223
|
+
timeout: int = 10,
|
|
224
|
+
blocking_timeout: int = 5,
|
|
225
|
+
thread_local: bool = True,
|
|
226
|
+
) -> bool:
|
|
227
|
+
if lock_name not in self._locks:
|
|
228
|
+
self._locks[lock_name] = asyncio.Lock()
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
await asyncio.wait_for(self._locks[lock_name].acquire(), timeout=blocking_timeout)
|
|
232
|
+
return True
|
|
233
|
+
except asyncio.TimeoutError:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
async def release_lock(self, lock_name: str) -> bool:
|
|
237
|
+
if lock_name in self._locks and self._locks[lock_name].locked():
|
|
238
|
+
self._locks[lock_name].release()
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
@asynccontextmanager
|
|
243
|
+
async def lock(self, lock_name: str, timeout: int = 10, blocking_timeout: int = 5):
|
|
244
|
+
acquired = await self.acquire_lock(lock_name, timeout, blocking_timeout)
|
|
245
|
+
if not acquired:
|
|
246
|
+
raise CacheError(f"Failed to acquire lock: {lock_name}")
|
|
247
|
+
try:
|
|
248
|
+
yield
|
|
249
|
+
finally:
|
|
250
|
+
await self.release_lock(lock_name)
|
|
251
|
+
|
|
252
|
+
async def close(self) -> None:
|
|
253
|
+
"""Clean up resources"""
|
|
254
|
+
self._data.clear()
|
|
255
|
+
self._expiry.clear()
|
|
256
|
+
self._lists.clear()
|
|
257
|
+
self._sets.clear()
|
|
258
|
+
self._hashes.clear()
|
|
259
|
+
self._locks.clear()
|
|
260
|
+
|
|
261
|
+
def clear(self) -> None:
|
|
262
|
+
"""Clear all cached data"""
|
|
263
|
+
self._data.clear()
|
|
264
|
+
self._expiry.clear()
|
|
265
|
+
self._lists.clear()
|
|
266
|
+
self._sets.clear()
|
|
267
|
+
self._hashes.clear()
|
|
@@ -47,7 +47,7 @@ class RedisCache(Cache):
|
|
|
47
47
|
self,
|
|
48
48
|
key: str,
|
|
49
49
|
value: Any,
|
|
50
|
-
ex:
|
|
50
|
+
ex: int | None = None,
|
|
51
51
|
nx: bool = False,
|
|
52
52
|
xx: bool = False,
|
|
53
53
|
) -> bool:
|
|
@@ -65,7 +65,7 @@ class RedisCache(Cache):
|
|
|
65
65
|
except RedisError as e:
|
|
66
66
|
raise CacheError(f"Failed to set key {key}", e) from e
|
|
67
67
|
|
|
68
|
-
async def get(self, key: str) ->
|
|
68
|
+
async def get(self, key: str) -> Any | None:
|
|
69
69
|
"""Get a value from cache."""
|
|
70
70
|
try:
|
|
71
71
|
return await self.redis_client.get(key)
|
|
@@ -127,7 +127,7 @@ class RedisCache(Cache):
|
|
|
127
127
|
except RedisError as e:
|
|
128
128
|
raise CacheError(f"Failed to set hash field {key} in {name}", e) from e
|
|
129
129
|
|
|
130
|
-
async def hget(self, name: str, key: str) ->
|
|
130
|
+
async def hget(self, name: str, key: str) -> Any | None:
|
|
131
131
|
"""Get a field from a hash."""
|
|
132
132
|
try:
|
|
133
133
|
return await self.redis_client.hget(name, key)
|
|
@@ -164,14 +164,14 @@ class RedisCache(Cache):
|
|
|
164
164
|
except RedisError as e:
|
|
165
165
|
raise CacheError(f"Failed to rpush values to list {name}", e) from e
|
|
166
166
|
|
|
167
|
-
async def lpop(self, name: str) ->
|
|
167
|
+
async def lpop(self, name: str) -> Any | None:
|
|
168
168
|
"""Pop a value from the left of a list."""
|
|
169
169
|
try:
|
|
170
170
|
return await self.redis_client.lpop(name)
|
|
171
171
|
except RedisError as e:
|
|
172
172
|
raise CacheError(f"Failed to lpop from list {name}", e) from e
|
|
173
173
|
|
|
174
|
-
async def rpop(self, name: str) ->
|
|
174
|
+
async def rpop(self, name: str) -> Any | None:
|
|
175
175
|
"""Pop a value from the right of a list."""
|
|
176
176
|
try:
|
|
177
177
|
return await self.redis_client.rpop(name)
|
|
@@ -7,13 +7,13 @@ for different caching implementations like Redis, memory cache, etc.
|
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CacheError(Exception):
|
|
14
14
|
"""Custom exception for cache-related errors."""
|
|
15
15
|
|
|
16
|
-
def __init__(self, message: str, original_error:
|
|
16
|
+
def __init__(self, message: str, original_error: Exception | None = None):
|
|
17
17
|
self.message = message
|
|
18
18
|
self.original_error = original_error
|
|
19
19
|
super().__init__(f"Cache Error: {message}")
|
|
@@ -37,7 +37,7 @@ class Cache(ABC):
|
|
|
37
37
|
self,
|
|
38
38
|
key: str,
|
|
39
39
|
value: Any,
|
|
40
|
-
ex:
|
|
40
|
+
ex: int | None = None,
|
|
41
41
|
nx: bool = False,
|
|
42
42
|
xx: bool = False,
|
|
43
43
|
) -> bool:
|
|
@@ -45,7 +45,7 @@ class Cache(ABC):
|
|
|
45
45
|
pass
|
|
46
46
|
|
|
47
47
|
@abstractmethod
|
|
48
|
-
async def get(self, key: str) ->
|
|
48
|
+
async def get(self, key: str) -> Any | None:
|
|
49
49
|
"""Get a value from cache."""
|
|
50
50
|
pass
|
|
51
51
|
|
|
@@ -85,7 +85,7 @@ class Cache(ABC):
|
|
|
85
85
|
pass
|
|
86
86
|
|
|
87
87
|
@abstractmethod
|
|
88
|
-
async def hget(self, name: str, key: str) ->
|
|
88
|
+
async def hget(self, name: str, key: str) -> Any | None:
|
|
89
89
|
"""Get a field from a hash."""
|
|
90
90
|
pass
|
|
91
91
|
|
|
@@ -110,12 +110,12 @@ class Cache(ABC):
|
|
|
110
110
|
pass
|
|
111
111
|
|
|
112
112
|
@abstractmethod
|
|
113
|
-
async def lpop(self, name: str) ->
|
|
113
|
+
async def lpop(self, name: str) -> Any | None:
|
|
114
114
|
"""Pop a value from the left of a list."""
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
117
|
@abstractmethod
|
|
118
|
-
async def rpop(self, name: str) ->
|
|
118
|
+
async def rpop(self, name: str) -> Any | None:
|
|
119
119
|
"""Pop a value from the right of a list."""
|
|
120
120
|
pass
|
|
121
121
|
|
|
@@ -11,15 +11,13 @@ async def get_cache_client() -> Cache:
|
|
|
11
11
|
Returns:
|
|
12
12
|
Cache: Redis client if Redis is enabled in config, otherwise returns page cache.
|
|
13
13
|
"""
|
|
14
|
-
from fastlib.cache._redis_cache import RedisManager
|
|
15
|
-
|
|
16
14
|
config = ConfigManager.get_database_config()
|
|
17
15
|
if config.enable_redis:
|
|
18
|
-
from fastlib.cache._redis_cache import RedisCache
|
|
16
|
+
from fastlib.cache._redis_cache import RedisCache, RedisManager
|
|
19
17
|
|
|
20
18
|
redis_client = await RedisManager.get_instance()
|
|
21
19
|
return RedisCache(redis_client)
|
|
22
20
|
else:
|
|
23
|
-
from fastlib.cache.
|
|
21
|
+
from fastlib.cache._memony_cache import MemoryCache
|
|
24
22
|
|
|
25
|
-
return
|
|
23
|
+
return MemoryCache()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""ConfigLoader class for loading and managing application configurations from YAML files."""
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from loguru import logger
|
|
@@ -12,7 +12,7 @@ from fastlib.config import utils as config_util
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class ConfigLoader:
|
|
15
|
-
def __init__(self, env: str, base_config_file:
|
|
15
|
+
def __init__(self, env: str, base_config_file: str | None = None) -> None:
|
|
16
16
|
"""
|
|
17
17
|
Initializes a new instance of the ConfigLoader class
|
|
18
18
|
|
|
@@ -59,7 +59,7 @@ class ConfigLoader:
|
|
|
59
59
|
except yaml.YAMLError as e:
|
|
60
60
|
raise yaml.YAMLError(f"Error parsing YAML file {file_path}: {e}") from e
|
|
61
61
|
|
|
62
|
-
def load_config(self, environment:
|
|
62
|
+
def load_config(self, environment: str | None = None) -> dict[str, Any]:
|
|
63
63
|
"""
|
|
64
64
|
Load the base configuration and merge with environment-specific settings.
|
|
65
65
|
"""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Configuration loading and management with dynamic registry support."""
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from fastlib import constants as constant
|
|
8
8
|
from fastlib.config import utils as config_util
|
|
@@ -38,7 +38,7 @@ def config_class(name: str):
|
|
|
38
38
|
class ConfigManager:
|
|
39
39
|
"""Global configuration manager with static methods and dynamic registry support."""
|
|
40
40
|
|
|
41
|
-
_global_config_dict:
|
|
41
|
+
_global_config_dict: dict[str, Any] | None = None
|
|
42
42
|
_config_instances: dict[str, BaseConfig] = {}
|
|
43
43
|
_initialized: bool = False
|
|
44
44
|
|
|
@@ -63,9 +63,9 @@ class ConfigManager:
|
|
|
63
63
|
|
|
64
64
|
@staticmethod
|
|
65
65
|
def initialize_global_config(
|
|
66
|
-
env:
|
|
67
|
-
config_file:
|
|
68
|
-
custom_configs:
|
|
66
|
+
env: str | None = None,
|
|
67
|
+
config_file: str | None = None,
|
|
68
|
+
custom_configs: dict[str, Any] | None = None,
|
|
69
69
|
) -> None:
|
|
70
70
|
"""
|
|
71
71
|
Initialize global configuration.
|
|
@@ -136,7 +136,7 @@ class ConfigManager:
|
|
|
136
136
|
) from e
|
|
137
137
|
|
|
138
138
|
@staticmethod
|
|
139
|
-
def get_config_instance(config_name: str) ->
|
|
139
|
+
def get_config_instance(config_name: str) -> BaseConfig | None:
|
|
140
140
|
"""
|
|
141
141
|
Get a specific configuration instance.
|
|
142
142
|
|
|
@@ -205,7 +205,7 @@ class ConfigManager:
|
|
|
205
205
|
return ConfigManager._initialized
|
|
206
206
|
|
|
207
207
|
@staticmethod
|
|
208
|
-
def get_config_dict() ->
|
|
208
|
+
def get_config_dict() -> dict[str, Any] | None:
|
|
209
209
|
"""
|
|
210
210
|
Get the raw configuration dictionary.
|
|
211
211
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Configuration registry for dynamic configuration class management."""
|
|
3
3
|
|
|
4
4
|
# Import BaseConfig from config module to avoid circular imports
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ConfigRegistry:
|
|
@@ -36,7 +36,7 @@ class ConfigRegistry:
|
|
|
36
36
|
cls._registry[name] = config_class
|
|
37
37
|
|
|
38
38
|
@classmethod
|
|
39
|
-
def get_config_class(cls, name: str) ->
|
|
39
|
+
def get_config_class(cls, name: str) -> type | None:
|
|
40
40
|
"""
|
|
41
41
|
Get a registered configuration class.
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ class ConfigRegistry:
|
|
|
49
49
|
return cls._registry.get(name)
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
|
-
def create_instance(cls, name: str, config_dict: dict[str, Any]) ->
|
|
52
|
+
def create_instance(cls, name: str, config_dict: dict[str, Any]) -> Any | None:
|
|
53
53
|
"""
|
|
54
54
|
Create an instance of a registered configuration class.
|
|
55
55
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import configparser
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, NamedTuple
|
|
6
|
+
from typing import Any, NamedTuple
|
|
7
7
|
from urllib.parse import urlparse
|
|
8
8
|
|
|
9
9
|
from loguru import logger
|
|
@@ -36,8 +36,8 @@ class ProjectInfo:
|
|
|
36
36
|
self,
|
|
37
37
|
name: str,
|
|
38
38
|
version: str,
|
|
39
|
-
description:
|
|
40
|
-
authors:
|
|
39
|
+
description: str | None = None,
|
|
40
|
+
authors: list[str] | None = None,
|
|
41
41
|
):
|
|
42
42
|
self.name = name
|
|
43
43
|
self.version = version
|
|
@@ -6,12 +6,12 @@ the current authenticated user's ID throughout the request lifecycle.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from contextvars import ContextVar
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
from loguru import logger
|
|
12
12
|
|
|
13
13
|
# Type aliases for better readability
|
|
14
|
-
UserID =
|
|
14
|
+
UserID = int | None
|
|
15
15
|
|
|
16
16
|
# Context variable declaration
|
|
17
17
|
_current_user_id: ContextVar[UserID] = ContextVar("current_user_id", default=None)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Base exception class for the application."""
|
|
3
3
|
|
|
4
4
|
from http import HTTPStatus
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
@@ -27,8 +27,8 @@ class BaseException(Exception):
|
|
|
27
27
|
def __init__(
|
|
28
28
|
self,
|
|
29
29
|
code: ErrorDetail,
|
|
30
|
-
message:
|
|
31
|
-
details:
|
|
30
|
+
message: str | None = None,
|
|
31
|
+
details: Any | None = None,
|
|
32
32
|
):
|
|
33
33
|
"""
|
|
34
34
|
Initialize the HTTPException.
|
|
@@ -58,5 +58,5 @@ class SystemErrorCode:
|
|
|
58
58
|
|
|
59
59
|
class SystemException(BaseException):
|
|
60
60
|
code: SystemErrorCode
|
|
61
|
-
message:
|
|
62
|
-
details:
|
|
61
|
+
message: str | None = None
|
|
62
|
+
details: Any | None = None
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import textwrap
|
|
5
5
|
import traceback
|
|
6
6
|
from http import HTTPStatus
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from fastapi import Request
|
|
10
10
|
from fastapi.exception_handlers import http_exception_handler
|
|
@@ -95,7 +95,7 @@ def build_error_response(
|
|
|
95
95
|
exc: Exception,
|
|
96
96
|
request: Request,
|
|
97
97
|
status_code: int,
|
|
98
|
-
headers:
|
|
98
|
+
headers: dict[str, str] | None = None,
|
|
99
99
|
) -> Response:
|
|
100
100
|
"""
|
|
101
101
|
Build standardized error response.
|
|
@@ -5,8 +5,8 @@ Internationalization context manager
|
|
|
5
5
|
|
|
6
6
|
import contextvars
|
|
7
7
|
import functools
|
|
8
|
-
from collections.abc import Awaitable
|
|
9
|
-
from typing import Any,
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
10
|
|
|
11
11
|
from fastlib.i18n.types import Language
|
|
12
12
|
|