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.
Files changed (74) hide show
  1. fastlib/__init__.py +2 -0
  2. fastlib/cache/__init__.py +7 -0
  3. fastlib/cache/base_cache.py +175 -0
  4. fastlib/cache/cache_manager.py +25 -0
  5. fastlib/cache/disk_cache.py +214 -0
  6. fastlib/cache/redis_cache.py +369 -0
  7. fastlib/config/__init__.py +10 -0
  8. fastlib/config/config.py +20 -0
  9. fastlib/config/database_config.py +83 -0
  10. fastlib/config/loader.py +91 -0
  11. fastlib/config/manager.py +244 -0
  12. fastlib/config/registry.py +132 -0
  13. fastlib/config/security_config.py +57 -0
  14. fastlib/config/server_config.py +66 -0
  15. fastlib/constant/__init__.py +19 -0
  16. fastlib/constant/constant.py +30 -0
  17. fastlib/context/__init__.py +0 -0
  18. fastlib/context/contextvars.py +7 -0
  19. fastlib/enums/__init__.py +11 -0
  20. fastlib/enums/base_error_code.py +15 -0
  21. fastlib/enums/enum.py +33 -0
  22. fastlib/exception/__init__.py +6 -0
  23. fastlib/exception/custom_exception.py +40 -0
  24. fastlib/exception/exception_handler.py +269 -0
  25. fastlib/exception/exception_manager.py +32 -0
  26. fastlib/i18n/i18n_context.py +217 -0
  27. fastlib/i18n/i18n_messages.py +100 -0
  28. fastlib/i18n/i18n_types.py +12 -0
  29. fastlib/i18n/i18n_util.py +231 -0
  30. fastlib/logging/__init__.py +0 -0
  31. fastlib/logging/config.py +41 -0
  32. fastlib/logging/handlers.py +207 -0
  33. fastlib/mapper/__init__.py +0 -0
  34. fastlib/mapper/base_mapper.py +260 -0
  35. fastlib/mapper/impl/__init__.py +0 -0
  36. fastlib/mapper/impl/base_mapper_impl.py +488 -0
  37. fastlib/middleware/__init__.py +7 -0
  38. fastlib/middleware/db_session_middleware.py +117 -0
  39. fastlib/middleware/jwt_middleware.py +86 -0
  40. fastlib/middleware/log_middleware.py +50 -0
  41. fastlib/migrate/migrate.py +61 -0
  42. fastlib/model/__init__.py +5 -0
  43. fastlib/model/base_model.py +62 -0
  44. fastlib/openapi/__init__.py +5 -0
  45. fastlib/openapi/offline.py +53 -0
  46. fastlib/router/__init__.py +5 -0
  47. fastlib/router/router_v1.py +129 -0
  48. fastlib/schema/__init__.py +19 -0
  49. fastlib/schema/response.py +110 -0
  50. fastlib/schema/schema.py +82 -0
  51. fastlib/security/__init__.py +23 -0
  52. fastlib/security/security.py +202 -0
  53. fastlib/server.py +189 -0
  54. fastlib/service/__init__.py +0 -0
  55. fastlib/service/base_service.py +75 -0
  56. fastlib/service/impl/__init__.py +0 -0
  57. fastlib/service/impl/base_service_impl.py +79 -0
  58. fastlib/session/__init__.py +5 -0
  59. fastlib/session/db_engine.py +43 -0
  60. fastlib/session/db_session.py +31 -0
  61. fastlib/utils/__init__.py +2 -0
  62. fastlib/utils/excel_util.py +123 -0
  63. fastlib/utils/file_util.py +178 -0
  64. fastlib/utils/http_util.py +275 -0
  65. fastlib/utils/ip_util.py +23 -0
  66. fastlib/utils/project_config_util.py +209 -0
  67. fastlib/utils/security_util.py +590 -0
  68. fastlib/utils/snowflake_util.py +262 -0
  69. fastlib/utils/str_util.py +22 -0
  70. fastlib_py-0.1.0.dist-info/METADATA +35 -0
  71. fastlib_py-0.1.0.dist-info/RECORD +74 -0
  72. fastlib_py-0.1.0.dist-info/WHEEL +5 -0
  73. fastlib_py-0.1.0.dist-info/licenses/LICENSE +21 -0
  74. fastlib_py-0.1.0.dist-info/top_level.txt +1 -0
fastlib/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Fast Web Application Libraries Package."""
@@ -0,0 +1,7 @@
1
+ """Provides a unified cache client based on configuration."""
2
+
3
+ from .cache_manager import get_cache_client
4
+
5
+ __all__ = [
6
+ get_cache_client,
7
+ ]
@@ -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()