pycacheable 0.1.1__tar.gz → 0.2.0__tar.gz

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 (27) hide show
  1. {pycacheable-0.1.1/src/pycacheable.egg-info → pycacheable-0.2.0}/PKG-INFO +33 -17
  2. {pycacheable-0.1.1 → pycacheable-0.2.0}/README.md +18 -13
  3. {pycacheable-0.1.1 → pycacheable-0.2.0}/pyproject.toml +21 -6
  4. pycacheable-0.2.0/src/pycacheable/backend_memory.py +79 -0
  5. pycacheable-0.2.0/src/pycacheable/backend_sqlite.py +99 -0
  6. pycacheable-0.2.0/src/pycacheable/cache_base.py +40 -0
  7. pycacheable-0.2.0/src/pycacheable/cacheable.py +105 -0
  8. pycacheable-0.2.0/src/pycacheable/serializers.py +50 -0
  9. {pycacheable-0.1.1 → pycacheable-0.2.0/src/pycacheable.egg-info}/PKG-INFO +33 -17
  10. {pycacheable-0.1.1 → pycacheable-0.2.0}/src/pycacheable.egg-info/SOURCES.txt +2 -0
  11. pycacheable-0.2.0/src/pycacheable.egg-info/requires.txt +6 -0
  12. pycacheable-0.2.0/tests/test_memory_cache.py +143 -0
  13. pycacheable-0.2.0/tests/test_serializers.py +111 -0
  14. pycacheable-0.2.0/tests/test_sqlite_cache.py +175 -0
  15. pycacheable-0.1.1/src/pycacheable/backend_memory.py +0 -48
  16. pycacheable-0.1.1/src/pycacheable/backend_sqlite.py +0 -70
  17. pycacheable-0.1.1/src/pycacheable/cache_base.py +0 -19
  18. pycacheable-0.1.1/src/pycacheable/cacheable.py +0 -53
  19. pycacheable-0.1.1/src/pycacheable.egg-info/requires.txt +0 -1
  20. pycacheable-0.1.1/tests/test_memory_cache.py +0 -49
  21. pycacheable-0.1.1/tests/test_sqlite_cache.py +0 -58
  22. {pycacheable-0.1.1 → pycacheable-0.2.0}/LICENSE +0 -0
  23. {pycacheable-0.1.1 → pycacheable-0.2.0}/setup.cfg +0 -0
  24. {pycacheable-0.1.1 → pycacheable-0.2.0}/src/pycacheable/__init__.py +0 -0
  25. {pycacheable-0.1.1 → pycacheable-0.2.0}/src/pycacheable/hashing.py +0 -0
  26. {pycacheable-0.1.1 → pycacheable-0.2.0}/src/pycacheable.egg-info/dependency_links.txt +0 -0
  27. {pycacheable-0.1.1 → pycacheable-0.2.0}/src/pycacheable.egg-info/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycacheable
3
- Version: 0.1.1
4
- Summary: Decorator de cache para funções e métodos Python, com backends InMemory e SQLite, TTL configurável e hash estável de parâmetros.
3
+ Version: 0.2.0
4
+ Summary: Decorator de cache para funções e métodos Python, com backends InMemory e SQLite, TTL configurável, hash estável de parâmetros e serialização segura.
5
5
  Author-email: Leonardo Pinho <contato@leonardopinho.com>
6
+ Maintainer-email: Leonardo Pinho <contato@leonardopinho.com>
6
7
  License: MIT License
7
8
 
8
9
  Copyright (c) 2025 Leonardo Pinho
@@ -28,27 +29,37 @@ License: MIT License
28
29
  Project-URL: Homepage, https://github.com/leonardopinho/pycacheable
29
30
  Project-URL: Repository, https://github.com/leonardopinho/pycacheable
30
31
  Project-URL: Issues, https://github.com/leonardopinho/pycacheable/issues
