shadowcache 0.1.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.
- shadowcache-0.1.0/LICENSE +21 -0
- shadowcache-0.1.0/PKG-INFO +230 -0
- shadowcache-0.1.0/README.md +201 -0
- shadowcache-0.1.0/pyproject.toml +45 -0
- shadowcache-0.1.0/setup.cfg +4 -0
- shadowcache-0.1.0/shadowcache/__init__.py +31 -0
- shadowcache-0.1.0/shadowcache/core.py +360 -0
- shadowcache-0.1.0/shadowcache/exceptions.py +17 -0
- shadowcache-0.1.0/shadowcache/logger.py +39 -0
- shadowcache-0.1.0/shadowcache/parser.py +86 -0
- shadowcache-0.1.0/shadowcache/py.typed +0 -0
- shadowcache-0.1.0/shadowcache.egg-info/PKG-INFO +230 -0
- shadowcache-0.1.0/shadowcache.egg-info/SOURCES.txt +16 -0
- shadowcache-0.1.0/shadowcache.egg-info/dependency_links.txt +1 -0
- shadowcache-0.1.0/shadowcache.egg-info/requires.txt +6 -0
- shadowcache-0.1.0/shadowcache.egg-info/top_level.txt +1 -0
- shadowcache-0.1.0/tests/test_core.py +353 -0
- shadowcache-0.1.0/tests/test_parser.py +93 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pratham Bhosale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shadowcache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Transparent Redis caching for raw SQL connections - no ORM required
|
|
5
|
+
Author: Pratham Bhosale
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pratham2402/ShadowCache
|
|
8
|
+
Project-URL: Repository, https://github.com/pratham2402/ShadowCache
|
|
9
|
+
Keywords: redis,mysql,cache,sql,caching,database,db-api,transparent-cache,query-cache
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: redis>=4.0
|
|
24
|
+
Requires-Dist: mysql-connector-python>=8.0
|
|
25
|
+
Requires-Dist: sqlglot>=20.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="https://img.shields.io/static/v1?label=%F0%9F%8C%9F&message=If%20Useful&style=flat&color=BC4E99">
|
|
32
|
+
<img src="https://badges.frapsoft.com/os/v1/open-source.svg?v=103">
|
|
33
|
+
<a href="https://github.com/pratham2402"><img src="https://img.shields.io/badge/View-My_Profile-green?logo=GitHub"></a>
|
|
34
|
+
<a href="https://github.com/pratham2402?tab=repositories"><img src="https://img.shields.io/badge/View-My_Repositories-blue?logo=GitHub"></a>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python">
|
|
39
|
+
<img src="https://img.shields.io/badge/license-MIT-green">
|
|
40
|
+
<img src="https://img.shields.io/badge/platform-mysql%20%7C%20redis-red">
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
# ShadowCache
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="./README%20Banner%20Art.png" alt="ShadowCache Banner">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
> **Write SQL. Get caching. Nothing else.**
|
|
50
|
+
|
|
51
|
+
ShadowCache wraps your MySQL connection and transparently caches SELECT results
|
|
52
|
+
in Redis. INSERT, UPDATE, or DELETE statements automatically evict affected cache
|
|
53
|
+
entries so your reads never serve stale data. No ORM. No boilerplate. No config.
|
|
54
|
+
|
|
55
|
+
<br>
|
|
56
|
+
|
|
57
|
+
<details open>
|
|
58
|
+
<summary><b>Table of Contents</b></summary>
|
|
59
|
+
|
|
60
|
+
- [The Problem](#the-problem)
|
|
61
|
+
- [Features](#features)
|
|
62
|
+
- [Installation](#installation)
|
|
63
|
+
- [Quick Start](#quick-start)
|
|
64
|
+
- [API Reference](#api-reference)
|
|
65
|
+
- [Configuration](#configuration)
|
|
66
|
+
- [Running Tests](#running-tests)
|
|
67
|
+
- [License](#license)
|
|
68
|
+
|
|
69
|
+
</details>
|
|
70
|
+
|
|
71
|
+
## The Problem
|
|
72
|
+
|
|
73
|
+
Every developer who writes raw SQL eventually writes this:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# 8 lines of boilerplate for every cached query
|
|
77
|
+
cache_key = f"user:{user_id}"
|
|
78
|
+
cached = redis.get(cache_key)
|
|
79
|
+
if cached:
|
|
80
|
+
return json.loads(cached)
|
|
81
|
+
|
|
82
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
83
|
+
row = cursor.fetchone()
|
|
84
|
+
redis.set(cache_key, json.dumps(row), ex=300)
|
|
85
|
+
return row
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
And on every INSERT, UPDATE, or DELETE you need to remember:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
cursor.execute("UPDATE users SET name = %s WHERE id = %s", (name, user_id))
|
|
92
|
+
redis.delete(f"user:{user_id}") # easy to forget, easy to get wrong
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```diff
|
|
96
|
+
- Boilerplate for every query
|
|
97
|
+
- Manual invalidation you will forget
|
|
98
|
+
+ One line. Caching and invalidation are automatic.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**With ShadowCache:**
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
105
|
+
cursor, _ = cache.execute("UPDATE users SET name = %s WHERE id = %s", ("Alice", 42))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
| | |
|
|
111
|
+
|---|---|
|
|
112
|
+
| **Zero-schema caching** | Works with any MySQL table, any query. No model definitions needed. |
|
|
113
|
+
| **Write-triggered eviction** | INSERT, UPDATE, and DELETE automatically evict cached SELECTs for the same table. |
|
|
114
|
+
| **TTL safety net** | Cached entries expire after a configurable time-to-live. Eventual consistency guaranteed. |
|
|
115
|
+
| **Graceful fallback** | If Redis is unreachable, queries still execute against MySQL. |
|
|
116
|
+
|
|
117
|
+
## Installation
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install shadowcache
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> Or install from source:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
git clone https://github.com/pratham2402/ShadowCache.git
|
|
127
|
+
cd ShadowCache
|
|
128
|
+
pip install -r requirements.txt
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Requires:** Python 3.8+, Redis, MySQL.
|
|
132
|
+
|
|
133
|
+
## Quick Start
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import mysql.connector
|
|
137
|
+
from shadowcache import ShadowCache
|
|
138
|
+
|
|
139
|
+
conn = mysql.connector.connect(
|
|
140
|
+
host="localhost", database="my_app",
|
|
141
|
+
user="app_user", password="secret",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
cache = ShadowCache(conn)
|
|
145
|
+
|
|
146
|
+
# Cold miss -- hits MySQL, stores in Redis
|
|
147
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
148
|
+
|
|
149
|
+
# Warm hit -- returns from Redis instantly
|
|
150
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
151
|
+
|
|
152
|
+
# Write evicts the cache
|
|
153
|
+
cache.execute("UPDATE users SET name = %s WHERE id = %s", ("Alice", 42))
|
|
154
|
+
|
|
155
|
+
# Cache was evicted -- fresh data from MySQL
|
|
156
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## API Reference
|
|
160
|
+
|
|
161
|
+
### `ShadowCache(db_connection, *, ...)`
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
ShadowCache(
|
|
165
|
+
db_connection,
|
|
166
|
+
*,
|
|
167
|
+
redis_client=None,
|
|
168
|
+
redis_host="localhost",
|
|
169
|
+
redis_port=6379,
|
|
170
|
+
ttl=300,
|
|
171
|
+
auto_invalidate=True,
|
|
172
|
+
key_prefix="shadowcache",
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Parameter | Default | Description |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `db_connection` | *(required)* | An open DB-API2 MySQL connection |
|
|
179
|
+
| `redis_client` | `None` | Pre-configured `redis.Redis` instance; created automatically if omitted |
|
|
180
|
+
| `redis_host` | `"localhost"` | Redis hostname |
|
|
181
|
+
| `redis_port` | `6379` | Redis port |
|
|
182
|
+
| `ttl` | `300` | Cache TTL in seconds |
|
|
183
|
+
| `auto_invalidate` | `True` | Whether writes automatically evict related cache entries |
|
|
184
|
+
| `key_prefix` | `"shadowcache"` | Namespace prefix for all Redis keys |
|
|
185
|
+
|
|
186
|
+
### `ShadowCache.execute(sql, params=None)`
|
|
187
|
+
|
|
188
|
+
Returns `(cursor, rows)`.
|
|
189
|
+
|
|
190
|
+
| SQL | Behaviour |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `SELECT` | Checks Redis first. Hit returns `(None, cached_rows)`. Miss executes on MySQL, caches, returns `(cursor, rows)`. |
|
|
193
|
+
| `INSERT` | Executes on MySQL. Returns `(cursor, None)`. See `cursor.lastrowid`. |
|
|
194
|
+
| `UPDATE` / `DELETE` | Executes on MySQL, evicts cache for affected tables. Returns `(cursor, None)`. See `cursor.rowcount`. |
|
|
195
|
+
| DDL / other | Executes on MySQL. No caching, no eviction. |
|
|
196
|
+
|
|
197
|
+
### Other Methods
|
|
198
|
+
|
|
199
|
+
| Method | Description |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `invalidate_table(name)` | Evict all cached entries for a table. Returns count of keys removed. |
|
|
202
|
+
| `flush_cache()` | Remove all ShadowCache keys from Redis. Returns count of keys removed. |
|
|
203
|
+
| `stats` | Property. Returns a dict with keys `hits`, `misses`, `total_requests`, `hit_ratio`. |
|
|
204
|
+
| `close()` | Close the wrapped database connection. |
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
207
|
+
|
|
208
|
+
Copy `.env.example` to `.env` and set your credentials:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
REDIS_HOST=localhost
|
|
212
|
+
REDIS_PORT=6379
|
|
213
|
+
MYSQL_HOST=localhost
|
|
214
|
+
MYSQL_PORT=3306
|
|
215
|
+
MYSQL_USER=your_db_user
|
|
216
|
+
MYSQL_PASSWORD=your_db_password
|
|
217
|
+
MYSQL_DATABASE=your_database
|
|
218
|
+
LOG_LEVEL=INFO
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Running Tests
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Unit tests -- no Redis or MySQL needed, all mocks
|
|
225
|
+
python -m pytest tests/ -v
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/static/v1?label=%F0%9F%8C%9F&message=If%20Useful&style=flat&color=BC4E99">
|
|
3
|
+
<img src="https://badges.frapsoft.com/os/v1/open-source.svg?v=103">
|
|
4
|
+
<a href="https://github.com/pratham2402"><img src="https://img.shields.io/badge/View-My_Profile-green?logo=GitHub"></a>
|
|
5
|
+
<a href="https://github.com/pratham2402?tab=repositories"><img src="https://img.shields.io/badge/View-My_Repositories-blue?logo=GitHub"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python">
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-green">
|
|
11
|
+
<img src="https://img.shields.io/badge/platform-mysql%20%7C%20redis-red">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
# ShadowCache
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="./README%20Banner%20Art.png" alt="ShadowCache Banner">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
> **Write SQL. Get caching. Nothing else.**
|
|
21
|
+
|
|
22
|
+
ShadowCache wraps your MySQL connection and transparently caches SELECT results
|
|
23
|
+
in Redis. INSERT, UPDATE, or DELETE statements automatically evict affected cache
|
|
24
|
+
entries so your reads never serve stale data. No ORM. No boilerplate. No config.
|
|
25
|
+
|
|
26
|
+
<br>
|
|
27
|
+
|
|
28
|
+
<details open>
|
|
29
|
+
<summary><b>Table of Contents</b></summary>
|
|
30
|
+
|
|
31
|
+
- [The Problem](#the-problem)
|
|
32
|
+
- [Features](#features)
|
|
33
|
+
- [Installation](#installation)
|
|
34
|
+
- [Quick Start](#quick-start)
|
|
35
|
+
- [API Reference](#api-reference)
|
|
36
|
+
- [Configuration](#configuration)
|
|
37
|
+
- [Running Tests](#running-tests)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
</details>
|
|
41
|
+
|
|
42
|
+
## The Problem
|
|
43
|
+
|
|
44
|
+
Every developer who writes raw SQL eventually writes this:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# 8 lines of boilerplate for every cached query
|
|
48
|
+
cache_key = f"user:{user_id}"
|
|
49
|
+
cached = redis.get(cache_key)
|
|
50
|
+
if cached:
|
|
51
|
+
return json.loads(cached)
|
|
52
|
+
|
|
53
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
54
|
+
row = cursor.fetchone()
|
|
55
|
+
redis.set(cache_key, json.dumps(row), ex=300)
|
|
56
|
+
return row
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
And on every INSERT, UPDATE, or DELETE you need to remember:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
cursor.execute("UPDATE users SET name = %s WHERE id = %s", (name, user_id))
|
|
63
|
+
redis.delete(f"user:{user_id}") # easy to forget, easy to get wrong
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```diff
|
|
67
|
+
- Boilerplate for every query
|
|
68
|
+
- Manual invalidation you will forget
|
|
69
|
+
+ One line. Caching and invalidation are automatic.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**With ShadowCache:**
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
76
|
+
cursor, _ = cache.execute("UPDATE users SET name = %s WHERE id = %s", ("Alice", 42))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
| | |
|
|
82
|
+
|---|---|
|
|
83
|
+
| **Zero-schema caching** | Works with any MySQL table, any query. No model definitions needed. |
|
|
84
|
+
| **Write-triggered eviction** | INSERT, UPDATE, and DELETE automatically evict cached SELECTs for the same table. |
|
|
85
|
+
| **TTL safety net** | Cached entries expire after a configurable time-to-live. Eventual consistency guaranteed. |
|
|
86
|
+
| **Graceful fallback** | If Redis is unreachable, queries still execute against MySQL. |
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install shadowcache
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> Or install from source:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/pratham2402/ShadowCache.git
|
|
98
|
+
cd ShadowCache
|
|
99
|
+
pip install -r requirements.txt
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Requires:** Python 3.8+, Redis, MySQL.
|
|
103
|
+
|
|
104
|
+
## Quick Start
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import mysql.connector
|
|
108
|
+
from shadowcache import ShadowCache
|
|
109
|
+
|
|
110
|
+
conn = mysql.connector.connect(
|
|
111
|
+
host="localhost", database="my_app",
|
|
112
|
+
user="app_user", password="secret",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
cache = ShadowCache(conn)
|
|
116
|
+
|
|
117
|
+
# Cold miss -- hits MySQL, stores in Redis
|
|
118
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
119
|
+
|
|
120
|
+
# Warm hit -- returns from Redis instantly
|
|
121
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
122
|
+
|
|
123
|
+
# Write evicts the cache
|
|
124
|
+
cache.execute("UPDATE users SET name = %s WHERE id = %s", ("Alice", 42))
|
|
125
|
+
|
|
126
|
+
# Cache was evicted -- fresh data from MySQL
|
|
127
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## API Reference
|
|
131
|
+
|
|
132
|
+
### `ShadowCache(db_connection, *, ...)`
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
ShadowCache(
|
|
136
|
+
db_connection,
|
|
137
|
+
*,
|
|
138
|
+
redis_client=None,
|
|
139
|
+
redis_host="localhost",
|
|
140
|
+
redis_port=6379,
|
|
141
|
+
ttl=300,
|
|
142
|
+
auto_invalidate=True,
|
|
143
|
+
key_prefix="shadowcache",
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Parameter | Default | Description |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `db_connection` | *(required)* | An open DB-API2 MySQL connection |
|
|
150
|
+
| `redis_client` | `None` | Pre-configured `redis.Redis` instance; created automatically if omitted |
|
|
151
|
+
| `redis_host` | `"localhost"` | Redis hostname |
|
|
152
|
+
| `redis_port` | `6379` | Redis port |
|
|
153
|
+
| `ttl` | `300` | Cache TTL in seconds |
|
|
154
|
+
| `auto_invalidate` | `True` | Whether writes automatically evict related cache entries |
|
|
155
|
+
| `key_prefix` | `"shadowcache"` | Namespace prefix for all Redis keys |
|
|
156
|
+
|
|
157
|
+
### `ShadowCache.execute(sql, params=None)`
|
|
158
|
+
|
|
159
|
+
Returns `(cursor, rows)`.
|
|
160
|
+
|
|
161
|
+
| SQL | Behaviour |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `SELECT` | Checks Redis first. Hit returns `(None, cached_rows)`. Miss executes on MySQL, caches, returns `(cursor, rows)`. |
|
|
164
|
+
| `INSERT` | Executes on MySQL. Returns `(cursor, None)`. See `cursor.lastrowid`. |
|
|
165
|
+
| `UPDATE` / `DELETE` | Executes on MySQL, evicts cache for affected tables. Returns `(cursor, None)`. See `cursor.rowcount`. |
|
|
166
|
+
| DDL / other | Executes on MySQL. No caching, no eviction. |
|
|
167
|
+
|
|
168
|
+
### Other Methods
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `invalidate_table(name)` | Evict all cached entries for a table. Returns count of keys removed. |
|
|
173
|
+
| `flush_cache()` | Remove all ShadowCache keys from Redis. Returns count of keys removed. |
|
|
174
|
+
| `stats` | Property. Returns a dict with keys `hits`, `misses`, `total_requests`, `hit_ratio`. |
|
|
175
|
+
| `close()` | Close the wrapped database connection. |
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
Copy `.env.example` to `.env` and set your credentials:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
REDIS_HOST=localhost
|
|
183
|
+
REDIS_PORT=6379
|
|
184
|
+
MYSQL_HOST=localhost
|
|
185
|
+
MYSQL_PORT=3306
|
|
186
|
+
MYSQL_USER=your_db_user
|
|
187
|
+
MYSQL_PASSWORD=your_db_password
|
|
188
|
+
MYSQL_DATABASE=your_database
|
|
189
|
+
LOG_LEVEL=INFO
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Running Tests
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Unit tests -- no Redis or MySQL needed, all mocks
|
|
196
|
+
python -m pytest tests/ -v
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shadowcache"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Transparent Redis caching for raw SQL connections - no ORM required"
|
|
9
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{name = "Pratham Bhosale"}]
|
|
12
|
+
keywords = ["redis", "mysql", "cache", "sql", "caching", "database", "db-api", "transparent-cache", "query-cache"]
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Database",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"redis>=4.0",
|
|
28
|
+
"mysql-connector-python>=8.0",
|
|
29
|
+
"sqlglot>=20.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/pratham2402/ShadowCache"
|
|
34
|
+
Repository = "https://github.com/pratham2402/ShadowCache"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
include = ["shadowcache*"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
shadowcache = ["py.typed"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""ShadowCache -- Transparent Redis caching for raw SQL connections.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import mysql.connector
|
|
5
|
+
from shadowcache import ShadowCache
|
|
6
|
+
|
|
7
|
+
conn = mysql.connector.connect(host="localhost", database="mydb")
|
|
8
|
+
cache = ShadowCache(conn)
|
|
9
|
+
|
|
10
|
+
# SELECTs are transparently cached
|
|
11
|
+
cursor, rows = cache.execute("SELECT * FROM users WHERE id = %s", (42,))
|
|
12
|
+
|
|
13
|
+
# INSERT/UPDATE/DELETE automatically evict related cache entries
|
|
14
|
+
cache.execute("UPDATE users SET name = %s WHERE id = %s", ("Alice", 42))
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from shadowcache.exceptions import ShadowCacheError
|
|
18
|
+
|
|
19
|
+
__all__ = ["ShadowCache", "ShadowCacheError"]
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __getattr__(name):
|
|
24
|
+
"""Lazily import ShadowCache so the package is usable even when
|
|
25
|
+
only a subset of modules have been installed."""
|
|
26
|
+
if name == "ShadowCache":
|
|
27
|
+
from shadowcache.core import ShadowCache as _sc
|
|
28
|
+
|
|
29
|
+
return _sc
|
|
30
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
31
|
+
|