fastlib-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastlib/__init__.py +2 -0
- fastlib/cache/__init__.py +7 -0
- fastlib/cache/base_cache.py +175 -0
- fastlib/cache/cache_manager.py +25 -0
- fastlib/cache/disk_cache.py +214 -0
- fastlib/cache/redis_cache.py +369 -0
- fastlib/config/__init__.py +10 -0
- fastlib/config/config.py +20 -0
- fastlib/config/database_config.py +83 -0
- fastlib/config/loader.py +91 -0
- fastlib/config/manager.py +244 -0
- fastlib/config/registry.py +132 -0
- fastlib/config/security_config.py +57 -0
- fastlib/config/server_config.py +66 -0
- fastlib/constant/__init__.py +19 -0
- fastlib/constant/constant.py +30 -0
- fastlib/context/__init__.py +0 -0
- fastlib/context/contextvars.py +7 -0
- fastlib/enums/__init__.py +11 -0
- fastlib/enums/base_error_code.py +15 -0
- fastlib/enums/enum.py +33 -0
- fastlib/exception/__init__.py +6 -0
- fastlib/exception/custom_exception.py +40 -0
- fastlib/exception/exception_handler.py +269 -0
- fastlib/exception/exception_manager.py +32 -0
- fastlib/i18n/i18n_context.py +217 -0
- fastlib/i18n/i18n_messages.py +100 -0
- fastlib/i18n/i18n_types.py +12 -0
- fastlib/i18n/i18n_util.py +231 -0
- fastlib/logging/__init__.py +0 -0
- fastlib/logging/config.py +41 -0
- fastlib/logging/handlers.py +207 -0
- fastlib/mapper/__init__.py +0 -0
- fastlib/mapper/base_mapper.py +260 -0
- fastlib/mapper/impl/__init__.py +0 -0
- fastlib/mapper/impl/base_mapper_impl.py +488 -0
- fastlib/middleware/__init__.py +7 -0
- fastlib/middleware/db_session_middleware.py +117 -0
- fastlib/middleware/jwt_middleware.py +86 -0
- fastlib/middleware/log_middleware.py +50 -0
- fastlib/migrate/migrate.py +61 -0
- fastlib/model/__init__.py +5 -0
- fastlib/model/base_model.py +62 -0
- fastlib/openapi/__init__.py +5 -0
- fastlib/openapi/offline.py +53 -0
- fastlib/router/__init__.py +5 -0
- fastlib/router/router_v1.py +129 -0
- fastlib/schema/__init__.py +19 -0
- fastlib/schema/response.py +110 -0
- fastlib/schema/schema.py +82 -0
- fastlib/security/__init__.py +23 -0
- fastlib/security/security.py +202 -0
- fastlib/server.py +189 -0
- fastlib/service/__init__.py +0 -0
- fastlib/service/base_service.py +75 -0
- fastlib/service/impl/__init__.py +0 -0
- fastlib/service/impl/base_service_impl.py +79 -0
- fastlib/session/__init__.py +5 -0
- fastlib/session/db_engine.py +43 -0
- fastlib/session/db_session.py +31 -0
- fastlib/utils/__init__.py +2 -0
- fastlib/utils/excel_util.py +123 -0
- fastlib/utils/file_util.py +178 -0
- fastlib/utils/http_util.py +275 -0
- fastlib/utils/ip_util.py +23 -0
- fastlib/utils/project_config_util.py +209 -0
- fastlib/utils/security_util.py +590 -0
- fastlib/utils/snowflake_util.py +262 -0
- fastlib/utils/str_util.py +22 -0
- fastlib_py-0.1.0.dist-info/METADATA +35 -0
- fastlib_py-0.1.0.dist-info/RECORD +74 -0
- fastlib_py-0.1.0.dist-info/WHEEL +5 -0
- fastlib_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- fastlib_py-0.1.0.dist-info/top_level.txt +1 -0
fastlib/__init__.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base cache interface providing a unified caching abstraction.
|
|
3
|
+
|
|
4
|
+
This module defines the base Cache class that provides a common interface
|
|
5
|
+
for different caching implementations like Redis, memory cache, etc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CacheError(Exception):
|
|
14
|
+
"""Custom exception for cache-related errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, original_error: Optional[Exception] = None):
|
|
17
|
+
self.message = message
|
|
18
|
+
self.original_error = original_error
|
|
19
|
+
super().__init__(f"Cache Error: {message}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Cache(ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract base class for cache implementations.
|
|
25
|
+
|
|
26
|
+
This class defines the interface that all cache implementations must follow,
|
|
27
|
+
providing a unified API for caching operations across different backends.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def ping(self) -> bool:
|
|
32
|
+
"""Test cache connection/availability."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def set(
|
|
37
|
+
self,
|
|
38
|
+
key: str,
|
|
39
|
+
value: Any,
|
|
40
|
+
ex: Optional[int] = None,
|
|
41
|
+
nx: bool = False,
|
|
42
|
+
xx: bool = False,
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Set a key-value pair in cache."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def get(self, key: str) -> Optional[Any]:
|
|
49
|
+
"""Get a value from cache."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def delete(self, *keys: str) -> int:
|
|
54
|
+
"""Delete one or more keys from cache."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def exists(self, *keys: str) -> int:
|
|
59
|
+
"""Check if keys exist in cache."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def expire(self, key: str, seconds: int) -> bool:
|
|
64
|
+
"""Set expiration time for a key."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def ttl(self, key: str) -> int:
|
|
69
|
+
"""Get time to live for a key."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def incr(self, key: str, amount: int = 1) -> int:
|
|
74
|
+
"""Increment a counter in cache."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def decr(self, key: str, amount: int = 1) -> int:
|
|
79
|
+
"""Decrement a counter in cache."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
async def hset(self, name: str, key: str, value: Any) -> int:
|
|
84
|
+
"""Set a field in a hash."""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
async def hget(self, name: str, key: str) -> Optional[Any]:
|
|
89
|
+
"""Get a field from a hash."""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
async def hgetall(self, name: str) -> Dict[str, Any]:
|
|
94
|
+
"""Get all fields from a hash."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def hdel(self, name: str, *keys: str) -> int:
|
|
99
|
+
"""Delete fields from a hash."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def lpush(self, name: str, *values: Any) -> int:
|
|
104
|
+
"""Push values to the left of a list."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
async def rpush(self, name: str, *values: Any) -> int:
|
|
109
|
+
"""Push values to the right of a list."""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
async def lpop(self, name: str) -> Optional[Any]:
|
|
114
|
+
"""Pop a value from the left of a list."""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def rpop(self, name: str) -> Optional[Any]:
|
|
119
|
+
"""Pop a value from the right of a list."""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def lrange(self, name: str, start: int = 0, end: int = -1) -> List[Any]:
|
|
124
|
+
"""Get a range of values from a list."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def sadd(self, name: str, *values: Any) -> int:
|
|
129
|
+
"""Add values to a set."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
async def smembers(self, name: str) -> List[Any]:
|
|
134
|
+
"""Get all members of a set."""
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
async def srem(self, name: str, *values: Any) -> int:
|
|
139
|
+
"""Remove values from a set."""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
async def acquire_lock(
|
|
144
|
+
self,
|
|
145
|
+
lock_name: str,
|
|
146
|
+
timeout: int = 10,
|
|
147
|
+
blocking_timeout: int = 5,
|
|
148
|
+
thread_local: bool = True,
|
|
149
|
+
) -> bool:
|
|
150
|
+
"""Acquire a distributed lock."""
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
async def release_lock(self, lock_name: str) -> bool:
|
|
155
|
+
"""Release a distributed lock."""
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
@asynccontextmanager
|
|
160
|
+
async def lock(self, lock_name: str, timeout: int = 10, blocking_timeout: int = 5):
|
|
161
|
+
"""Context manager for distributed locks."""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
async def close(self) -> None:
|
|
166
|
+
"""Close cache connections."""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
async def __aenter__(self):
|
|
170
|
+
"""Async context manager entry."""
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
174
|
+
"""Async context manager exit."""
|
|
175
|
+
await self.close()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Cache Client manager to instantiate the appropriate cache client"""
|
|
3
|
+
|
|
4
|
+
from fastlib.cache.base_cache import Cache
|
|
5
|
+
from fastlib.cache.redis_cache import RedisManager
|
|
6
|
+
from fastlib.config.manager import ConfigManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_cache_client() -> Cache:
|
|
10
|
+
"""Initialize and return the appropriate cache client based on configuration.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Cache: Redis client if Redis is enabled in config, otherwise returns page cache.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
config = ConfigManager.get_database_config()
|
|
17
|
+
if config.enable_redis:
|
|
18
|
+
from fastlib.cache.redis_cache import RedisCache
|
|
19
|
+
|
|
20
|
+
redis_client = await RedisManager.get_instance()
|
|
21
|
+
return RedisCache(redis_client)
|
|
22
|
+
else:
|
|
23
|
+
from fastlib.cache.disk_cache import PageCache
|
|
24
|
+
|
|
25
|
+
return PageCache()
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""DiskCache-based PageCache implementation compatible with Cache interface."""
|
|
3
|
+
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
import diskcache
|
|
10
|
+
|
|
11
|
+
from fastlib.cache.base_cache import Cache, CacheError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PageCache(Cache):
|
|
15
|
+
def __init__(self, directory: str = "./.diskcache"):
|
|
16
|
+
self.cache = diskcache.Cache(directory)
|
|
17
|
+
self._locks: Dict[str, threading.Lock] = {}
|
|
18
|
+
|
|
19
|
+
async def ping(self) -> bool:
|
|
20
|
+
try:
|
|
21
|
+
self.cache.set("__ping__", "pong", expire=1)
|
|
22
|
+
return True
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise CacheError("Ping failed", e)
|
|
25
|
+
|
|
26
|
+
async def set(
|
|
27
|
+
self,
|
|
28
|
+
key: str,
|
|
29
|
+
value: Any,
|
|
30
|
+
ex: Optional[int] = None,
|
|
31
|
+
nx: bool = False, # only set if not exists
|
|
32
|
+
xx: bool = False, # only set if exists
|
|
33
|
+
) -> bool:
|
|
34
|
+
if nx and key in self.cache:
|
|
35
|
+
return False
|
|
36
|
+
if xx and key not in self.cache:
|
|
37
|
+
return False
|
|
38
|
+
return self.cache.set(key, value, expire=ex)
|
|
39
|
+
|
|
40
|
+
async def get(self, key: str) -> Optional[Any]:
|
|
41
|
+
return self.cache.get(key, None)
|
|
42
|
+
|
|
43
|
+
async def delete(self, *keys: str) -> int:
|
|
44
|
+
count = 0
|
|
45
|
+
for k in keys:
|
|
46
|
+
if k in self.cache:
|
|
47
|
+
self.cache.delete(k)
|
|
48
|
+
count += 1
|
|
49
|
+
return count
|
|
50
|
+
|
|
51
|
+
async def exists(self, *keys: str) -> int:
|
|
52
|
+
return sum(1 for k in keys if k in self.cache)
|
|
53
|
+
|
|
54
|
+
async def expire(self, key: str, seconds: int) -> bool:
|
|
55
|
+
if key not in self.cache:
|
|
56
|
+
return False
|
|
57
|
+
value = self.cache.get(key)
|
|
58
|
+
self.cache.set(key, value, expire=seconds)
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
async def ttl(self, key: str) -> int:
|
|
62
|
+
if key not in self.cache:
|
|
63
|
+
return -2 # Redis convention: -2 means key does not exist
|
|
64
|
+
expiry = self.cache.ttl(key)
|
|
65
|
+
return int(expiry) if expiry is not None else -1 # -1 means no expiry
|
|
66
|
+
|
|
67
|
+
async def incr(self, key: str, amount: int = 1) -> int:
|
|
68
|
+
with self.cache.transact():
|
|
69
|
+
val = self.cache.get(key, 0)
|
|
70
|
+
if not isinstance(val, int):
|
|
71
|
+
raise CacheError("Value is not integer")
|
|
72
|
+
val += amount
|
|
73
|
+
self.cache.set(key, val)
|
|
74
|
+
return val
|
|
75
|
+
|
|
76
|
+
async def decr(self, key: str, amount: int = 1) -> int:
|
|
77
|
+
return await self.incr(key, -amount)
|
|
78
|
+
|
|
79
|
+
# ---------------- Hash (dict) ----------------
|
|
80
|
+
async def hset(self, name: str, key: str, value: Any) -> int:
|
|
81
|
+
data = self.cache.get(name, {})
|
|
82
|
+
if not isinstance(data, dict):
|
|
83
|
+
data = {}
|
|
84
|
+
data[key] = value
|
|
85
|
+
self.cache.set(name, data)
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
async def hget(self, name: str, key: str) -> Optional[Any]:
|
|
89
|
+
data = self.cache.get(name, {})
|
|
90
|
+
return data.get(key) if isinstance(data, dict) else None
|
|
91
|
+
|
|
92
|
+
async def hgetall(self, name: str) -> Dict[str, Any]:
|
|
93
|
+
data = self.cache.get(name, {})
|
|
94
|
+
return data if isinstance(data, dict) else {}
|
|
95
|
+
|
|
96
|
+
async def hdel(self, name: str, *keys: str) -> int:
|
|
97
|
+
data = self.cache.get(name, {})
|
|
98
|
+
if not isinstance(data, dict):
|
|
99
|
+
return 0
|
|
100
|
+
removed = 0
|
|
101
|
+
for k in keys:
|
|
102
|
+
if k in data:
|
|
103
|
+
del data[k]
|
|
104
|
+
removed += 1
|
|
105
|
+
self.cache.set(name, data)
|
|
106
|
+
return removed
|
|
107
|
+
|
|
108
|
+
# ---------------- List ----------------
|
|
109
|
+
def _get_list(self, key: str) -> List[Any]:
|
|
110
|
+
data = self.cache.get(key)
|
|
111
|
+
if data is None:
|
|
112
|
+
return []
|
|
113
|
+
if isinstance(data, str): # if JSON stored
|
|
114
|
+
try:
|
|
115
|
+
return json.loads(data)
|
|
116
|
+
except Exception:
|
|
117
|
+
return []
|
|
118
|
+
if isinstance(data, list):
|
|
119
|
+
return data
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
def _set_list(self, key: str, value: List[Any]):
|
|
123
|
+
self.cache.set(key, json.dumps(value))
|
|
124
|
+
|
|
125
|
+
async def lpush(self, name: str, *values: Any) -> int:
|
|
126
|
+
lst = self._get_list(name)
|
|
127
|
+
for v in values:
|
|
128
|
+
lst.insert(0, v)
|
|
129
|
+
self._set_list(name, lst)
|
|
130
|
+
return len(lst)
|
|
131
|
+
|
|
132
|
+
async def rpush(self, name: str, *values: Any) -> int:
|
|
133
|
+
lst = self._get_list(name)
|
|
134
|
+
lst.extend(values)
|
|
135
|
+
self._set_list(name, lst)
|
|
136
|
+
return len(lst)
|
|
137
|
+
|
|
138
|
+
async def lpop(self, name: str) -> Optional[Any]:
|
|
139
|
+
lst = self._get_list(name)
|
|
140
|
+
if not lst:
|
|
141
|
+
return None
|
|
142
|
+
val = lst.pop(0)
|
|
143
|
+
self._set_list(name, lst)
|
|
144
|
+
return val
|
|
145
|
+
|
|
146
|
+
async def rpop(self, name: str) -> Optional[Any]:
|
|
147
|
+
lst = self._get_list(name)
|
|
148
|
+
if not lst:
|
|
149
|
+
return None
|
|
150
|
+
val = lst.pop()
|
|
151
|
+
self._set_list(name, lst)
|
|
152
|
+
return val
|
|
153
|
+
|
|
154
|
+
async def lrange(self, name: str, start: int = 0, end: int = -1) -> List[Any]:
|
|
155
|
+
lst = self._get_list(name)
|
|
156
|
+
if end == -1:
|
|
157
|
+
end = len(lst) - 1
|
|
158
|
+
return lst[start : end + 1]
|
|
159
|
+
|
|
160
|
+
# ---------------- Set ----------------
|
|
161
|
+
async def sadd(self, name: str, *values: Any) -> int:
|
|
162
|
+
current = self.cache.get(name, set())
|
|
163
|
+
if not isinstance(current, set):
|
|
164
|
+
current = set()
|
|
165
|
+
before = len(current)
|
|
166
|
+
current.update(values)
|
|
167
|
+
self.cache.set(name, current)
|
|
168
|
+
return len(current) - before
|
|
169
|
+
|
|
170
|
+
async def smembers(self, name: str) -> List[Any]:
|
|
171
|
+
current = self.cache.get(name, set())
|
|
172
|
+
return list(current) if isinstance(current, set) else []
|
|
173
|
+
|
|
174
|
+
async def srem(self, name: str, *values: Any) -> int:
|
|
175
|
+
current = self.cache.get(name, set())
|
|
176
|
+
if not isinstance(current, set):
|
|
177
|
+
return 0
|
|
178
|
+
before = len(current)
|
|
179
|
+
for v in values:
|
|
180
|
+
current.discard(v)
|
|
181
|
+
self.cache.set(name, current)
|
|
182
|
+
return before - len(current)
|
|
183
|
+
|
|
184
|
+
# ---------------- Lock (simple thread-based) ----------------
|
|
185
|
+
async def acquire_lock(
|
|
186
|
+
self, lock_name: str, timeout: int = 10, blocking_timeout: int = 5, thread_local: bool = True
|
|
187
|
+
) -> bool:
|
|
188
|
+
lock = self._locks.setdefault(lock_name, threading.Lock())
|
|
189
|
+
start = time.time()
|
|
190
|
+
while time.time() - start < blocking_timeout:
|
|
191
|
+
if lock.acquire(blocking=False):
|
|
192
|
+
return True
|
|
193
|
+
time.sleep(0.05)
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
async def release_lock(self, lock_name: str) -> bool:
|
|
197
|
+
lock = self._locks.get(lock_name)
|
|
198
|
+
if lock and lock.locked():
|
|
199
|
+
lock.release()
|
|
200
|
+
return True
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
@asynccontextmanager
|
|
204
|
+
async def lock(self, lock_name: str, timeout: int = 10, blocking_timeout: int = 5):
|
|
205
|
+
acquired = await self.acquire_lock(lock_name, timeout, blocking_timeout)
|
|
206
|
+
if not acquired:
|
|
207
|
+
raise CacheError(f"Failed to acquire lock: {lock_name}")
|
|
208
|
+
try:
|
|
209
|
+
yield
|
|
210
|
+
finally:
|
|
211
|
+
await self.release_lock(lock_name)
|
|
212
|
+
|
|
213
|
+
async def close(self) -> None:
|
|
214
|
+
self.cache.close()
|