31
- Keywords: cache,decorator,sqlite,python,performance,memoization,pycharm
32
+ Project-URL: Changelog, https://github.com/leonardopinho/pycacheable/blob/main/CHANGES.md
33
+ Project-URL: Documentation, https://github.com/leonardopinho/pycacheable/blob/main/README.md
34
+ Project-URL: Benchmarks, https://github.com/leonardopinho/pycacheable/tree/main/benchmarks
35
+ Keywords: cache,decorator,sqlite,python,performance,memoization,caching
32
36
  Classifier: Development Status :: 4 - Beta
33
37
  Classifier: Intended Audience :: Developers
34
38
  Classifier: License :: OSI Approved :: MIT License
35
39
  Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3 :: Only
36
41
  Classifier: Programming Language :: Python :: 3.9
37
42
  Classifier: Programming Language :: Python :: 3.10
38
43
  Classifier: Programming Language :: Python :: 3.11
39
44
  Classifier: Programming Language :: Python :: 3.12
40
45
  Classifier: Topic :: Software Development :: Libraries
41
46
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
47
+ Classifier: Topic :: Utilities
48
+ Classifier: Topic :: Database
42
49
  Classifier: Operating System :: OS Independent
43
50
  Requires-Python: >=3.9
44
51
  Description-Content-Type: text/markdown
45
52
  License-File: LICENSE
46
- Requires-Dist: typing-extensions>=4.0.0
53
+ Provides-Extra: dev
54
+ Requires-Dist: build>=1.2.0; extra == "dev"
55
+ Requires-Dist: twine>=5.0.0; extra == "dev"
56
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
57
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
47
58
  Dynamic: license-file
48
59
 
49
60
  # PyCacheable
50
61
 
51
- Decorator de cache para métodos e funções Python com backends em memória e SQLite — serialização automática, hash estável de parâmetros, suporte a instância/estado e arquitetura plugável.
62
+ Decorator de cache para métodos e funções Python com backends em memória e SQLite — serialização automática com estratégia JSON-first + pickle fallback, hash estável de parâmetros, suporte a instância/estado e arquitetura plugável.
52
63
 
53
64
  ---
54
65
 
@@ -74,6 +85,9 @@ A biblioteca fornece:
74
85
  - `InMemoryCache`: cache volátil em memória com LRU + TTL.
75
86
  - `SQLiteCache`: cache persistente em disco (SQLite) com TTL, ideal para entre execuções ou processos;
76
87
  - Logs claros de fluxo: HIT / MISS / EXPIRE — permitindo entender se o cache está funcionando;
88
+ - Serialização segura:
89
+ - JSON-first para estruturas simples (seguro, sem risco de RCE)
90
+ - Pickle fallback para objetos complexos (flexível)
77
91
  - Métodos auxiliares:
78
92
  - `.cache_clear()`, `.cache_info()` no wrapper para inspeção/manutenção;
79
93
 
@@ -110,11 +124,12 @@ u2 = repo.get_user(42) # HIT → retorna cache, consulta não é executada
110
124
 
111
125
  ## Benefícios
112
126
 
113
- - Menor latência em chamadas repetidas (hit quase instantâneo).
114
- - Menor carga no banco/serviço, menos I/O repetido.
115
- - Persistência local (via SQLite) permite cache entre reinícios/processos.
116
- - Transparente para o usuário da função — apenas aplicar o decorator.
127
+ - Menor latência em chamadas repetidas (hit quase instantâneo).
128
+ - Menor carga no banco/serviço, menos I/O repetido.
129
+ - Persistência local (via SQLite) permite cache entre reinícios/processos.
130
+ - Transparente para o usuário da função — apenas aplicar o decorator.
117
131
  - Logs e métricas ajudam a monitorar impacto real.
132
+ - Serialização segura com JSON-first (sem risco de arbitrary code execution).
118
133
 
119
134
  ---
120
135
 
@@ -129,10 +144,11 @@ u2 = repo.get_user(42) # HIT → retorna cache, consulta não é executada
129
144
 
130
145
  ## Considerações e limites
131
146
 
