python3-commons 0.8.10__py3-none-any.whl → 0.8.11__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,249 @@
1
+ import logging
2
+ import socket
3
+ from platform import platform
4
+ from typing import Any, Mapping, Sequence
5
+
6
+ import valkey
7
+ from pydantic import RedisDsn
8
+ from valkey.asyncio import Valkey, StrictValkey, ConnectionPool, Sentinel
9
+ from valkey.asyncio.retry import Retry
10
+ from valkey.backoff import FullJitterBackoff
11
+
12
+ from python3_commons.conf import valkey_settings
13
+ from python3_commons.helpers import SingletonMeta
14
+ from python3_commons.serializers.msgspec import (
15
+ serialize_msgpack, deserialize_msgpack, deserialize_msgpack_native, serialize_msgpack_native
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class AsyncValkeyClient(metaclass=SingletonMeta):
22
+ def __init__(self, dsn: RedisDsn, sentinel_dsn: RedisDsn | None):
23
+ self._valkey_pool = None
24
+ self._valkey = None
25
+
26
+ if sentinel_dsn:
27
+ self._initialize_sentinel(sentinel_dsn)
28
+ else:
29
+ self._initialize_standard_pool(dsn)
30
+
31
+ @staticmethod
32
+ def _get_keepalive_options():
33
+ if platform == 'linux' or platform == 'darwin':
34
+ return {
35
+ socket.TCP_KEEPIDLE: 10,
36
+ socket.TCP_KEEPINTVL: 5,
37
+ socket.TCP_KEEPCNT: 5
38
+ }
39
+ else:
40
+ return {}
41
+
42
+ def _initialize_sentinel(self, dsn: RedisDsn):
43
+ sentinel = Sentinel(
44
+ [(dsn.host, dsn.port)],
45
+ socket_connect_timeout=10,
46
+ socket_timeout=60,
47
+ password=dsn.password,
48
+ sentinel_kwargs={'password': dsn.password}
49
+ )
50
+
51
+ ka_options = self._get_keepalive_options()
52
+
53
+ self._valkey = sentinel.master_for(
54
+ 'myprimary',
55
+ valkey_class=StrictValkey,
56
+ socket_connect_timeout=10,
57
+ socket_timeout=60,
58
+ health_check_interval=30,
59
+ retry_on_timeout=True,
60
+ retry=Retry(FullJitterBackoff(cap=5, base=1), 5),
61
+ socket_keepalive=True,
62
+ socket_keepalive_options=ka_options
63
+ )
64
+
65
+ def _initialize_standard_pool(self, dsn: RedisDsn):
66
+ self._valkey_pool = ConnectionPool.from_url(str(dsn))
67
+ self._valkey = StrictValkey(connection_pool=self._valkey_pool)
68
+
69
+ def get_client(self) -> Valkey:
70
+ return self._valkey
71
+
72
+
73
+ def get_valkey_client() -> Valkey:
74
+ return AsyncValkeyClient(valkey_settings.dsn, valkey_settings.sentinel_dsn).get_client()
75
+
76
+
77
+ async def delete(name: str):
78
+ await get_valkey_client().delete(name)
79
+
80
+
81
+ async def store_bytes(name: str, data: bytes, ttl: int = None, if_not_set: bool = False):
82
+ r = get_valkey_client()
83
+
84
+ return r.set(name, data, ex=ttl, nx=if_not_set)
85
+
86
+
87
+ async def get_bytes(name: str) -> bytes | None:
88
+ r = get_valkey_client()
89
+
90
+ return r.get(name)
91
+
92
+
93
+ async def store(name: str, obj: Any, ttl: int = None, if_not_set: bool = False):
94
+ return store_bytes(name, serialize_msgpack_native(obj), ttl, if_not_set)
95
+
96
+
97
+ async def get(name: str, default=None, data_type: Any = None) -> Any:
98
+ if data := await get_bytes(name):
99
+ return deserialize_msgpack_native(data, data_type)
100
+
101
+ return default
102
+
103
+
104
+ async def store_string(name: str, data: str, ttl: int = None):
105
+ await store_bytes(name, data.encode(), ttl)
106
+
107
+
108
+ async def get_string(name: str) -> str | None:
109
+ if data := await get_bytes(name):
110
+ return data.decode('utf-8')
111
+
112
+ return None
113
+
114
+
115
+ async def store_sequence(name: str, data: Sequence, ttl: int = None):
116
+ if data:
117
+ try:
118
+ r = get_valkey_client()
119
+ await r.rpush(name, *map(serialize_msgpack_native, data))
120
+
121
+ if ttl:
122
+ await r.expire(name, ttl)
123
+ except valkey.exceptions.ConnectionError as e:
124
+ logger.error(f'Failed to store sequence in cache: {e}')
125
+
126
+
127
+ async def get_sequence(name: str, _type: type = list) -> Sequence:
128
+ r = get_valkey_client()
129
+
130
+ return _type(map(deserialize_msgpack_native, r.lrange(name, 0, -1)))
131
+
132
+
133
+ async def store_dict(name: str, data: Mapping, ttl: int = None):
134
+ if data:
135
+ try:
136
+ r = get_valkey_client()
137
+ data = {k: serialize_msgpack_native(v) for k, v in data.items()}
138
+ await r.hset(name, mapping=data)
139
+
140
+ if ttl:
141
+ await r.expire(name, ttl)
142
+ except valkey.exceptions.ConnectionError as e:
143
+ logger.error(f'Failed to store dict in cache: {e}')
144
+
145
+
146
+ async def get_dict(name: str, value_data_type=None) -> dict | None:
147
+ r = get_valkey_client()
148
+
149
+ if data := r.hgetall(name):
150
+ data = {k.decode(): deserialize_msgpack(v, value_data_type) for k, v in data.items()}
151
+
152
+ return data
153
+
154
+ return None
155
+
156
+
157
+ async def set_dict(name: str, mapping: dict, ttl: int = None):
158
+ if mapping:
159
+ try:
160
+ r = get_valkey_client()
161
+ mapping = {str(k): serialize_msgpack(v) for k, v in mapping.items()}
162
+ await r.hset(name, mapping=mapping)
163
+
164
+ if ttl:
165
+ await r.expire(name, ttl)
166
+ except valkey.exceptions.ConnectionError as e:
167
+ logger.error(f'Failed to set dict in cache: {e}')
168
+
169
+
170
+ async def get_dict_item(name: str, key: str, data_type=None, default=None):
171
+ try:
172
+ r = get_valkey_client()
173
+
174
+ if data := await r.hget(name, key):
175
+ return deserialize_msgpack_native(data, data_type)
176
+
177
+ return default
178
+ except valkey.exceptions.ConnectionError as e:
179
+ logger.error(f'Failed to get dict item from cache: {e}')
180
+
181
+ return None
182
+
183
+
184
+ async def set_dict_item(name: str, key: str, obj: Any):
185
+ try:
186
+ r = get_valkey_client()
187
+ await r.hset(name, key, serialize_msgpack_native(obj))
188
+ except valkey.exceptions.ConnectionError as e:
189
+ logger.error(f'Failed to set dict item in cache: {e}')
190
+
191
+
192
+ async def delete_dict_item(name: str, *keys):
193
+ try:
194
+ r = get_valkey_client()
195
+ await r.hdel(name, *keys)
196
+ except valkey.exceptions.ConnectionError as e:
197
+ logger.error(f'Failed to delete dict item from cache: {e}')
198
+
199
+
200
+ async def store_set(name: str, value: set, ttl: int = None):
201
+ try:
202
+ r = get_valkey_client()
203
+ await r.sadd(name, *map(serialize_msgpack_native, value))
204
+
205
+ if ttl:
206
+ await r.expire(name, ttl)
207
+ except valkey.exceptions.ConnectionError as e:
208
+ logger.error(f'Failed to store set in cache: {e}')
209
+
210
+
211
+ async def has_set_item(name: str, value: str) -> bool:
212
+ try:
213
+ r = get_valkey_client()
214
+
215
+ return r.sismember(name, serialize_msgpack_native(value)) == 1
216
+ except valkey.exceptions.ConnectionError as e:
217
+ logger.error(f'Failed to check if set has item in cache: {e}')
218
+
219
+ return False
220
+
221
+
222
+ async def add_set_item(name: str, *values: str):
223
+ try:
224
+ r = get_valkey_client()
225
+ await r.sadd(name, *map(serialize_msgpack_native, values))
226
+ except valkey.exceptions.ConnectionError as e:
227
+ logger.error(f'Failed to add set item into cache: {e}')
228
+
229
+
230
+ async def delete_set_item(name: str, value: str):
231
+ r = get_valkey_client()
232
+ await r.srem(name, serialize_msgpack_native(value))
233
+
234
+
235
+ async def get_set_members(name: str) -> set[str] | None:
236
+ try:
237
+ r = get_valkey_client()
238
+
239
+ return set(map(deserialize_msgpack_native, r.smembers(name)))
240
+ except valkey.exceptions.ConnectionError as e:
241
+ logger.error(f'Failed to get set members from cache: {e}')
242
+
243
+ return None
244
+
245
+
246
+ async def exists(name: str) -> bool:
247
+ r = get_valkey_client()
248
+
249
+ return r.exists(name) == 1
python3_commons/conf.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import SecretStr, PostgresDsn, Field
1
+ from pydantic import SecretStr, PostgresDsn, Field, RedisDsn
2
2
  from pydantic_settings import BaseSettings, SettingsConfigDict
3
3
 
4
4
 
@@ -8,6 +8,13 @@ class CommonSettings(BaseSettings):
8
8
  logging_formatter: str = 'default'
9
9
 
10
10
 
11
+ class ValkeySettings(BaseSettings):
12
+ model_config = SettingsConfigDict(env_prefix='VALKEY_')
13
+
14
+ dsn: RedisDsn | None = None
15
+ sentinel_dsn: RedisDsn | None = None
16
+
17
+
11
18
  class DBSettings(BaseSettings):
12
19
  model_config = SettingsConfigDict(env_prefix='DB_')
13
20
 
@@ -31,5 +38,6 @@ class S3Settings(BaseSettings):
31
38
 
32
39
 
33
40
  settings = CommonSettings()
41
+ valkey_settings = ValkeySettings()
34
42
  db_settings = DBSettings()
35
43
  s3_settings = S3Settings()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.8.10
3
+ Version: 0.8.11
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License: gpl-3
@@ -23,6 +23,7 @@ Requires-Dist: msgspec~=0.19.0
23
23
  Requires-Dist: pydantic[email]~=2.11.0
24
24
  Requires-Dist: pydantic-settings~=2.8.1
25
25
  Requires-Dist: SQLAlchemy[asyncio]~=2.0.40
26
+ Requires-Dist: valkey[libvalkey]~=6.1.0
26
27
  Requires-Dist: zeep~=4.3.1
27
28
  Provides-Extra: testing
28
29
  Requires-Dist: pytest; extra == "testing"
@@ -1,7 +1,8 @@
1
1
  python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
2
  python3_commons/api_client.py,sha256=zj6zTF-DhUMu4bjj9VWLi63mVODG_SB47Q5qlmG-Sxg,4539
3
3
  python3_commons/audit.py,sha256=DMQ-nrWSs0qilD7wkz_8PV4jXcee75O8FgAm2YIuOiY,6256
4
- python3_commons/conf.py,sha256=jRk6kraRHVXsajmUPl0NjcZsH4_MgrN1IHlzdg06cSw,1015
4
+ python3_commons/cache.py,sha256=dBxoZw_LB52cAw_RI4KTM5WtJo0MtkhDw1Om_kCLCTU,7354
5
+ python3_commons/conf.py,sha256=Vu6eZ8uHiKFHe3OLdtImrUZe5JH4KH7HULwF5M4kY8U,1232
5
6
  python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
6
7
  python3_commons/helpers.py,sha256=OmuCF7UeJ6oe-rH1Y4ZYVW_uPQ973lCLsikj01wmNqs,3101
7
8
  python3_commons/object_storage.py,sha256=Bte49twIJ4l6VAzIqK6tLx7DFYwijErUmmhWkrZpSJE,4074
@@ -19,9 +20,9 @@ python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
19
20
  python3_commons/serializers/json.py,sha256=P288wWz9ic38QWEMrpp_uwKPYkQiOgvE1cI4WZn6ZCg,808
20
21
  python3_commons/serializers/msgpack.py,sha256=tzIGGyDL3UpZnnouCtnxuYDx6InKM_C3PP1N4PN8wd4,1269
21
22
  python3_commons/serializers/msgspec.py,sha256=FuZVqOLJb0-lEKrs7dtjhwEbHIpfMUk5yu1hD64zRdc,2038
22
- python3_commons-0.8.10.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
23
- python3_commons-0.8.10.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
24
- python3_commons-0.8.10.dist-info/METADATA,sha256=mV1IKo0huatH5mjlEfE7VF7b-Xxv5-XIp3S9PxdHtYQ,1150
25
- python3_commons-0.8.10.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
26
- python3_commons-0.8.10.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
27
- python3_commons-0.8.10.dist-info/RECORD,,
23
+ python3_commons-0.8.11.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
24
+ python3_commons-0.8.11.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
25
+ python3_commons-0.8.11.dist-info/METADATA,sha256=0jX6TPiS5aB8FK2DOS3aF5N0FbDuUFGnp5OlpUjDeDw,1190
26
+ python3_commons-0.8.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
27
+ python3_commons-0.8.11.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
28
+ python3_commons-0.8.11.dist-info/RECORD,,