redis-xsync 1.0.1__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.
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ @File : __init__.py
4
+ @Author : Jay
5
+ @Date : 2025/3/16 14:37
6
+ @Desc : Redis client registry and default instance setup
7
+ """
8
+ import asyncio
9
+
10
+ # Set Windows event loop policy to avoid runtime errors on Windows
11
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
12
+
13
+ from collections import defaultdict
14
+ from typing import Optional, Union
15
+
16
+ from .rxcore import RedisXsync
17
+
18
+ # Registry of Redis instances keyed by label
19
+ _redis_registry: defaultdict[str, RedisXsync] = defaultdict(RedisXsync)
20
+
21
+ # Global default Redis instance
22
+ redisXsync: Optional[RedisXsync] = RedisXsync(host="localhost", port=6379, db=-1)
23
+
24
+
25
+ class RedisNotInstantiatedError(Exception):
26
+ """Raised when trying to access a Redis instance that hasn't been registered."""
27
+ pass
28
+
29
+
30
+ def register_redis(label: str, host: str = None, port: int = None, db: Union[str, int] = None,
31
+ password: Optional[str] = None, decode_responses: bool = None, **kwargs) -> RedisXsync:
32
+ """Register a Redis instance in the global registry.
33
+
34
+ If no global default exists, the first registered instance becomes the default.
35
+
36
+ Args:
37
+ label (str): Unique identifier for the Redis instance.
38
+ host (str, optional): Redis server host.
39
+ port (int, optional): Redis server port.
40
+ db (int|str, optional): Redis database index.
41
+ password (str, optional): Redis password.
42
+ decode_responses (bool, optional): Whether to decode byte responses.
43
+ **kwargs: Additional Redis configuration parameters.
44
+
45
+ Returns:
46
+ RedisXsync: The registered Redis client instance.
47
+ """
48
+ global redisXsync
49
+ rx = RedisXsync(
50
+ host=host, port=port,
51
+ db=db, password=password,
52
+ decode_responses=decode_responses,
53
+ **kwargs
54
+ )
55
+ # Set the first registered instance as the default if none exists
56
+ if not _redis_registry:
57
+ redisXsync.rxs = rx.rxs
58
+ _redis_registry.setdefault(label, rx)
59
+ return _redis_registry[label]
60
+
61
+
62
+ def resolve_redis(label: str = None) -> RedisXsync:
63
+ """Resolve a registered Redis instance by label.
64
+
65
+ Args:
66
+ label (str, optional): The label of the Redis instance. If None, returns the first registered instance.
67
+
68
+ Returns:
69
+ RedisXsync: The resolved Redis client instance.
70
+
71
+ Raises:
72
+ RedisNotInstantiatedError: If no instance with the given label is registered.
73
+ """
74
+ if label is None and _redis_registry:
75
+ return next(iter(_redis_registry.values()))
76
+ r = _redis_registry.get(label)
77
+ if r:
78
+ return r
79
+ raise RedisNotInstantiatedError(f"No Redis instance registered with label '{label}'")
80
+
81
+
82
+ __all__ = ["register_redis", "resolve_redis", "RedisXsync", "redisXsync"]
@@ -0,0 +1,126 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ @File : __init__.py
4
+ @Author : Jay
5
+ @Date : 2026/3/19 18:01
6
+ @Desc : Utilities to detect synchronous vs asynchronous call context
7
+ and to decorate async functions to fallback to sync implementations.
8
+ """
9
+ import inspect
10
+ import sys
11
+ from functools import wraps
12
+ from typing import Callable, Any
13
+
14
+ # Flags from code objects that indicate coroutine behavior
15
+ _CO_FROM_COROUTINE = (
16
+ inspect.CO_COROUTINE
17
+ | inspect.CO_ITERABLE_COROUTINE
18
+ | inspect.CO_ASYNC_GENERATOR
19
+ )
20
+
21
+
22
+ def is_coroutine_context(level: int = 2) -> bool:
23
+ """Determine if the current call context is asynchronous (coroutine).
24
+
25
+ Args:
26
+ level (int): Stack level to inspect (default 2). Increase if called from wrapper functions.
27
+
28
+ Returns:
29
+ bool: True if the current frame is part of a coroutine context, False if synchronous.
30
+ """
31
+ frame = sys._getframe(level)
32
+ f_code = frame.f_code
33
+
34
+ # Check if the code object has coroutine flags
35
+ if f_code.co_flags & _CO_FROM_COROUTINE:
36
+ return True
37
+
38
+ # Handle nested "<...>" function wrappers (like decorators)
39
+ if f_code.co_flags & inspect.CO_NESTED and f_code.co_name[0] == '<':
40
+ return is_coroutine_context(level + 2)
41
+
42
+ return False
43
+
44
+
45
+ def awaitable(sync_func: Callable) -> Callable:
46
+ """Decorator for async functions to use a synchronous fallback when called from sync context.
47
+
48
+ Usage:
49
+ def foo_sync(x, y): ...
50
+
51
+ @awaitable(foo_sync)
52
+ async def foo_async(x, y): ...
53
+
54
+ Args:
55
+ sync_func (Callable): Synchronous implementation of the async function.
56
+
57
+ Returns:
58
+ Callable: Decorated async function that uses `sync_func` if called synchronously.
59
+ """
60
+
61
+ def decorator(async_func: Callable) -> Callable:
62
+ # Signature check to ensure sync/async match
63
+ sig_sync = inspect.signature(sync_func)
64
+ sig_async = inspect.signature(async_func)
65
+ if sig_sync != sig_async:
66
+ raise TypeError(
67
+ f"{sync_func.__name__} and async {async_func.__name__} signatures differ:\n"
68
+ f" sync: {sig_sync}\n"
69
+ f" async: {sig_async}"
70
+ )
71
+
72
+ @wraps(async_func)
73
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
74
+ # If currently inside a coroutine, run async function
75
+ if is_coroutine_context():
76
+ return async_func(*args, **kwargs)
77
+
78
+ # Otherwise, run synchronous version
79
+ result = sync_func(*args, **kwargs)
80
+ if inspect.iscoroutine(result):
81
+ raise RuntimeError(
82
+ f"Synchronous function {sync_func.__name__} returned a coroutine object. "
83
+ f"Ensure it is a true synchronous implementation."
84
+ )
85
+ return result
86
+
87
+ # Attach references for introspection
88
+ wrapper._sync_func = sync_func
89
+ wrapper._async_func = async_func
90
+ wrapper._awaitable = True
91
+ return wrapper
92
+
93
+ return decorator
94
+
95
+
96
+ __all__ = ["awaitable", "is_coroutine_context"]
97
+
98
+ if __name__ == '__main__':
99
+ # Demonstration of `awaitable` decorator usage
100
+ class Limited:
101
+ def _ask_available_container_sync(self, quantity):
102
+ """Synchronous fallback implementation."""
103
+ return "sync-"
104
+
105
+ @awaitable(_ask_available_container_sync)
106
+ async def ask_available_container(self, quantity):
107
+ """Async implementation."""
108
+ return "async+"
109
+
110
+
111
+ lt = Limited()
112
+
113
+ # Synchronous call
114
+ res_sync = lt.ask_available_container(1)
115
+ print("Sync result:", res_sync)
116
+
117
+ # Asynchronous call
118
+ import asyncio
119
+
120
+
121
+ async def main():
122
+ res_async = await lt.ask_available_container(2)
123
+ print("Async result:", res_async)
124
+
125
+
126
+ asyncio.run(main())
@@ -0,0 +1,202 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ @File : distributed_lock.py
4
+ @Author : Jay
5
+ @Date : 2026/3/18 17:47
6
+ @Desc : Redis connection pool and distributed lock implementation
7
+ """
8
+ import asyncio
9
+ import time
10
+ import uuid
11
+
12
+ _ACQUIRE_LUA = """
13
+ local ttl_ms = tonumber(ARGV[#ARGV])
14
+ for i=1,#KEYS do
15
+ if redis.call('exists', KEYS[i]) == 1 then
16
+ return 0
17
+ end
18
+ redis.call('set', KEYS[i], ARGV[i], 'PX', ttl_ms)
19
+ end
20
+ return 1
21
+ """
22
+ _RELEASE_LUA = """
23
+ for i=1,#KEYS do
24
+ if redis.call('get', KEYS[i]) == ARGV[i] then
25
+ redis.call('del', KEYS[i])
26
+ end
27
+ end
28
+ return 1
29
+ """
30
+
31
+
32
+ # Distributed lock class, use `aioredlock` for cluster-wide locks
33
+ class AsyncRedisAtomicMultiLock:
34
+ """Implements a Redis-based atomic multi-key lock with optional blocking behavior."""
35
+
36
+ def __init__(
37
+ self,
38
+ *keys: str,
39
+ redis_xsync,
40
+ redis_db,
41
+ ttl_ms: int = 5000,
42
+ blocking: bool = False,
43
+ blocking_timeout: int = 5,
44
+ sleep: float = 0.3
45
+ ):
46
+ """Initialize the atomic multi-key lock.
47
+
48
+ Args:
49
+ *keys (str): Redis keys to lock, can pass multiple keys.
50
+ redis_xsync: Redis client factory supporting sync/async context.
51
+ redis_db: Redis database number.
52
+ ttl_ms (int): Lock timeout in milliseconds.
53
+ blocking (bool): If True, wait until lock is acquired when already locked.
54
+ blocking_timeout (int): Maximum blocking wait time in seconds.
55
+ sleep (float): Sleep interval between retries when blocking.
56
+ """
57
+ self.redis_xsync = redis_xsync
58
+ self.redis_db = redis_db
59
+ self.keys = list(keys)
60
+ self.ttl_ms = int(ttl_ms)
61
+ self.container = list(map(lambda _: uuid.uuid4().hex, keys)) # Unique lock values
62
+ self.acquired = False
63
+
64
+ self.blocking = blocking
65
+ self.blocking_timeout = blocking_timeout
66
+ self.sleep = sleep
67
+
68
+ # <editor-fold desc="Distributed lock: async implementation">
69
+ async def _async_with_client(self, script: str, keys, *args):
70
+ """Helper to run Lua scripts asynchronously against Redis.
71
+
72
+ Args:
73
+ script (str): Lua script to execute.
74
+ keys (list): List of Redis keys.
75
+ *args: Arguments for the Lua script.
76
+
77
+ Returns:
78
+ bool: True if Lua script returned success (1), False otherwise.
79
+ """
80
+ async with self.redis_xsync(db=self.redis_db) as redis:
81
+ return (await redis.eval(script, len(keys), *keys, *args)) == 1
82
+
83
+ async def async_acquire(self) -> bool:
84
+ """Asynchronously acquire the lock.
85
+
86
+ Returns:
87
+ bool: True if lock acquired successfully, False otherwise.
88
+ """
89
+ argv = [*self.container, str(self.ttl_ms)]
90
+
91
+ async def try_once():
92
+ return await self._async_with_client(_ACQUIRE_LUA, self.keys, *argv)
93
+
94
+ if not self.blocking:
95
+ self.acquired = await try_once()
96
+ return self.acquired
97
+
98
+ end_time = asyncio.get_event_loop().time() + self.blocking_timeout
99
+ while True:
100
+ if await try_once():
101
+ self.acquired = True
102
+ return True
103
+ if asyncio.get_event_loop().time() > end_time:
104
+ return False
105
+ await asyncio.sleep(self.sleep)
106
+
107
+ async def async_release(self) -> None:
108
+ """Asynchronously release the lock, only if held by this instance."""
109
+ try:
110
+ await self._async_with_client(_RELEASE_LUA, self.keys, *self.container)
111
+ finally:
112
+ self.acquired = False
113
+
114
+ async def __aenter__(self):
115
+ """Async context manager entry.
116
+
117
+ Raises:
118
+ TimeoutError: If lock cannot be acquired within timeout.
119
+
120
+ Returns:
121
+ AsyncRedisAtomicMultiLock: self
122
+ """
123
+ ok = await self.async_acquire()
124
+ if not ok:
125
+ raise TimeoutError("Failed to acquire lock within timeout")
126
+ return self
127
+
128
+ async def __aexit__(self, exc_type, exc, tb):
129
+ """Async context manager exit."""
130
+ await self.async_release()
131
+
132
+ # </editor-fold>
133
+
134
+ # <editor-fold desc="Distributed lock: sync implementation">
135
+ def _sync_with_client(self, script: str, keys, *args) -> bool:
136
+ """Helper to run Lua scripts synchronously against Redis.
137
+
138
+ Args:
139
+ script (str): Lua script to execute.
140
+ keys (list): List of Redis keys.
141
+ *args: Arguments for the Lua script.
142
+
143
+ Returns:
144
+ bool: True if Lua script returned success (1), False otherwise.
145
+ """
146
+ with self.redis_xsync(db=self.redis_db) as redis:
147
+ return int(redis.eval(script, len(keys), *keys, *args)) == 1
148
+
149
+ def sync_acquire(self) -> bool:
150
+ """Synchronously acquire the lock.
151
+
152
+ Returns:
153
+ bool: True if lock acquired successfully, False otherwise.
154
+ """
155
+ argv = [*self.container, str(self.ttl_ms)]
156
+
157
+ def try_once():
158
+ return self._sync_with_client(_ACQUIRE_LUA, self.keys, *argv)
159
+
160
+ if not self.blocking:
161
+ self.acquired = try_once()
162
+ return self.acquired
163
+
164
+ end_time = time.time() + self.blocking_timeout
165
+ while True:
166
+ if try_once():
167
+ self.acquired = True
168
+ return True
169
+ if time.time() > end_time:
170
+ return False
171
+ time.sleep(self.sleep)
172
+
173
+ def sync_release(self) -> None:
174
+ """Synchronously release the lock."""
175
+ try:
176
+ self._sync_with_client(_RELEASE_LUA, self.keys, *self.container)
177
+ finally:
178
+ self.acquired = False
179
+
180
+ def __enter__(self):
181
+ """Sync context manager entry.
182
+
183
+ Raises:
184
+ TimeoutError: If lock cannot be acquired within timeout.
185
+
186
+ Returns:
187
+ AsyncRedisAtomicMultiLock: self
188
+ """
189
+ ok = self.sync_acquire()
190
+ if not ok:
191
+ raise TimeoutError(
192
+ f"Failed to acquire lock on keys {self.keys} in {self.blocking_timeout}s"
193
+ )
194
+ return self
195
+
196
+ def __exit__(self, exc_type, exc_val, exc_tb):
197
+ """Sync context manager exit."""
198
+ self.sync_release()
199
+ # </editor-fold>
200
+
201
+
202
+ __all__ = ["AsyncRedisAtomicMultiLock"]