132
- - O cache evita reexecuções **somente** se os parâmetros para o método forem os mesmos e serializáveis.
133
- - Se o método depende de estados mutáveis fora dos parâmetros (ex.: `self.some_state`), você deve usar `include_self=True` ou custom `key_fn`.
134
- - TTL é usado para expiração — resultados podem ficar stale se parâmetros ou contexto mudarem sem mudar a chave.
135
- - Embora o backend SQLite seja persistente, ele **não substitui** um cache distribuído (ex.: Redis) em cenários multiprocesso/semidistribuídos.
147
+ - O cache evita reexecuções **somente** se os parâmetros para o método forem os mesmos e serializáveis.
148
+ - Se o método depende de estados mutáveis fora dos parâmetros (ex.: `self.some_state`), você deve usar `include_self=True` ou custom `key_fn`.
149
+ - TTL é usado para expiração — resultados podem ficar "stale" se parâmetros ou contexto mudarem sem mudar a chave.
150
+ - Embora o backend SQLite seja persistente, ele **não substitui** um cache distribuído (ex.: Redis) em cenários multi-processo/semi-distribuídos.
151
+ - Pickle fallback mantém compatibilidade com objetos complexos, mas use com dados confiáveis.
136
152
 
137
153
  ---
138
154
 
@@ -142,11 +158,11 @@ Veja resultados reais que medem MISS vs HIT:
142
158
 
143
159
  | Backend | MISS (s) | HIT (s) | Speedup | Calls |
144
160
  |----------|-----------|----------|----------|--------|
145
- | RAW | 0.6827 | — | — | — |
146
- | InMemory | 0.6630 | 0.000113 | ~5 870× | 1 |
147
- | SQLite | 0.7157 | 0.000098 | ~7 300× | 1 |
161
+ | RAW | 0.4410 | — | — | — |
162
+ | InMemory | 0.4043 | 0.000043 | ~9,494x | 1 |
163
+ | SQLite | 0.4001 | 0.000763 | ~524x | 1 |
148
164
 
149
- O cache reduz o tempo de execução de ~0.68 s para ~0.0001 s — um **speedup superior a 5 000×**.
165
+ O cache reduz o tempo de execução de ~0.4 s para ~0.00004 s — um **speedup superior a 9.000×**.
150
166
 
151
167
  ---
152
168
 
@@ -1,6 +1,6 @@
1
1
  # PyCacheable
2
2
 
3
- Decorator de cache para métodos e funções Python com backends em memória e SQLite — serialização automática, hash estável de parâmetros, suporte a instância/estado e arquitetura plugável.
3
+ Decorator de cache para métodos e funções Python com backends em memória e SQLite — serialização automática com estratégia JSON-first + pickle fallback, hash estável de parâmetros, suporte a instância/estado e arquitetura plugável.
4
4
 
5
5
  ---
6
6
 
@@ -26,6 +26,9 @@ A biblioteca fornece:
26
26
  - `InMemoryCache`: cache volátil em memória com LRU + TTL.
27
27
  - `SQLiteCache`: cache persistente em disco (SQLite) com TTL, ideal para entre execuções ou processos;
28
28
  - Logs claros de fluxo: HIT / MISS / EXPIRE — permitindo entender se o cache está funcionando;
29
+ - Serialização segura:
30
+ - JSON-first para estruturas simples (seguro, sem risco de RCE)
31
+ - Pickle fallback para objetos complexos (flexível)
29
32
  - Métodos auxiliares:
30
33
  - `.cache_clear()`, `.cache_info()` no wrapper para inspeção/manutenção;
31
34
 
@@ -62,11 +65,12 @@ u2 = repo.get_user(42) # HIT → retorna cache, consulta não é executada
62
65
 
63
66
  ## Benefícios
64
67
 
65
- - Menor latência em chamadas repetidas (hit quase instantâneo).
66
- - Menor carga no banco/serviço, menos I/O repetido.
67
- - Persistência local (via SQLite) permite cache entre reinícios/processos.
68
- - Transparente para o usuário da função — apenas aplicar o decorator.
68
+ - Menor latência em chamadas repetidas (hit quase instantâneo).
69
+ - Menor carga no banco/serviço, menos I/O repetido.
70
+ - Persistência local (via SQLite) permite cache entre reinícios/processos.
71
+ - Transparente para o usuário da função — apenas aplicar o decorator.
69
72
  - Logs e métricas ajudam a monitorar impacto real.
