redis-simplify 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.
@@ -0,0 +1,19 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+
4
+ def _get_version():
5
+ try:
6
+ from importlib.metadata import version
7
+ return version("redis-simplify")
8
+ except Exception:
9
+ # Fallback: lê do pyproject.toml
10
+ try:
11
+ pyproject = Path(__file__).parent.parent / "pyproject.toml"
12
+ if pyproject.exists():
13
+ data = tomllib.loads(pyproject.read_text())
14
+ return data["project"]["version"]
15
+ except Exception:
16
+ pass
17
+ return "0.1.0" # último recurso
18
+
19
+ __version__ = _get_version()
@@ -0,0 +1,323 @@
1
+ # app/core/redis_client.py
2
+ import redis
3
+ import os
4
+ import logging
5
+ from typing import Optional, Set, List
6
+ import json
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class RedisClient:
12
+ """Cliente Redis genérico SÍNCRONO - pode ser usado por qualquer parte do sistema"""
13
+
14
+ def __init__(
15
+ self,
16
+ host: str,
17
+ port: int = 6379,
18
+ password: Optional[str] = None,
19
+ db: int = 0,
20
+ decode_responses: bool = True,
21
+ socket_keepalive: bool = True,
22
+ health_check_interval: int = 30
23
+ ):
24
+ # Configuração obrigatória via parâmetros (não lê .env)
25
+ self.host = host
26
+ self.port = port
27
+ self.password = password
28
+ self.db = db
29
+
30
+ self.decode_responses = decode_responses
31
+ self.socket_keepalive = socket_keepalive
32
+ self.health_check_interval = health_check_interval
33
+
34
+ self.client: Optional[redis.Redis] = None
35
+ self._connect()
36
+
37
+ def _connect(self):
38
+ """Estabelece conexão com Redis (síncrona)"""
39
+ try:
40
+ self.client = redis.Redis(
41
+ host=self.host,
42
+ port=self.port,
43
+ password=self.password or None,
44
+ db=self.db,
45
+ decode_responses=self.decode_responses,
46
+ socket_keepalive=self.socket_keepalive,
47
+ health_check_interval=self.health_check_interval
48
+ )
49
+ # Testa conexão
50
+ self.client.ping()
51
+ logger.info(f"RedisClient síncrono conectado: {self.host}:{self.port}")
52
+ except Exception as e:
53
+ logger.error(f"Erro ao conectar Redis: {e}")
54
+ self.client = None
55
+
56
+ def _ensure_connection(self) -> bool:
57
+ """Verifica conexão e tenta reconectar se necessário"""
58
+ if self.client:
59
+ try:
60
+ self.client.ping()
61
+ return True
62
+ except Exception:
63
+ self.client = None
64
+
65
+ self._connect()
66
+ return self.client is not None
67
+
68
+ def ping(self) -> bool:
69
+ """Testa conectividade"""
70
+ if not self._ensure_connection():
71
+ return False
72
+ try:
73
+ return self.client.ping()
74
+ except Exception as e:
75
+ logger.error(f"Erro no ping: {e}")
76
+ return False
77
+
78
+
79
+
80
+ def scan(self, cursor: int = 0, match: Optional[str] = None, count: Optional[int] = None) -> tuple:
81
+ """
82
+ Implementação do comando SCAN do Redis
83
+ Retorna (nova_cursor, lista_de_chaves)
84
+ """
85
+ if not self._ensure_connection():
86
+ return 0, []
87
+ try:
88
+ # Redis-py retorna (cursor, keys)
89
+ return self.client.scan(cursor=cursor, match=match, count=count)
90
+ except Exception as e:
91
+ logger.error(f"Erro no scan: {e}")
92
+ return 0, []
93
+ # ========== OPERAÇÕES DE STRING ==========
94
+
95
+ def set(self, key: str, value: str, expire_seconds: Optional[int] = None) -> bool:
96
+ """Define valor de uma chave"""
97
+ if not self._ensure_connection():
98
+ return False
99
+ try:
100
+ if expire_seconds:
101
+ self.client.setex(key, expire_seconds, value)
102
+ else:
103
+ self.client.set(key, value)
104
+ return True
105
+ except Exception as e:
106
+ logger.error(f"Erro no set {key}: {e}")
107
+ return False
108
+
109
+ def get(self, key: str) -> Optional[str]:
110
+ """Obtém valor de uma chave"""
111
+ if not self._ensure_connection():
112
+ return None
113
+ try:
114
+ return self.client.get(key)
115
+ except Exception as e:
116
+ logger.error(f"Erro no get {key}: {e}")
117
+ return None
118
+
119
+ def delete(self, *keys: str) -> int:
120
+ """Deleta uma ou mais chaves"""
121
+ if not self._ensure_connection():
122
+ return 0
123
+ try:
124
+ return self.client.delete(*keys)
125
+ except Exception as e:
126
+ logger.error(f"Erro no delete {keys}: {e}")
127
+ return 0
128
+
129
+ def exists(self, key: str) -> bool:
130
+ """Verifica se chave existe"""
131
+ if not self._ensure_connection():
132
+ return False
133
+ try:
134
+ result = self.client.exists(key)
135
+ return bool(result)
136
+ except Exception as e:
137
+ logger.error(f"Erro no exists {key}: {e}")
138
+ return False
139
+
140
+ def expire(self, key: str, seconds: int) -> bool:
141
+ """Define tempo de expiração"""
142
+ if not self._ensure_connection():
143
+ return False
144
+ try:
145
+ result = self.client.expire(key, seconds)
146
+ return bool(result)
147
+ except Exception as e:
148
+ logger.error(f"Erro no expire {key}: {e}")
149
+ return False
150
+
151
+ def incr(self, key: str) -> int:
152
+ """Incrementa contador"""
153
+ if not self._ensure_connection():
154
+ return 0
155
+ try:
156
+ return self.client.incr(key)
157
+ except Exception as e:
158
+ logger.error(f"Erro no incr {key}: {e}")
159
+ return 0
160
+
161
+ def decr(self, key: str) -> int:
162
+ """Decrementa contador"""
163
+ if not self._ensure_connection():
164
+ return 0
165
+ try:
166
+ return self.client.decr(key)
167
+ except Exception as e:
168
+ logger.error(f"Erro no decr {key}: {e}")
169
+ return 0
170
+
171
+ # ========== OPERAÇÕES DE SET (CONJUNTOS) ==========
172
+
173
+ def sadd(self, key: str, *values: str) -> int:
174
+ """Adiciona valores a um set"""
175
+ if not self._ensure_connection():
176
+ return 0
177
+ try:
178
+ return self.client.sadd(key, *values)
179
+ except Exception as e:
180
+ logger.error(f"Erro no sadd {key}: {e}")
181
+ return 0
182
+
183
+ def srem(self, key: str, *values: str) -> int:
184
+ """Remove valores de um set"""
185
+ if not self._ensure_connection():
186
+ return 0
187
+ try:
188
+ return self.client.srem(key, *values)
189
+ except Exception as e:
190
+ logger.error(f"Erro no srem {key}: {e}")
191
+ return 0
192
+
193
+ def smembers(self, key: str) -> Set[str]:
194
+ """Retorna todos os membros de um set"""
195
+ if not self._ensure_connection():
196
+ return set()
197
+ try:
198
+ return self.client.smembers(key)
199
+ except Exception as e:
200
+ logger.error(f"Erro no smembers {key}: {e}")
201
+ return set()
202
+
203
+ def sismember(self, key: str, value: str) -> bool:
204
+ """Verifica se valor está no set"""
205
+ if not self._ensure_connection():
206
+ return False
207
+ try:
208
+ result = self.client.sismember(key, value)
209
+ return bool(result)
210
+ except Exception as e:
211
+ logger.error(f"Erro no sismember {key}: {e}")
212
+ return False
213
+
214
+ def scard(self, key: str) -> int:
215
+ """Retorna tamanho do set"""
216
+ if not self._ensure_connection():
217
+ return 0
218
+ try:
219
+ return self.client.scard(key)
220
+ except Exception as e:
221
+ logger.error(f"Erro no scard {key}: {e}")
222
+ return 0
223
+
224
+ # ========== OPERAÇÕES DE HASH ==========
225
+
226
+ def hset(self, key: str, field: str, value: str) -> int:
227
+ """Define campo em hash"""
228
+ if not self._ensure_connection():
229
+ return 0
230
+ try:
231
+ return self.client.hset(key, field, value)
232
+ except Exception as e:
233
+ logger.error(f"Erro no hset {key}: {e}")
234
+ return 0
235
+
236
+ def hget(self, key: str, field: str) -> Optional[str]:
237
+ """Obtém campo de hash"""
238
+ if not self._ensure_connection():
239
+ return None
240
+ try:
241
+ return self.client.hget(key, field)
242
+ except Exception as e:
243
+ logger.error(f"Erro no hget {key}: {e}")
244
+ return None
245
+
246
+ def hgetall(self, key: str) -> dict:
247
+ """Obtém todo hash"""
248
+ if not self._ensure_connection():
249
+ return {}
250
+ try:
251
+ return self.client.hgetall(key)
252
+ except Exception as e:
253
+ logger.error(f"Erro no hgetall {key}: {e}")
254
+ return {}
255
+
256
+ # ========== OPERAÇÕES DE LISTA ==========
257
+
258
+ def lpush(self, key: str, *values: str) -> int:
259
+ """Adiciona ao início da lista"""
260
+ if not self._ensure_connection():
261
+ return 0
262
+ try:
263
+ return self.client.lpush(key, *values)
264
+ except Exception as e:
265
+ logger.error(f"Erro no lpush {key}: {e}")
266
+ return 0
267
+
268
+ def rpush(self, key: str, *values: str) -> int:
269
+ """Adiciona ao final da lista"""
270
+ if not self._ensure_connection():
271
+ return 0
272
+ try:
273
+ return self.client.rpush(key, *values)
274
+ except Exception as e:
275
+ logger.error(f"Erro no rpush {key}: {e}")
276
+ return 0
277
+
278
+ def lrange(self, key: str, start: int, end: int) -> List[str]:
279
+ """Retorna range da lista"""
280
+ if not self._ensure_connection():
281
+ return []
282
+ try:
283
+ return self.client.lrange(key, start, end)
284
+ except Exception as e:
285
+ logger.error(f"Erro no lrange {key}: {e}")
286
+ return []
287
+
288
+ # ========== OPERAÇÕES COM JSON ==========
289
+
290
+ def set_json(self, key: str, data: dict, expire_seconds: Optional[int] = None) -> bool:
291
+ """Armazena dados JSON"""
292
+ return self.set(key, json.dumps(data), expire_seconds)
293
+
294
+ def get_json(self, key: str) -> Optional[dict]:
295
+ """Recupera dados JSON"""
296
+ value = self.get(key)
297
+ return json.loads(value) if value else None
298
+
299
+ # ========== OPERAÇÕES DE PIPELINE ==========
300
+
301
+ def pipeline(self):
302
+ """Retorna pipeline para operações em lote"""
303
+ if not self._ensure_connection():
304
+ return None
305
+ return self.client.pipeline()
306
+
307
+ # ========== OPERAÇÕES DE ADMIN ==========
308
+
309
+ def flush_all(self):
310
+ """Limpa tudo (CUIDADO: apenas para testes!)"""
311
+ if not self._ensure_connection():
312
+ return
313
+ try:
314
+ self.client.flushall()
315
+ logger.warning("Redis flushall executado!")
316
+ except Exception as e:
317
+ logger.error(f"Erro no flushall: {e}")
318
+
319
+ def close(self):
320
+ """Fecha conexão"""
321
+ if self.client:
322
+ self.client.close()
323
+ logger.info("Conexão Redis fechada")
@@ -0,0 +1,364 @@
1
+ Metadata-Version: 2.4
2
+ Name: redis-simplify
3
+ Version: 0.1.0
4
+ Summary: Wrapper de conveniência para Redis - conexão, JSON helpers e reconexão automática
5
+ Author-email: Paulo Ricardo Tebet Lyrio <tebetpaulo91@yahoo.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Paulouuul/redis-simplify
8
+ Project-URL: Repository, https://github.com/Paulouuul/redis-simplify
9
+ Project-URL: Issues, https://github.com/Paulouuul/redis-simplify/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: redis>=4.0.0
22
+
23
+ # redis-simplify
24
+
25
+ [![PyPI Version](https://img.shields.io/pypi/v/redis-simplify)](https://pypi.org/project/redis-simplify/)
26
+ [![Python Versions](https://img.shields.io/pypi/pyversions/redis-simplify)](https://pypi.org/project/redis-simplify/)
27
+ [![License](https://img.shields.io/pypi/l/redis-simplify)](LICENSE)
28
+
29
+ A lightweight synchronous convenience wrapper for Redis built on top of **redis-py**.
30
+
31
+ `redis-simplify` was created to reduce repetitive Redis boilerplate in Python applications by providing a simple and consistent interface with automatic reconnection, JSON helpers, centralized logging, and defensive error handling.
32
+
33
+ > This package is not a Redis client replacement. It is a convenience layer built on top of `redis-py` to simplify common Redis operations.
34
+
35
+ ---
36
+
37
+ ## Features
38
+
39
+ * Explicit Redis configuration (`host`, `port`, `password`, `db`)
40
+ * Does **not** automatically read `.env` files
41
+ * Automatic reconnection when Redis becomes unavailable
42
+ * Centralized logging and error handling
43
+ * JSON helpers for storing Python dictionaries
44
+ * Safe fallback values on failures
45
+ * Fully tested with `pytest`
46
+ * Lightweight implementation
47
+ * Synchronous API
48
+ * Support for the most commonly used Redis operations:
49
+
50
+ * Strings
51
+ * Sets
52
+ * Hashes
53
+ * Lists
54
+ * Pipelines
55
+ * SCAN iteration
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install redis-simplify
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Requirements
68
+
69
+ * Python >= 3.8
70
+ * redis-py >= 4.0.0
71
+
72
+ ---
73
+
74
+ ## Quick Start
75
+
76
+ ```python
77
+ from redis_simplify import RedisClient
78
+
79
+ client = RedisClient(
80
+ host="localhost",
81
+ port=6379
82
+ )
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Basic Usage
88
+
89
+ ### Strings
90
+
91
+ ```python
92
+ from redis_simplify import RedisClient
93
+
94
+ client = RedisClient(host="localhost", port=6379)
95
+
96
+ client.set("chave", "valor")
97
+
98
+ print(client.get("chave"))
99
+ ```
100
+
101
+ Output:
102
+
103
+ ```python
104
+ valor
105
+ ```
106
+
107
+ ---
108
+
109
+ ### JSON Helpers
110
+
111
+ Store Python dictionaries directly in Redis.
112
+
113
+ ```python
114
+ client.set_json(
115
+ "usuario:1",
116
+ {
117
+ "nome": "João",
118
+ "idade": 30
119
+ }
120
+ )
121
+
122
+ print(client.get_json("usuario:1"))
123
+ ```
124
+
125
+ Output:
126
+
127
+ ```python
128
+ {
129
+ "nome": "João",
130
+ "idade": 30
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ### Sets
137
+
138
+ ```python
139
+ client.sadd("tags", "python", "redis")
140
+
141
+ print(client.smembers("tags"))
142
+ ```
143
+
144
+ Possible output:
145
+
146
+ ```python
147
+ {"python", "redis"}
148
+ ```
149
+
150
+ ---
151
+
152
+ ### Connection Check
153
+
154
+ ```python
155
+ if client.ping():
156
+ print("Redis online")
157
+ else:
158
+ print("Redis unavailable")
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Automatic Reconnection
164
+
165
+ Before executing operations, the client verifies the connection status.
166
+
167
+ If Redis becomes unavailable, the wrapper automatically attempts to reconnect before executing the requested command.
168
+
169
+ This behavior is transparent to application code and helps reduce connection-management boilerplate.
170
+
171
+ ---
172
+
173
+ ## Error Handling
174
+
175
+ All operations include consistent exception handling and logging.
176
+
177
+ Instead of propagating Redis exceptions, the wrapper logs errors and returns safe fallback values whenever possible.
178
+
179
+ ### Fallback Values
180
+
181
+ | Return Type | Fallback |
182
+ | --------------- | -------- |
183
+ | Object / String | `None` |
184
+ | Boolean | `False` |
185
+ | Numeric | `0` |
186
+ | List | `[]` |
187
+ | Dictionary | `{}` |
188
+ | Set | `set()` |
189
+
190
+ This approach helps keep application code clean and reduces repetitive `try/except` blocks.
191
+
192
+ ---
193
+
194
+ ## Available Methods
195
+
196
+ ### Strings
197
+
198
+ | Method | Description |
199
+ | -------------------------------------- | ----------------------- |
200
+ | `set(key, value, expire_seconds=None)` | Set a value |
201
+ | `get(key)` | Retrieve a value |
202
+ | `delete(*keys)` | Delete one or more keys |
203
+ | `exists(key)` | Check if a key exists |
204
+ | `expire(key, seconds)` | Set expiration time |
205
+ | `incr(key)` | Increment a value |
206
+ | `decr(key)` | Decrement a value |
207
+
208
+ ---
209
+
210
+ ### JSON
211
+
212
+ | Method | Description |
213
+ | ------------------------------------------ | ----------------------------- |
214
+ | `set_json(key, data, expire_seconds=None)` | Store a dictionary as JSON |
215
+ | `get_json(key)` | Retrieve and deserialize JSON |
216
+
217
+ ---
218
+
219
+ ### Sets
220
+
221
+ | Method | Description |
222
+ | ----------------------- | -------------------- |
223
+ | `sadd(key, *values)` | Add members |
224
+ | `srem(key, *values)` | Remove members |
225
+ | `smembers(key)` | Retrieve all members |
226
+ | `sismember(key, value)` | Check membership |
227
+ | `scard(key)` | Count members |
228
+
229
+ ---
230
+
231
+ ### Hashes
232
+
233
+ | Method | Description |
234
+ | ------------------------- | ------------------- |
235
+ | `hset(key, field, value)` | Set a hash field |
236
+ | `hget(key, field)` | Retrieve a field |
237
+ | `hgetall(key)` | Retrieve all fields |
238
+
239
+ ---
240
+
241
+ ### Lists
242
+
243
+ | Method | Description |
244
+ | ------------------------- | ---------------------------- |
245
+ | `lpush(key, *values)` | Push values to the beginning |
246
+ | `rpush(key, *values)` | Push values to the end |
247
+ | `lrange(key, start, end)` | Retrieve a range of values |
248
+
249
+ ---
250
+
251
+ ### Utilities
252
+
253
+ | Method | Description |
254
+ | ---------------------------------------- | -------------------------- |
255
+ | `ping()` | Verify connectivity |
256
+ | `pipeline()` | Create a Redis pipeline |
257
+ | `scan(cursor=0, match=None, count=None)` | Iterate keys using SCAN |
258
+ | `flush_all()` | Remove all Redis databases |
259
+ | `close()` | Close the connection |
260
+
261
+ ---
262
+
263
+ ## Pipeline Example
264
+
265
+ ```python
266
+ pipe = client.pipeline()
267
+
268
+ pipe.set("user:1", "John")
269
+ pipe.set("user:2", "Jane")
270
+
271
+ pipe.execute()
272
+ ```
273
+
274
+ ---
275
+
276
+ ## SCAN Example
277
+
278
+ ```python
279
+ cursor = 0
280
+
281
+ while True:
282
+ cursor, keys = client.scan(
283
+ cursor=cursor,
284
+ match="user:*",
285
+ count=100
286
+ )
287
+
288
+ print(keys)
289
+
290
+ if cursor == 0:
291
+ break
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Shared Instance Pattern
297
+
298
+ `redis-simplify` does not enforce a Singleton pattern.
299
+
300
+ However, many applications create a single shared instance and reuse it throughout the project:
301
+
302
+ ```python
303
+ from redis_simplify import RedisClient
304
+
305
+ redis_client = RedisClient(
306
+ host="localhost",
307
+ port=6379
308
+ )
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Why redis-simplify?
314
+
315
+ Many projects repeatedly implement:
316
+
317
+ * Redis connection setup
318
+ * Health checks
319
+ * Reconnection logic
320
+ * JSON serialization and deserialization
321
+ * Logging
322
+ * Defensive exception handling
323
+
324
+ `redis-simplify` centralizes these concerns into a small reusable wrapper while preserving the familiar Redis workflow provided by `redis-py`.
325
+
326
+ ---
327
+
328
+ ## Running Tests
329
+
330
+ The project includes automated tests built with `pytest`.
331
+
332
+ ```bash
333
+ pytest
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Contributing
339
+
340
+ Contributions are welcome.
341
+
342
+ To contribute:
343
+
344
+ 1. Fork the repository
345
+ 2. Create a feature branch
346
+ 3. Make your changes
347
+ 4. Add or update tests when applicable
348
+ 5. Open a Pull Request
349
+
350
+ Bug reports, improvements, and feature suggestions are appreciated.
351
+
352
+ ---
353
+
354
+ ## License
355
+
356
+ This project is licensed under the MIT License.
357
+
358
+ ---
359
+
360
+ ## Author
361
+
362
+ **Paulo Ricardo Tebet Lyrio**
363
+
364
+ GitHub: https://github.com/Paulouuul/redis-simplify
@@ -0,0 +1,6 @@
1
+ redis_simplify/__init__.py,sha256=uAcB3kC0h5vr1_zKZx_GMa7lCY7MDOGKozjlsrbZtB0,584
2
+ redis_simplify/client.py,sha256=IYjt7UYmA69z-1A2JBvxoWByjO7r2SOtwjalSYjlUjM,10796
3
+ redis_simplify-0.1.0.dist-info/METADATA,sha256=SgLtV4BlfXTxOgswVYv5uPIzSGjC9NmeybaHDlAyvts,8620
4
+ redis_simplify-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ redis_simplify-0.1.0.dist-info/top_level.txt,sha256=5exHW7LkFNhYgELOqOPbubOiiSJfLdqJdpjZsPwPSRE,15
6
+ redis_simplify-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ redis_simplify