smartinno 1.0.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.
redis_sdk/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .factory import RedisClientFactory
2
+ from .config import Architecture, UseCase, RedisConfig
3
+ from .clients.base import CircuitBreaker, CircuitState
4
+ from .communication_service import CommunicationService, communication_service
5
+
6
+ __all__ = [
7
+ "RedisClientFactory",
8
+ "Architecture",
9
+ "UseCase",
10
+ "RedisConfig",
11
+ "CircuitBreaker",
12
+ "CircuitState",
13
+ "CommunicationService",
14
+ "communication_service",
15
+ ]
File without changes
@@ -0,0 +1,405 @@
1
+ import time
2
+ import json
3
+ import logging
4
+ import uuid
5
+ from datetime import datetime
6
+ import functools
7
+ from enum import Enum
8
+ from typing import Any, List
9
+ from redis_sdk.interfaces import IRedisClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class CircuitState(Enum):
15
+ CLOSED = "closed" # Normal operation — requests pass through
16
+ OPEN = "open" # Failing — all requests are rejected immediately
17
+ HALF_OPEN = "half_open" # Recovery probe — one request allowed to test
18
+
19
+
20
+ class CircuitBreaker:
21
+ """
22
+ Built-in circuit breaker for Redis operations.
23
+
24
+ Tracks consecutive failures. When the failure threshold is exceeded the circuit
25
+ opens and all subsequent calls are rejected immediately for `recovery_timeout`
26
+ seconds. After that window a single probe request is allowed through; if it
27
+ succeeds the circuit closes again, otherwise it re-opens.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ failure_threshold: int = 5,
33
+ recovery_timeout: float = 30.0,
34
+ name: str = "redis",
35
+ ):
36
+ self.failure_threshold = failure_threshold
37
+ self.recovery_timeout = recovery_timeout
38
+ self.name = name
39
+
40
+ self._state = CircuitState.CLOSED
41
+ self._failure_count = 0
42
+ self._last_failure_time: float = 0.0
43
+
44
+ @property
45
+ def state(self) -> CircuitState:
46
+ if self._state == CircuitState.OPEN:
47
+ # Check if enough time has passed to move to HALF_OPEN
48
+ if time.monotonic() - self._last_failure_time >= self.recovery_timeout:
49
+ self._state = CircuitState.HALF_OPEN
50
+ logger.info(
51
+ f"Circuit breaker [{self.name}] transitioning OPEN -> HALF_OPEN"
52
+ )
53
+ return self._state
54
+
55
+ def record_success(self) -> None:
56
+ self._failure_count = 0
57
+ if self._state != CircuitState.CLOSED:
58
+ logger.info(f"Circuit breaker [{self.name}] closing (recovered)")
59
+ self._state = CircuitState.CLOSED
60
+
61
+ def record_failure(self) -> None:
62
+ self._failure_count += 1
63
+ self._last_failure_time = time.monotonic()
64
+ if self._failure_count >= self.failure_threshold:
65
+ self._state = CircuitState.OPEN
66
+ logger.warning(
67
+ f"Circuit breaker [{self.name}] OPEN after "
68
+ f"{self._failure_count} consecutive failures"
69
+ )
70
+
71
+ def allow_request(self) -> bool:
72
+ current = self.state
73
+ if current == CircuitState.CLOSED:
74
+ return True
75
+ if current == CircuitState.HALF_OPEN:
76
+ return True # Allow one probe
77
+ return False
78
+
79
+
80
+ def _circuit_protected(method):
81
+ """Decorator that wraps a Redis command with circuit breaker logic."""
82
+
83
+ @functools.wraps(method)
84
+ def wrapper(self, *args, **kwargs):
85
+ if not self._circuit_breaker.allow_request():
86
+ raise ConnectionError(
87
+ f"Circuit breaker [{self._circuit_breaker.name}] is OPEN — "
88
+ f"request rejected. Will retry after "
89
+ f"{self._circuit_breaker.recovery_timeout}s."
90
+ )
91
+ try:
92
+ result = method(self, *args, **kwargs)
93
+ self._circuit_breaker.record_success()
94
+ return result
95
+ except Exception:
96
+ self._circuit_breaker.record_failure()
97
+ raise
98
+
99
+ return wrapper
100
+
101
+
102
+ class BaseRedisAdapter(IRedisClient):
103
+ """
104
+ Base implementation that wraps standard redis commands with an integrated
105
+ circuit breaker. Requires self.client to be initialized by subclasses.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ failure_threshold: int = 5,
111
+ recovery_timeout: float = 30.0,
112
+ breaker_name: str = "redis",
113
+ service_name: str = "safari_pro",
114
+ ):
115
+ self.client = None
116
+ self.service_name = service_name
117
+ self._circuit_breaker = CircuitBreaker(
118
+ failure_threshold=failure_threshold,
119
+ recovery_timeout=recovery_timeout,
120
+ name=breaker_name,
121
+ )
122
+
123
+ # --- Data Standardization Utilities ---
124
+
125
+ def _format_key(self, key: str, action: str = "data") -> str:
126
+ """Enforces a standardized key naming convention: {service}:{action}:{key}"""
127
+ if not isinstance(key, str):
128
+ return key
129
+ if key.startswith(f"{self.service_name}:"):
130
+ return key
131
+ return f"{self.service_name}:{action}:{key}"
132
+
133
+ def _wrap_payload(self, payload: Any, event: str = "update", type_action: str = "cache") -> str:
134
+ """Wraps a payload into a standardized JSON envelope."""
135
+ data = {
136
+ "request_id": str(uuid.uuid4()),
137
+ "event": event,
138
+ "type": type_action,
139
+ "service": self.service_name,
140
+ "payload": payload,
141
+ "timestamp": datetime.utcnow().isoformat()
142
+ }
143
+ return json.dumps(data)
144
+
145
+ def _unwrap_payload(self, data: Any) -> Any:
146
+ """Unwraps a standardized JSON envelope, returning the original payload."""
147
+ if data is None:
148
+ return None
149
+
150
+ data_str = data
151
+ if isinstance(data, bytes):
152
+ try:
153
+ data_str = data.decode('utf-8')
154
+ except UnicodeDecodeError:
155
+ return data
156
+
157
+ if not isinstance(data_str, str):
158
+ return data
159
+
160
+ try:
161
+ obj = json.loads(data_str)
162
+ if isinstance(obj, dict) and "payload" in obj and "service" in obj:
163
+ return obj["payload"]
164
+ except json.JSONDecodeError:
165
+ pass
166
+
167
+ return data
168
+
169
+ # --- Explicitly wrapped commands (circuit-protected) ---
170
+
171
+ def _apply_ttl(self, key: str, ttl: int = None):
172
+ """Helper to apply the TTL to collections if explicitly requested."""
173
+ if ttl is not None:
174
+ try:
175
+ self.client.expire(key, ttl)
176
+ except Exception as e:
177
+ logger.warning(f"Failed to apply TTL to {key}: {e}")
178
+
179
+ @_circuit_protected
180
+ def set(self, key: str, value: Any, ex: int = None, **kwargs) -> bool:
181
+ if ex is None:
182
+ ex = 259200 # Default 3 days
183
+ f_key = self._format_key(key, "cache")
184
+ wrapped_val = self._wrap_payload(value, "set", "cache")
185
+ return self.client.set(f_key, wrapped_val, ex=ex, **kwargs)
186
+
187
+ @_circuit_protected
188
+ def get(self, key: str) -> Any:
189
+ f_key = self._format_key(key, "cache")
190
+ val = self.client.get(f_key)
191
+ return self._unwrap_payload(val)
192
+
193
+ @_circuit_protected
194
+ def mget(self, keys: List[str]) -> List[Any]:
195
+ f_keys = [self._format_key(k, "cache") for k in keys]
196
+ vals = self.client.mget(f_keys)
197
+ return [self._unwrap_payload(v) for v in vals]
198
+
199
+ @_circuit_protected
200
+ def publish(self, channel: str, message: Any) -> int:
201
+ f_channel = self._format_key(channel, "pubsub")
202
+ wrapped_msg = self._wrap_payload(message, "publish", "pubsub")
203
+ return self.client.publish(f_channel, wrapped_msg)
204
+
205
+ @_circuit_protected
206
+ def pipeline(self, transaction: bool = True) -> Any:
207
+ # Pipeline requires special handling at the caller level to wrap/unwrap
208
+ # since it queues commands. For now we return the native pipeline.
209
+ return self.client.pipeline(transaction=transaction)
210
+
211
+ @_circuit_protected
212
+ def xadd(self, name: str, fields: dict, maxlen: int = 10000, approximate: bool = True) -> str:
213
+ """
214
+ Adds a message to a stream. Ensures the stream is created if it does not exist.
215
+ Caps the stream length using maxlen (defaults to 10000) to prevent OOM.
216
+ """
217
+ f_name = self._format_key(name, "stream")
218
+ # For streams, wrap the entire fields dict into a payload field
219
+ wrapped_fields = {
220
+ "request_id": str(uuid.uuid4()),
221
+ "event": "update",
222
+ "type": "stream",
223
+ "service": self.service_name,
224
+ "payload": json.dumps(fields),
225
+ "timestamp": datetime.utcnow().isoformat()
226
+ }
227
+ res = self.client.xadd(
228
+ f_name,
229
+ wrapped_fields,
230
+ maxlen=maxlen,
231
+ approximate=approximate,
232
+ nomkstream=False # Ensures stream is created if it doesn't exist
233
+ )
234
+ return res
235
+
236
+ @_circuit_protected
237
+ def xread(self, streams: dict, count: int = None, block: int = None) -> List:
238
+ f_streams = {self._format_key(k, "stream"): v for k, v in streams.items()}
239
+ result = self.client.xread(f_streams, count=count, block=block)
240
+
241
+ # Unwrap
242
+ unwrapped = []
243
+ for stream_name, messages in result:
244
+ u_messages = []
245
+ for msg_id, fields in messages:
246
+ payload_val = fields.get(b"payload") or fields.get("payload")
247
+ if payload_val:
248
+ try:
249
+ u_fields = json.loads(payload_val)
250
+ except Exception:
251
+ u_fields = fields
252
+ else:
253
+ u_fields = fields
254
+ u_messages.append((msg_id, u_fields))
255
+
256
+ # Restore original stream name from input
257
+ original_stream_name = stream_name
258
+ prefix_b = f"{self.service_name}:stream:".encode('utf-8')
259
+ prefix_s = f"{self.service_name}:stream:"
260
+ if isinstance(stream_name, bytes) and stream_name.startswith(prefix_b):
261
+ original_stream_name = stream_name[len(prefix_b):].decode('utf-8')
262
+ elif isinstance(stream_name, str) and stream_name.startswith(prefix_s):
263
+ original_stream_name = stream_name[len(prefix_s):]
264
+
265
+ unwrapped.append((original_stream_name, u_messages))
266
+ return unwrapped
267
+
268
+ # --- Hashes ---
269
+ @_circuit_protected
270
+ def hset(self, name: str, key: str = None, value: str = None, mapping: dict = None, ttl: int = None) -> int:
271
+ f_name = self._format_key(name, "hash")
272
+
273
+ if mapping:
274
+ # Wrap all values in the mapping
275
+ wrapped_mapping = {k: self._wrap_payload(v, "hset", "hash") for k, v in mapping.items()}
276
+ res = self.client.hset(f_name, mapping=wrapped_mapping)
277
+ else:
278
+ wrapped_value = self._wrap_payload(value, "hset", "hash")
279
+ res = self.client.hset(f_name, key=key, value=wrapped_value)
280
+
281
+ self._apply_ttl(f_name, ttl)
282
+ return res
283
+
284
+ @_circuit_protected
285
+ def hget(self, name: str, key: str) -> Any:
286
+ f_name = self._format_key(name, "hash")
287
+ val = self.client.hget(f_name, key)
288
+ return self._unwrap_payload(val)
289
+
290
+ # --- Lists ---
291
+ @_circuit_protected
292
+ def lpush(self, name: str, *values, ttl: int = None) -> int:
293
+ f_name = self._format_key(name, "list")
294
+ wrapped_values = [self._wrap_payload(v, "lpush", "list") for v in values]
295
+ res = self.client.lpush(f_name, *wrapped_values)
296
+ self._apply_ttl(f_name, ttl)
297
+ return res
298
+
299
+ @_circuit_protected
300
+ def rpop(self, name: str) -> Any:
301
+ f_name = self._format_key(name, "list")
302
+ val = self.client.rpop(f_name)
303
+ return self._unwrap_payload(val)
304
+
305
+ # --- Sets ---
306
+ @_circuit_protected
307
+ def sadd(self, name: str, *values, ttl: int = None) -> int:
308
+ f_name = self._format_key(name, "set")
309
+ wrapped_values = [self._wrap_payload(v, "sadd", "set") for v in values]
310
+ res = self.client.sadd(f_name, *wrapped_values)
311
+ self._apply_ttl(f_name, ttl)
312
+ return res
313
+
314
+ @_circuit_protected
315
+ def smembers(self, name: str) -> set:
316
+ f_name = self._format_key(name, "set")
317
+ members = self.client.smembers(f_name)
318
+ return {self._unwrap_payload(m) for m in members}
319
+
320
+ # --- Sorted Sets ---
321
+ @_circuit_protected
322
+ def zadd(self, name: str, mapping: dict, ttl: int = None) -> int:
323
+ f_name = self._format_key(name, "zset")
324
+ # mapping is {member: score}
325
+ wrapped_mapping = {self._wrap_payload(k, "zadd", "zset"): v for k, v in mapping.items()}
326
+ res = self.client.zadd(f_name, wrapped_mapping)
327
+ self._apply_ttl(f_name, ttl)
328
+ return res
329
+
330
+ @_circuit_protected
331
+ def zrange(self, name: str, start: int, end: int, desc: bool = False, withscores: bool = False) -> List:
332
+ f_name = self._format_key(name, "zset")
333
+ res = self.client.zrange(f_name, start, end, desc=desc, withscores=withscores)
334
+ if withscores:
335
+ return [(self._unwrap_payload(member), score) for member, score in res]
336
+ return [self._unwrap_payload(member) for member in res]
337
+
338
+ # --- Geospatial ---
339
+ @_circuit_protected
340
+ def geoadd(self, name: str, values: tuple, ttl: int = None) -> int:
341
+ f_name = self._format_key(name, "geo")
342
+ # values is typically (longitude, latitude, member)
343
+ # We will format the key but NOT wrap the member to preserve geo exactness
344
+ res = self.client.geoadd(f_name, values)
345
+ self._apply_ttl(f_name, ttl)
346
+ return res
347
+
348
+ @_circuit_protected
349
+ def geosearch(self, name: str, longitude: float, latitude: float, radius: float, unit: str = 'km') -> List:
350
+ f_name = self._format_key(name, "geo")
351
+ return self.client.geosearch(f_name, longitude=longitude, latitude=latitude, radius=radius, unit=unit)
352
+
353
+ # --- Bitmaps & HyperLogLog ---
354
+ @_circuit_protected
355
+ def setbit(self, name: str, offset: int, value: int, ttl: int = None) -> int:
356
+ f_name = self._format_key(name, "bitmap")
357
+ res = self.client.setbit(f_name, offset, value)
358
+ self._apply_ttl(f_name, ttl)
359
+ return res
360
+
361
+ @_circuit_protected
362
+ def pfadd(self, name: str, *values, ttl: int = None) -> int:
363
+ f_name = self._format_key(name, "hll")
364
+ wrapped_values = [self._wrap_payload(v, "pfadd", "hll") for v in values]
365
+ res = self.client.pfadd(f_name, *wrapped_values)
366
+ self._apply_ttl(f_name, ttl)
367
+ return res
368
+
369
+ @_circuit_protected
370
+ def close(self) -> None:
371
+ self.client.close()
372
+
373
+ # --- Health / Status helpers ---
374
+
375
+ def get_circuit_state(self) -> str:
376
+ """Return the current circuit breaker state as a string."""
377
+ return self._circuit_breaker.state.value
378
+
379
+ def __getattr__(self, item):
380
+ """
381
+ Proxy any unknown method calls to the underlying Redis client.
382
+ This allows the adapter to seamlessly support all native Redis commands
383
+ (like hset, publish, expire, etc.) without having to explicitly wrap them.
384
+ Proxied calls are also circuit-breaker protected.
385
+ """
386
+ if hasattr(self, 'client') and self.client is not None and hasattr(self.client, item):
387
+ attr = getattr(self.client, item)
388
+ if callable(attr):
389
+ @functools.wraps(attr)
390
+ def _protected_proxy(*args, **kwargs):
391
+ if not self._circuit_breaker.allow_request():
392
+ raise ConnectionError(
393
+ f"Circuit breaker [{self._circuit_breaker.name}] is OPEN — "
394
+ f"request rejected."
395
+ )
396
+ try:
397
+ result = attr(*args, **kwargs)
398
+ self._circuit_breaker.record_success()
399
+ return result
400
+ except Exception:
401
+ self._circuit_breaker.record_failure()
402
+ raise
403
+ return _protected_proxy
404
+ return attr
405
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'")
@@ -0,0 +1,65 @@
1
+ from redis.cluster import RedisCluster, ClusterNode
2
+ from redis.backoff import ExponentialBackoff
3
+ from redis.retry import Retry
4
+ from redis_sdk.clients.base import BaseRedisAdapter
5
+ from redis_sdk.config import RedisConfig
6
+
7
+ class ClusterClientAdapter(BaseRedisAdapter):
8
+ """
9
+ Adapter for Native Redis Cluster Architecture.
10
+ """
11
+ def __init__(self, config: RedisConfig):
12
+ super().__init__(breaker_name="cluster", service_name=config.service_name)
13
+
14
+ nodes = []
15
+ if config.startup_nodes:
16
+ nodes = [ClusterNode(host=n['host'], port=n['port']) for n in config.startup_nodes]
17
+ else:
18
+ nodes = [ClusterNode(host=config.host, port=config.port)]
19
+
20
+ # Transparently remap internal Docker hostnames to Windows localhost with mapped ports
21
+ def remap_host_port(host, port):
22
+ node_map = {
23
+ 'redis-node-1': ('127.0.0.1', 7001),
24
+ 'redis-node-2': ('127.0.0.1', 7002),
25
+ 'redis-node-3': ('127.0.0.1', 7003),
26
+ 'redis-node-4': ('127.0.0.1', 7004),
27
+ 'redis-node-5': ('127.0.0.1', 7005),
28
+ 'redis-node-6': ('127.0.0.1', 7006),
29
+ 'redis-cluster-node-1': ('127.0.0.1', 7001),
30
+ 'redis-cluster-node-2': ('127.0.0.1', 7002),
31
+ 'redis-cluster-node-3': ('127.0.0.1', 7003),
32
+ 'redis-cluster-node-4': ('127.0.0.1', 7004),
33
+ 'redis-cluster-node-5': ('127.0.0.1', 7005),
34
+ 'redis-cluster-node-6': ('127.0.0.1', 7006),
35
+ }
36
+ return node_map.get(host, (host, port))
37
+
38
+ retry_strategy = None
39
+ if config.retry_backoff:
40
+ retry_strategy = Retry(
41
+ ExponentialBackoff(cap=config.retry_backoff_max, base=config.retry_backoff_min),
42
+ config.retry_backoff_retries
43
+ )
44
+
45
+ self.client = RedisCluster(
46
+ startup_nodes=nodes,
47
+ password=config.password,
48
+ decode_responses=True,
49
+ host_port_remap=remap_host_port,
50
+ ssl=config.ssl,
51
+ ssl_cert_reqs=config.ssl_cert_reqs if config.ssl else None,
52
+ max_connections=config.max_connections,
53
+ socket_timeout=config.socket_timeout,
54
+ socket_connect_timeout=config.socket_connect_timeout,
55
+ socket_keepalive=config.socket_keepalive,
56
+ health_check_interval=config.health_check_interval,
57
+ retry_on_timeout=config.retry_on_timeout,
58
+ retry=retry_strategy
59
+ )
60
+
61
+ def pipeline(self, transaction: bool = False):
62
+ """
63
+ Native Cluster supports pipelines, but cross-slot transactions are complex.
64
+ """
65
+ return self.client.pipeline(transaction=transaction)
@@ -0,0 +1,51 @@
1
+ import redis
2
+ from redis.backoff import ExponentialBackoff
3
+ from redis.retry import Retry
4
+ from redis_sdk.clients.base import BaseRedisAdapter
5
+ from redis_sdk.config import RedisConfig
6
+
7
+ class HybridClientAdapter(BaseRedisAdapter):
8
+ """
9
+ Adapter for Hybrid (Predixy Proxy + Cluster) Architecture.
10
+ Acts as a standard Redis client, but overrides pipeline behavior.
11
+ """
12
+ def __init__(self, config: RedisConfig):
13
+ super().__init__(breaker_name="hybrid", service_name=config.service_name)
14
+
15
+ retry_strategy = None
16
+ if config.retry_backoff:
17
+ retry_strategy = Retry(
18
+ ExponentialBackoff(cap=config.retry_backoff_max, base=config.retry_backoff_min),
19
+ config.retry_backoff_retries
20
+ )
21
+
22
+ self.client = redis.Redis(
23
+ host=config.host,
24
+ port=config.port,
25
+ password=config.password,
26
+ decode_responses=True,
27
+ protocol=2, # Predixy operates reliably on RESP2
28
+ ssl=config.ssl,
29
+ ssl_cert_reqs=config.ssl_cert_reqs if config.ssl else None,
30
+ max_connections=config.max_connections,
31
+ socket_timeout=config.socket_timeout,
32
+ socket_connect_timeout=config.socket_connect_timeout,
33
+ socket_keepalive=config.socket_keepalive,
34
+ health_check_interval=config.health_check_interval,
35
+ retry_on_timeout=config.retry_on_timeout,
36
+ retry=retry_strategy
37
+ )
38
+
39
+ def pipeline(self, transaction: bool = False):
40
+ """
41
+ Predixy Proxy DOES NOT support MULTI/EXEC transactions efficiently across a cluster via python pipelines.
42
+ We MUST force transaction=False to ensure Predixy routes pipelines sequentially based on hash tags.
43
+ """
44
+ original_pipeline = self.client.pipeline
45
+
46
+ def predixy_pipeline(transaction=False, shard_hint=None):
47
+ if transaction:
48
+ print("Notice: Hybrid Predixy Proxy does not support transactions in pipelines. Forcing transaction=False.")
49
+ return original_pipeline(transaction=False, shard_hint=shard_hint)
50
+
51
+ return predixy_pipeline(transaction=transaction)
@@ -0,0 +1,59 @@
1
+ from redis.sentinel import Sentinel
2
+ from redis.backoff import ExponentialBackoff
3
+ from redis.retry import Retry
4
+ from redis_sdk.clients.base import BaseRedisAdapter
5
+ from redis_sdk.config import RedisConfig
6
+
7
+ class SentinelClientAdapter(BaseRedisAdapter):
8
+ """
9
+ Adapter for Redis Sentinel Architecture.
10
+ """
11
+ def __init__(self, config: RedisConfig):
12
+ super().__init__(breaker_name="sentinel", service_name=config.service_name)
13
+
14
+ retry_strategy = None
15
+ if config.retry_backoff:
16
+ retry_strategy = Retry(
17
+ ExponentialBackoff(cap=config.retry_backoff_max, base=config.retry_backoff_min),
18
+ config.retry_backoff_retries
19
+ )
20
+
21
+ if config.sentinel_nodes:
22
+ # Traditional Sentinel connection (connects to individuals to find master)
23
+ master_name = config.sentinel_master_name or 'mymaster'
24
+ self.sentinel = Sentinel(
25
+ config.sentinel_nodes,
26
+ socket_timeout=config.socket_timeout,
27
+ password=config.password,
28
+ sentinel_kwargs={'password': config.password}
29
+ )
30
+ self.client = self.sentinel.master_for(
31
+ master_name,
32
+ decode_responses=True,
33
+ ssl=config.ssl,
34
+ ssl_cert_reqs=config.ssl_cert_reqs if config.ssl else None,
35
+ max_connections=config.max_connections,
36
+ socket_connect_timeout=config.socket_connect_timeout,
37
+ socket_keepalive=config.socket_keepalive,
38
+ health_check_interval=config.health_check_interval,
39
+ retry_on_timeout=config.retry_on_timeout,
40
+ retry=retry_strategy
41
+ )
42
+ else:
43
+ # Connect via HAProxy Load Balancer directly
44
+ import redis
45
+ self.client = redis.Redis(
46
+ host=config.host,
47
+ port=config.port,
48
+ password=config.password,
49
+ decode_responses=True,
50
+ ssl=config.ssl,
51
+ ssl_cert_reqs=config.ssl_cert_reqs if config.ssl else None,
52
+ max_connections=config.max_connections,
53
+ socket_timeout=config.socket_timeout,
54
+ socket_connect_timeout=config.socket_connect_timeout,
55
+ socket_keepalive=config.socket_keepalive,
56
+ health_check_interval=config.health_check_interval,
57
+ retry_on_timeout=config.retry_on_timeout,
58
+ retry=retry_strategy
59
+ )