73
+ - Serialização segura com JSON-first (sem risco de arbitrary code execution).
70
74
 
71
75
  ---
72
76
 
@@ -81,10 +85,11 @@ u2 = repo.get_user(42) # HIT → retorna cache, consulta não é executada
81
85
 
82
86
  ## Considerações e limites
83
87
 
84
- - O cache evita reexecuções **somente** se os parâmetros para o método forem os mesmos e serializáveis.
85
- - Se o método depende de estados mutáveis fora dos parâmetros (ex.: `self.some_state`), você deve usar `include_self=True` ou custom `key_fn`.
86
- - TTL é usado para expiração — resultados podem ficar stale se parâmetros ou contexto mudarem sem mudar a chave.
87
- - Embora o backend SQLite seja persistente, ele **não substitui** um cache distribuído (ex.: Redis) em cenários multiprocesso/semidistribuídos.
88
+ - O cache evita reexecuções **somente** se os parâmetros para o método forem os mesmos e serializáveis.
89
+ - Se o método depende de estados mutáveis fora dos parâmetros (ex.: `self.some_state`), você deve usar `include_self=True` ou custom `key_fn`.
90
+ - TTL é usado para expiração — resultados podem ficar "stale" se parâmetros ou contexto mudarem sem mudar a chave.
91
+ - Embora o backend SQLite seja persistente, ele **não substitui** um cache distribuído (ex.: Redis) em cenários multi-processo/semi-distribuídos.
92
+ - Pickle fallback mantém compatibilidade com objetos complexos, mas use com dados confiáveis.
88
93
 
89
94
  ---
90
95
 
@@ -94,11 +99,11 @@ Veja resultados reais que medem MISS vs HIT:
94
99
 
95
100
  | Backend | MISS (s) | HIT (s) | Speedup | Calls |
96
101
  |----------|-----------|----------|----------|--------|
97
- | RAW | 0.6827 | — | — | — |
98
- | InMemory | 0.6630 | 0.000113 | ~5 870× | 1 |
99
- | SQLite | 0.7157 | 0.000098 | ~7 300× | 1 |
102
+ | RAW | 0.4410 | — | — | — |
103
+ | InMemory | 0.4043 | 0.000043 | ~9,494x | 1 |
104
+ | SQLite | 0.4001 | 0.000763 | ~524x | 1 |
100
105
 
101
- O cache reduz o tempo de execução de ~0.68 s para ~0.0001 s — um **speedup superior a 5 000×**.
106
+ O cache reduz o tempo de execução de ~0.4 s para ~0.00004 s — um **speedup superior a 9.000×**.
102
107
 
103
108
  ---
104
109
 
@@ -1,36 +1,51 @@
1
1
  [project]
2
2
  name = "pycacheable"
3
- version = "0.1.1"
4
- description = "Decorator de cache para funções e métodos Python, com backends InMemory e SQLite, TTL configurável e hash estável de parâmetros."
5
- readme = "README.md"
3
+ version = "0.2.0"
4
+ description = "Decorator de cache para funções e métodos Python, com backends InMemory e SQLite, TTL configurável, hash estável de parâmetros e serialização segura."
5
+ readme = { file = "README.md", content-type = "text/markdown" }
6
6
  authors = [
7
7
  { name = "Leonardo Pinho", email = "contato@leonardopinho.com" }
8
8
  ]
9
+ maintainers = [
10
+ { name = "Leonardo Pinho", email = "contato@leonardopinho.com" }
11
+ ]
9
12
  license = { file = "LICENSE" }
10
13
  requires-python = ">=3.9"
11
- keywords = ["cache", "decorator", "sqlite", "python", "performance", "memoization", "pycharm"]
14
+ keywords = ["cache", "decorator", "sqlite", "python", "performance", "memoization", "caching"]
12
15
  classifiers = [
13
16
  "Development Status :: 4 - Beta",
14
17
  "Intended Audience :: Developers",
15
18
  "License :: OSI Approved :: MIT License",
16
19
  "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
17
21
  "Programming Language :: Python :: 3.9",
18
22
  "Programming Language :: Python :: 3.10",
19
23
  "Programming Language :: Python :: 3.11",
20
24
  "Programming Language :: Python :: 3.12",
21
25
  "Topic :: Software Development :: Libraries",
22
26
  "Topic :: Software Development :: Libraries :: Python Modules",
27
+ "Topic :: Utilities",
28
+ "Topic :: Database",
23
29
  "Operating System :: OS Independent",
24
30
  ]
25
31
 
26
- dependencies = [
27
- "typing-extensions >=4.0.0",
32
+ dependencies = []
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "build>=1.2.0",
37
+ "twine>=5.0.0",
38
+ "ruff>=0.5.0",
39
+ "pytest>=8.0.0",
28
40
  ]
29
41
 
30
42
  [project.urls]
31
43
  Homepage = "https://github.com/leonardopinho/pycacheable"
32
44
  Repository = "https://github.com/leonardopinho/pycacheable"
33
45
  Issues = "https://github.com/leonardopinho/pycacheable/issues"
46
+ Changelog = "https://github.com/leonardopinho/pycacheable/blob/main/CHANGES.md"
47
+ Documentation = "https://github.com/leonardopinho/pycacheable/blob/main/README.md"
48
+ Benchmarks = "https://github.com/leonardopinho/pycacheable/tree/main/benchmarks"
34
49
 
35
50
  [build-system]
36
51
  requires = ["setuptools>=68.0", "wheel"]
@@ -0,0 +1,79 @@
1
+ """Cache em memória com LRU e TTL."""
2
+ import threading
3
+ import time
4
+ from collections import OrderedDict
5
+ from typing import Any, Dict, Optional, Tuple
6
+
7
+ from .cache_base import CacheBackend
8
+ from .serializers import SafeSerializer
9
+
10
+
11
+ class InMemoryCache(CacheBackend):
12
+ """Cache em memória com LRU e TTL, thread-safe."""
13
+
14
+ def __init__(self, max_entries: int = 1024, serializer=None):
15
+ """
16
+ Args:
17
+ max_entries: Máximo de itens antes de evicção LRU
18
+ serializer: Instância de serializer (padrão: SafeSerializer)
19
+ """
20
+ self._store: OrderedDict[str, Tuple[float, bytes]] = OrderedDict()
21
+ self._lock = threading.RLock()
22
+ self._max_entries = max_entries
23
+ self._serializer = serializer or SafeSerializer()
24
+
25
+ def get(self, key: str) -> Tuple[bool, Any, str]:
26
+ with self._lock:
27
+ entry = self._store.get(key)
28
+ now = time.time()
29
+
30
+ if not entry:
31
+ return False, None, "MISS"
32
+
33
+ expire_at, payload = entry
34
+
35
+ # Verifica expiração
36
+ if expire_at and expire_at < now:
37
+ self._store.pop(key, None)
38
+ return False, None, "EXPIRE"
39
+
40
+ # Move para fim (LRU)
41
+ self._store.move_to_end(key)
42
+
43
+ # Desserializa
44
+ try:
45
+ value = self._serializer.loads(payload)
46
+ return True, value, "HIT"
47
+ except Exception:
48
+ # Se falhar na desserialização, remove do cache
49
+ self._store.pop(key, None)
50
+ return False, None, "DESERIALIZE_ERROR"
51
+
52
+ def set(self, key: str, value: Any, ttl_seconds: Optional[int]) -> None:
53
+ with self._lock:
54
+ expire_at = (time.time() + ttl_seconds) if ttl_seconds else 0.0
55
+
56
+ try:
57
+ payload = self._serializer.dumps(value)
58
+ except Exception:
59
+ # Se falhar na serialização, pula (não armazena)
60
+ return
61
+
62
+ self._store[key] = (expire_at, payload)
63
+ self._store.move_to_end(key)
64
+
65
+ # Evicção LRU
66
+ while len(self._store) > self._max_entries:
67
+ self._store.popitem(last=False)
68
+
69
+ def clear(self) -> None:
70
+ with self._lock:
71
+ self._store.clear()
72
+
73
+ def info(self) -> Dict[str, Any]:
74
+ with self._lock:
75
+ return {
76
+ "backend": "InMemoryCache",
77
+ "size": len(self._store),
78
+ "max_entries": self._max_entries,
79
+ }
@@ -0,0 +1,99 @@
1
+ """Cache persistente em SQLite com TTL."""
2
+ import os
3
+ import sqlite3
4
+ import threading
5
+ import time
6
+ from typing import Any, Dict, Optional, Tuple
7
+
8
+ from .cache_base import CacheBackend
9
+ from .serializers import SafeSerializer
10
+
11
+
12
+ class SQLiteCache(CacheBackend):
13
+ """Cache persistente em SQLite com TTL, thread-safe."""
14
+
15
+ def __init__(self, path: str, serializer=None):
16
+ """
17
+ Args:
18
+ path: Caminho para arquivo SQLite
19
+ serializer: Instância de serializer (padrão: SafeSerializer)
20
+ """
21
+ self._path = path
22
+ self._lock = threading.RLock()
23
+ self._serializer = serializer or SafeSerializer()
24
+
25
+ # Cria diretório se não existir
26
+ dir_path = os.path.dirname(os.path.abspath(path))
27
+ if dir_path:
28
+ os.makedirs(dir_path, exist_ok=True)
29
+
30
+ # Inicializa banco
31
+ with self._conn() as conn:
32
+ conn.execute("""
33
+ CREATE TABLE IF NOT EXISTS cache (
34
+ k TEXT PRIMARY KEY,
35
+ expire_at REAL,
36
+ v BLOB
37
+ )
38
+ """)
39
+ conn.execute("PRAGMA journal_mode=WAL;")
40
+
41
+ def _conn(self):
42
+ """Cria nova conexão (autocommit mode)."""
43
+ return sqlite3.connect(self._path, timeout=30, isolation_level=None)
44
+
45
+ def get(self, key: str) -> Tuple[bool, Any, str]:
46
+ with self._lock, self._conn() as conn:
47
+ row = conn.execute(
48
+ "SELECT expire_at, v FROM cache WHERE k=?", (key,)
49
+ ).fetchone()
50
+
51
+ now = time.time()
52
+
53
+ if not row:
54
+ return False, None, "MISS"
55
+
56
+ expire_at, blob = row
57
+
58
+ # Verifica expiração
59
+ if expire_at and expire_at < now:
60
+ conn.execute("DELETE FROM cache WHERE k=?", (key,))
61
+ return False, None, "EXPIRE"
62
+
63
+ # Desserializa
64
+ try:
65
+ value = self._serializer.loads(blob)
66
+ return True, value, "HIT"
67
+ except Exception:
68
+ # Se falhar na desserialização, remove do cache
69
+ conn.execute("DELETE FROM cache WHERE k=?", (key,))
70
+ return False, None, "DESERIALIZE_ERROR"
71
+
72
+ def set(self, key: str, value: Any, ttl_seconds: Optional[int]) -> None:
73
+ expire_at = (time.time() + ttl_seconds) if ttl_seconds else 0.0
74
+
75
+ try:
76
+ blob = self._serializer.dumps(value)
77
+ except Exception:
78
+ # Se falhar na serialização, pula
79
+ return
80
+
81
+ with self._lock, self._conn() as conn:
82
+ conn.execute(
83
+ "REPLACE INTO cache (k, expire_at, v) VALUES (?, ?, ?)",
84
+ (key, expire_at, sqlite3.Binary(blob))
85
+ )
86
+
87
+ def clear(self) -> None:
88
+ with self._lock, self._conn() as conn:
89
+ conn.execute("DELETE FROM cache")
90
+
91
+ def info(self) -> Dict[str, Any]:
92
+ with self._lock, self._conn() as conn:
93
+ row = conn.execute("SELECT COUNT(*) FROM cache").fetchone()
94
+ size = int(row[0]) if row else 0
95
+ return {
96
+ "backend": "SQLiteCache",
97
+ "size": size,
98
+ "path": self._path,
99
+ }
@@ -0,0 +1,40 @@
1
+ """Interface abstrata para implementações de cache."""
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Dict, Optional, Tuple
4
+
5
+
6
+ class CacheBackend(ABC):
7
+ """Interface clara e tipada para backends de cache."""
8
+
9
+ @abstractmethod
10
+ def get(self, key: str) -> Tuple[bool, Any, str]:
11
+ """
12
+ Recupera um valor do cache.
13
+
14
+ Retorna:
15
+ (hit: bool, value: Any, status: str)
16
+ - hit: True se encontrou, não expirou e desserializou com sucesso
17
+ - value: o valor cacheado (None se miss/expire/erro)
18
+ - status: "HIT", "MISS", "EXPIRE", ou "DESERIALIZE_ERROR"
19
+ """
20
+ pass
21
+
22
+ @abstractmethod
23
+ def set(self, key: str, value: Any, ttl_seconds: Optional[int]) -> None:
24
+ """Armazena um valor no cache com TTL opcional."""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def clear(self) -> None:
29
+ """Remove todos os itens do cache."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def info(self) -> Dict[str, Any]:
34
+ """Retorna informações sobre o estado do cache."""
35
+ pass
36
+
37
+
38
+ # Alias para compatibilidade com código existente
39
+ CacheBase = CacheBackend
40
+
@@ -0,0 +1,105 @@
1
+ """Decorator de cache com backends plugáveis."""
2
+ import functools
3
+ from typing import Any, Callable, Dict, Optional, Tuple
4
+
5
+ from .backend_sqlite import SQLiteCache
6
+ from .cache_base import CacheBackend
7
+ from .hashing import _build_key_from_call
8
+
9
+
10
+ def cacheable(
11
+ ttl: Optional[int] = 300,
12
+ backend: Optional[CacheBackend] = None,
13
+ *,
14
+ include_self: bool = False,
15
+ key_fn: Optional[Callable[[Callable, Tuple[Any, ...], Dict[str, Any]], str]] = None,
16
+ logger: Optional[Callable[[str], None]] = None,
17
+ backend_factory: Optional[Callable[..., CacheBackend]] = None,
18
+ ):
19
+ """
20
+ Decorator de cache com backend configurável.
21
+
22
+ Ciclo de vida do backend (ordem de prioridade):
23
+ 1. Se backend_factory fornecido: chamado por chamada (per-instance ou per-param)
24
+ 2. Se wrapper.cache_backend foi atribuído externamente: usa esse override
25
+ 3. Se backend fornecido: usa esse (compartilhado entre chamadas)
26
+ 4. Padrão: cria SQLiteCache novo
27
+
28
+ Args:
29
+ ttl: Tempo de vida em segundos (None = sem expiração)
30
+ backend: Instância de CacheBackend compartilhada
31
+ include_self: Se True, inclui 'self' na chave (para estado de instância)
32
+ key_fn: Função customizada de geração de chave
33
+ logger: Função para log de eventos cache
34
+ backend_factory: Callable que retorna CacheBackend por chamada
35
+
36
+ Exemplo:
37
+ >>> @cacheable(ttl=60, backend=InMemoryCache())
38
+ ... def get_user(user_id: int):
39
+ ... return fetch_from_db(user_id)
40
+
41
+ >>> # Override backend em teste:
42
+ >>> get_user.cache_backend = test_backend
43
+ """
44
+ # Resolve backend padrão uma única vez
45
+ default_backend = backend or SQLiteCache(path="./.cache/db.sqlite3")
46
+
47
+ def decorator(func: Callable):
48
+ @functools.wraps(func)
49
+ def wrapper(*args, **kwargs):
50
+ # Resolve qual backend usar (com fallback chain)
51
+ be = None
52
+
53
+ if backend_factory is not None:
54
+ try:
55
+ be = backend_factory(*args, **kwargs)
56
+ except TypeError:
57
+ # Fallback: tenta com first positional arg (self em métodos)
58
+ try:
59
+ be = backend_factory(args[0]) if args else None
60
+ except Exception:
61
+ pass
62
+
63
+ # Se factory falhou ou não existe, usa cache_backend ou padrão
64
+ if be is None:
65
+ be = getattr(wrapper, "cache_backend", default_backend)
66
+
67
+ # Gera chave estável
68
+ key = (
69
+ key_fn(func, args, kwargs)
70
+ if key_fn
71
+ else _build_key_from_call(func, args, kwargs, include_self)
72
+ )
73
+
74
+ # Tenta obter do cache
75
+ hit, value, status = be.get(key)
76
+ if hit:
77
+ if logger:
78
+ logger(f"[CACHE {status}] {func.__qualname__} {key[:10]}…")
79
+ return value
80
+
81
+ # Cache miss: executa função
82
+ if logger:
83
+ logger(f"[CACHE {status}] {func.__qualname__} {key[:10]}… -> calling")
84
+
85
+ result = func(*args, **kwargs)
86
+
87
+ # Tenta armazenar no cache
88
+ try:
89
+ be.set(key, result, ttl)
90
+ if logger:
91
+ logger(f"[CACHE SET] {func.__qualname__} {key[:10]}… ttl={ttl}")
92
+ except Exception as e:
93
+ if logger:
94
+ logger(f"[CACHE ERROR SET] {func.__qualname__}: {e}")
95
+
96
+ return result
97
+
98
+ # Atributos públicos para inspeção/controle
99
+ wrapper.cache_backend = default_backend
100
+ wrapper.cache_clear = lambda: wrapper.cache_backend.clear()
101
+ wrapper.cache_info = lambda: wrapper.cache_backend.info()
102
+
103
+ return wrapper
104
+
105
+ return decorator
@@ -0,0 +1,50 @@
1
+ """Estratégias de serialização thread-safe para cache."""
2
+ import json
3
+ import pickle
4
+ from typing import Any
5
+
6
+
7
+ class SafeSerializer:
8
+ """
9
+ Serializer híbrido: tenta JSON (seguro) primeiro, fallback para pickle.
10
+
11
+ Prefixos:
12
+ - 0x00: JSON (seguro, sem risco de code execution)
13
+ - 0x01: Pickle (flexível, suporta objetos complexos)
14
+
15
+ Exemplo:
16
+ >>> ser = SafeSerializer()
17
+ >>> data = ser.dumps({"user": 42})
18
+ >>> obj = ser.loads(data)
19
+ """
20
+
21
+ def dumps(self, obj: Any) -> bytes:
22
+ """Serializa obj para bytes com prefixo de tipo."""
23
+ try:
24
+ # Tenta JSON primeiro
25
+ payload = json.dumps(
26
+ obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")
27
+ ).encode("utf-8")
28
+ return b"\x00" + payload
29
+ except (TypeError, ValueError):
30
+ # Fallback para pickle
31
+ payload = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
32
+ return b"\x01" + payload
33
+
34
+ def loads(self, data: bytes) -> Any:
35
+ """Desserializa bytes baseado no prefixo."""
36
+ if not data or len(data) < 1:
37
+ raise ValueError("Empty serialized data")
38
+
39
+ prefix = data[0:1]
40
+ payload = data[1:]
41
+
42
+ if prefix == b"\x00":
43
+ return json.loads(payload.decode("utf-8"))
44
+ elif prefix == b"\x01":
45
+ return pickle.loads(payload)
46
+ else:
47
+ prefix_hex = hex(prefix[0]) if prefix else "unknown"
48
+ raise ValueError(f"Unknown serializer prefix: {prefix_hex}")
49
+
50
+