async-redis-client 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.
Files changed (21) hide show
  1. async_redis_client-0.1.0/LICENSE +21 -0
  2. async_redis_client-0.1.0/PKG-INFO +280 -0
  3. async_redis_client-0.1.0/README.md +252 -0
  4. async_redis_client-0.1.0/pyproject.toml +117 -0
  5. async_redis_client-0.1.0/src/async_redis_client/__init__.py +52 -0
  6. async_redis_client-0.1.0/src/async_redis_client/adapters/__init__.py +1 -0
  7. async_redis_client-0.1.0/src/async_redis_client/adapters/memory/__init__.py +4 -0
  8. async_redis_client-0.1.0/src/async_redis_client/adapters/memory/async_adapter.py +105 -0
  9. async_redis_client-0.1.0/src/async_redis_client/adapters/memory/sync_adapter.py +144 -0
  10. async_redis_client-0.1.0/src/async_redis_client/adapters/redis/__init__.py +4 -0
  11. async_redis_client-0.1.0/src/async_redis_client/adapters/redis/_helpers.py +78 -0
  12. async_redis_client-0.1.0/src/async_redis_client/adapters/redis/async_adapter.py +247 -0
  13. async_redis_client-0.1.0/src/async_redis_client/adapters/redis/sync_adapter.py +255 -0
  14. async_redis_client-0.1.0/src/async_redis_client/crypto.py +73 -0
  15. async_redis_client-0.1.0/src/async_redis_client/errors.py +27 -0
  16. async_redis_client-0.1.0/src/async_redis_client/ports/__init__.py +4 -0
  17. async_redis_client-0.1.0/src/async_redis_client/ports/async_cache_port.py +65 -0
  18. async_redis_client-0.1.0/src/async_redis_client/ports/cache_port.py +6 -0
  19. async_redis_client-0.1.0/src/async_redis_client/ports/sync_cache_port.py +89 -0
  20. async_redis_client-0.1.0/src/async_redis_client/py.typed +0 -0
  21. async_redis_client-0.1.0/src/async_redis_client/serialization.py +44 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mato777
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,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: async-redis-client
3
+ Version: 0.1.0
4
+ Summary: Hexagonal cache library with sync/async Redis adapters, Fernet encryption, and Pydantic JSON.
5
+ Keywords: redis,cache,async,fernet,pydantic,hexagonal
6
+ Author: mato777
7
+ Author-email: mato777 <matias@landjourney.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: cryptography>=48.0.0
20
+ Requires-Dist: pydantic>=2.13.4
21
+ Requires-Dist: redis>=7.4.0
22
+ Requires-Python: >=3.11
23
+ Project-URL: Homepage, https://github.com/mato777/redis-adapter
24
+ Project-URL: Repository, https://github.com/mato777/redis-adapter
25
+ Project-URL: Documentation, https://github.com/mato777/redis-adapter#readme
26
+ Project-URL: Issues, https://github.com/mato777/redis-adapter/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ # async-redis-client
30
+
31
+ A small **Ports and Adapters (hexagonal)** cache library for Python: application code depends on **`CacheSyncPort` / `CacheAsyncPort`** (`typing.Protocol`); **Redis** (sync or asyncio) **or an in-memory implementation** satisfies those protocols behind the scenes.
32
+
33
+ **Features:**
34
+
35
+ - Sync and async Redis adapters built on **[redis-py](https://redis.readthedocs.io/)** (`Redis`, `RedisCluster`, and asyncio equivalents)—inject a client from your composition root, or use **`from_standalone_url`** / **`from_cluster_url`**.
36
+ - **Fernet encryption** at rest for cached values; payloads are **UTF-8 JSON** via **Pydantic v2** (`JsonValue`, `BaseModel`, `TypeAdapter`).
37
+ - **Optional secondary Fernet key** for decryption during rotation (`CACHE_FERNET_KEY_SECONDARY` or constructor arg); writes always use the primary key.
38
+ - **Plaintext integer counters** (`incr`, `decr`, `incrby`)—keep counter keys separate from encrypted JSON keys (for example a `counter:` prefix).
39
+ - **`set_many` / `get_many`** via pipeline/`MGET` semantics—on **Redis Cluster**, keys must land in the **same hash slot** (use hash tags in keys or `key_prefix`, e.g. `{tenant}:item:1`).
40
+ - **Memory adapters** for fast tests and local use (`MemoryCacheSyncAdapter`, `MemoryCacheAsyncAdapter`).
41
+
42
+ Requirements: **Python ≥ 3.11**, **redis-py ≥ 7.4** (stable `redis` on PyPI), **cryptography**, **pydantic ≥ 2**. Example and e2e Docker images use **Redis 8** server (`redis:8-alpine`).
43
+
44
+ ## Install
45
+
46
+ **PyPI distribution name:** `async-redis-client`
47
+ **Import name:** `async_redis_client`
48
+
49
+ ### In another project (uv)
50
+
51
+ From Git (pin a tag or commit for reproducibility):
52
+
53
+ ```bash
54
+ uv add "async-redis-client @ git+https://github.com/mato777/redis-adapter.git"
55
+ ```
56
+
57
+ From a local checkout (editable, good for monorepos):
58
+
59
+ ```bash
60
+ uv add --editable /path/to/async-redis-client
61
+ ```
62
+
63
+ Then import the public API:
64
+
65
+ ```python
66
+ from async_redis_client import CacheSyncPort, RedisCacheSyncAdapter
67
+ ```
68
+
69
+ ### pip / wheel
70
+
71
+ ```bash
72
+ pip install "async-redis-client @ git+https://github.com/mato777/redis-adapter.git"
73
+ # or, from a clone:
74
+ pip install .
75
+ ```
76
+
77
+ ### Develop this repo
78
+
79
+ Using [uv](https://docs.astral.sh/uv/) (recommended; `uv.lock` is in-repo):
80
+
81
+ ```bash
82
+ git clone https://github.com/mato777/redis-adapter.git
83
+ cd redis-adapter
84
+ uv sync
85
+ uv run pytest
86
+ ```
87
+
88
+ `uv sync` installs the package in editable mode so `import async_redis_client` works immediately.
89
+
90
+ ## Configuration
91
+
92
+ | Setting | Meaning |
93
+ |---------|---------|
94
+ | **`CACHE_FERNET_KEY`** | URL-safe base64 Fernet key (ASCII). Used when `fernet_key` is omitted in the adapter constructor. |
95
+ | **`CACHE_FERNET_KEY_SECONDARY`** | Optional legacy key tried on decrypt after primary fails (rotation). Constructor `fernet_key_secondary` overrides. |
96
+ | **`key_prefix`** (constructor) | Optional string prepended to logical keys on Redis adapters. |
97
+
98
+ Generate a Fernet key (store securely in production):
99
+
100
+ ```bash
101
+ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
102
+ ```
103
+
104
+ ## Usage
105
+
106
+ Depend on **`CacheSyncPort`** or **`CacheAsyncPort`** in your domain/services; compose a Redis or memory adapter in bootstrap.
107
+
108
+ ### Sync Redis (inject client)
109
+
110
+ ```python
111
+ from redis import Redis
112
+ from async_redis_client import CacheSyncPort, RedisCacheSyncAdapter
113
+
114
+
115
+ def bootstrap_cache() -> CacheSyncPort:
116
+ client = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
117
+ # owns_client=False (default): you must client.close() when finished.
118
+ return RedisCacheSyncAdapter(client, key_prefix="{myapp}") # CLUSTER_FRIENDLY PREFIX EXAMPLE
119
+
120
+
121
+ cache = bootstrap_cache()
122
+
123
+ cache.set_json("feature:toggle", {"enabled": True}, ttl_seconds=60)
124
+ payload = cache.get_json("feature:toggle")
125
+
126
+ cache.incrby("counter:requests", 1) # plaintext integer counter
127
+ # cache.close() is a no-op here; close the Redis client from your composition root.
128
+ ```
129
+
130
+ ### Sync Redis (URL factory)
131
+
132
+ ```python
133
+ from async_redis_client import RedisCacheSyncAdapter
134
+
135
+ with RedisCacheSyncAdapter.from_standalone_url(
136
+ "redis://localhost:6379/0",
137
+ key_prefix="{myapp}:",
138
+ ) as cache:
139
+ cache.set_many({"a": 1, "b": 2}, ttl_seconds=None) # keys must share a slot if using Cluster
140
+ assert cache.get_many(["a", "b"]) == {"a": 1, "b": 2}
141
+ # Or call cache.close() when not using a context manager—the adapter owns the Redis client.
142
+ ```
143
+
144
+ ### Pydantic models
145
+
146
+ ```python
147
+ from pydantic import BaseModel
148
+ from async_redis_client import RedisCacheSyncAdapter
149
+
150
+
151
+ class User(BaseModel):
152
+ id: int
153
+ name: str
154
+
155
+
156
+ cache = RedisCacheSyncAdapter.from_standalone_url("redis://localhost:6379/0")
157
+
158
+ user = User(id=1, name="Ada")
159
+ cache.set_model("user:1", user, ttl_seconds=3600)
160
+
161
+ loaded = cache.get_as_model("user:1", User)
162
+ assert loaded == user
163
+ ```
164
+
165
+ ### Async Redis
166
+
167
+ ```python
168
+ import asyncio
169
+
170
+ from redis.asyncio import Redis
171
+ from async_redis_client import RedisCacheAsyncAdapter
172
+
173
+
174
+ async def main():
175
+ client = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
176
+ cache = RedisCacheAsyncAdapter(client) # owns_client=False: you must await client.aclose()
177
+
178
+ await cache.set_json("hello", {"k": "v"})
179
+ got = await cache.get_json("hello")
180
+ await client.aclose()
181
+
182
+
183
+ asyncio.run(main())
184
+ ```
185
+
186
+ Or **`RedisCacheAsyncAdapter.from_standalone_url`** / **`from_cluster_url`** with the same `fernet_*` / `key_prefix` options as sync. Those factories own the client—use **`async with RedisCacheAsyncAdapter.from_standalone_url(...) as cache:`** or **`await cache.close()`** when done (`aclose` is the same as `close`).
187
+
188
+ ### In-memory adapter (tests)
189
+
190
+ ```python
191
+ from async_redis_client import MemoryCacheSyncAdapter
192
+
193
+ cache = MemoryCacheSyncAdapter() # no Fernet/redis; TTL args are ignored on memory adapters
194
+ cache.set_json("x", {"n": 1})
195
+ assert cache.get_json("x") == {"n": 1}
196
+ ```
197
+
198
+ ### Errors
199
+
200
+ - **`CacheError`** — missing key / bootstrap issues (for example unset Fernet key).
201
+ - **`DecryptionError`** — invalid Fernet token.
202
+ - **`SerializationError`** — wraps Pydantic validation problems after decryption.
203
+
204
+ Public exports are documented in **`async_redis_client.__init__.__all__`** (ports, adapters, errors, and **`SyncCachePort` / `AsyncCachePort`** aliases).
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ uv sync # deps + dev (pytest, fakeredis, …)
210
+ uv run pytest
211
+ ```
212
+
213
+ More design notes and module layout: [docs/PROJECT_CONTEXT.md](docs/PROJECT_CONTEXT.md) and [docs/PLAN.md](docs/PLAN.md).
214
+
215
+ ## Publish to PyPI
216
+
217
+ Distribution name on PyPI: **`async-redis-client`** (see `version` in `pyproject.toml`). Builds use **`uv_build`**; publish with **[uv](https://docs.astral.sh/uv/)**.
218
+
219
+ ### One-time setup
220
+
221
+ 1. Create an account on [pypi.org](https://pypi.org/) (and optionally [test.pypi.org](https://test.pypi.org/) for dry runs).
222
+ 2. Under **Account settings → API tokens**, create a token scoped to this project (or the whole account for the first upload).
223
+ 3. Export the token (do not commit it):
224
+
225
+ ```bash
226
+ export UV_PUBLISH_TOKEN="pypi-…" # uv reads this for `uv publish`
227
+ ```
228
+
229
+ Alternatively, pass `--token` on each `uv publish` invocation.
230
+
231
+ ### Release checklist
232
+
233
+ 1. Bump **`version`** in `pyproject.toml` (PyPI rejects re-uploading the same version).
234
+ 2. Run checks:
235
+
236
+ ```bash
237
+ uv sync
238
+ uv run pytest
239
+ uv run task lint
240
+ ```
241
+
242
+ 3. Build artifacts into `dist/`:
243
+
244
+ ```bash
245
+ uv build
246
+ ```
247
+
248
+ This produces a wheel (`.whl`) and source distribution (`.tar.gz`).
249
+
250
+ 4. Upload to **TestPyPI** first (optional but recommended):
251
+
252
+ ```bash
253
+ uv publish --publish-url https://test.pypi.org/legacy/
254
+ ```
255
+
256
+ Smoke-test install:
257
+
258
+ ```bash
259
+ pip install -i https://test.pypi.org/simple/ async-redis-client
260
+ ```
261
+
262
+ 5. Publish to **production PyPI**:
263
+
264
+ ```bash
265
+ uv publish
266
+ ```
267
+
268
+ After release, consumers can install with:
269
+
270
+ ```bash
271
+ uv add async-redis-client
272
+ # or
273
+ pip install async-redis-client
274
+ ```
275
+
276
+ ### Notes
277
+
278
+ - **First upload**: the PyPI project name must match `name` in `pyproject.toml` (`async-redis-client`). The first publish creates the project; later publishes require a token with upload rights.
279
+ - **Trusted publishing**: for CI, you can configure [PyPI trusted publishers](https://docs.pypi.org/trusted-publishers/) (e.g. GitHub Actions) instead of long-lived API tokens; this repo does not include a publish workflow yet.
280
+ - **Alternative**: `python -m build` plus `twine upload dist/*` works if you prefer not to use `uv publish`; keep using the same `pyproject.toml` / `uv_build` backend.
@@ -0,0 +1,252 @@
1
+ # async-redis-client
2
+
3
+ A small **Ports and Adapters (hexagonal)** cache library for Python: application code depends on **`CacheSyncPort` / `CacheAsyncPort`** (`typing.Protocol`); **Redis** (sync or asyncio) **or an in-memory implementation** satisfies those protocols behind the scenes.
4
+
5
+ **Features:**
6
+
7
+ - Sync and async Redis adapters built on **[redis-py](https://redis.readthedocs.io/)** (`Redis`, `RedisCluster`, and asyncio equivalents)—inject a client from your composition root, or use **`from_standalone_url`** / **`from_cluster_url`**.
8
+ - **Fernet encryption** at rest for cached values; payloads are **UTF-8 JSON** via **Pydantic v2** (`JsonValue`, `BaseModel`, `TypeAdapter`).
9
+ - **Optional secondary Fernet key** for decryption during rotation (`CACHE_FERNET_KEY_SECONDARY` or constructor arg); writes always use the primary key.
10
+ - **Plaintext integer counters** (`incr`, `decr`, `incrby`)—keep counter keys separate from encrypted JSON keys (for example a `counter:` prefix).
11
+ - **`set_many` / `get_many`** via pipeline/`MGET` semantics—on **Redis Cluster**, keys must land in the **same hash slot** (use hash tags in keys or `key_prefix`, e.g. `{tenant}:item:1`).
12
+ - **Memory adapters** for fast tests and local use (`MemoryCacheSyncAdapter`, `MemoryCacheAsyncAdapter`).
13
+
14
+ Requirements: **Python ≥ 3.11**, **redis-py ≥ 7.4** (stable `redis` on PyPI), **cryptography**, **pydantic ≥ 2**. Example and e2e Docker images use **Redis 8** server (`redis:8-alpine`).
15
+
16
+ ## Install
17
+
18
+ **PyPI distribution name:** `async-redis-client`
19
+ **Import name:** `async_redis_client`
20
+
21
+ ### In another project (uv)
22
+
23
+ From Git (pin a tag or commit for reproducibility):
24
+
25
+ ```bash
26
+ uv add "async-redis-client @ git+https://github.com/mato777/redis-adapter.git"
27
+ ```
28
+
29
+ From a local checkout (editable, good for monorepos):
30
+
31
+ ```bash
32
+ uv add --editable /path/to/async-redis-client
33
+ ```
34
+
35
+ Then import the public API:
36
+
37
+ ```python
38
+ from async_redis_client import CacheSyncPort, RedisCacheSyncAdapter
39
+ ```
40
+
41
+ ### pip / wheel
42
+
43
+ ```bash
44
+ pip install "async-redis-client @ git+https://github.com/mato777/redis-adapter.git"
45
+ # or, from a clone:
46
+ pip install .
47
+ ```
48
+
49
+ ### Develop this repo
50
+
51
+ Using [uv](https://docs.astral.sh/uv/) (recommended; `uv.lock` is in-repo):
52
+
53
+ ```bash
54
+ git clone https://github.com/mato777/redis-adapter.git
55
+ cd redis-adapter
56
+ uv sync
57
+ uv run pytest
58
+ ```
59
+
60
+ `uv sync` installs the package in editable mode so `import async_redis_client` works immediately.
61
+
62
+ ## Configuration
63
+
64
+ | Setting | Meaning |
65
+ |---------|---------|
66
+ | **`CACHE_FERNET_KEY`** | URL-safe base64 Fernet key (ASCII). Used when `fernet_key` is omitted in the adapter constructor. |
67
+ | **`CACHE_FERNET_KEY_SECONDARY`** | Optional legacy key tried on decrypt after primary fails (rotation). Constructor `fernet_key_secondary` overrides. |
68
+ | **`key_prefix`** (constructor) | Optional string prepended to logical keys on Redis adapters. |
69
+
70
+ Generate a Fernet key (store securely in production):
71
+
72
+ ```bash
73
+ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ Depend on **`CacheSyncPort`** or **`CacheAsyncPort`** in your domain/services; compose a Redis or memory adapter in bootstrap.
79
+
80
+ ### Sync Redis (inject client)
81
+
82
+ ```python
83
+ from redis import Redis
84
+ from async_redis_client import CacheSyncPort, RedisCacheSyncAdapter
85
+
86
+
87
+ def bootstrap_cache() -> CacheSyncPort:
88
+ client = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
89
+ # owns_client=False (default): you must client.close() when finished.
90
+ return RedisCacheSyncAdapter(client, key_prefix="{myapp}") # CLUSTER_FRIENDLY PREFIX EXAMPLE
91
+
92
+
93
+ cache = bootstrap_cache()
94
+
95
+ cache.set_json("feature:toggle", {"enabled": True}, ttl_seconds=60)
96
+ payload = cache.get_json("feature:toggle")
97
+
98
+ cache.incrby("counter:requests", 1) # plaintext integer counter
99
+ # cache.close() is a no-op here; close the Redis client from your composition root.
100
+ ```
101
+
102
+ ### Sync Redis (URL factory)
103
+
104
+ ```python
105
+ from async_redis_client import RedisCacheSyncAdapter
106
+
107
+ with RedisCacheSyncAdapter.from_standalone_url(
108
+ "redis://localhost:6379/0",
109
+ key_prefix="{myapp}:",
110
+ ) as cache:
111
+ cache.set_many({"a": 1, "b": 2}, ttl_seconds=None) # keys must share a slot if using Cluster
112
+ assert cache.get_many(["a", "b"]) == {"a": 1, "b": 2}
113
+ # Or call cache.close() when not using a context manager—the adapter owns the Redis client.
114
+ ```
115
+
116
+ ### Pydantic models
117
+
118
+ ```python
119
+ from pydantic import BaseModel
120
+ from async_redis_client import RedisCacheSyncAdapter
121
+
122
+
123
+ class User(BaseModel):
124
+ id: int
125
+ name: str
126
+
127
+
128
+ cache = RedisCacheSyncAdapter.from_standalone_url("redis://localhost:6379/0")
129
+
130
+ user = User(id=1, name="Ada")
131
+ cache.set_model("user:1", user, ttl_seconds=3600)
132
+
133
+ loaded = cache.get_as_model("user:1", User)
134
+ assert loaded == user
135
+ ```
136
+
137
+ ### Async Redis
138
+
139
+ ```python
140
+ import asyncio
141
+
142
+ from redis.asyncio import Redis
143
+ from async_redis_client import RedisCacheAsyncAdapter
144
+
145
+
146
+ async def main():
147
+ client = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
148
+ cache = RedisCacheAsyncAdapter(client) # owns_client=False: you must await client.aclose()
149
+
150
+ await cache.set_json("hello", {"k": "v"})
151
+ got = await cache.get_json("hello")
152
+ await client.aclose()
153
+
154
+
155
+ asyncio.run(main())
156
+ ```
157
+
158
+ Or **`RedisCacheAsyncAdapter.from_standalone_url`** / **`from_cluster_url`** with the same `fernet_*` / `key_prefix` options as sync. Those factories own the client—use **`async with RedisCacheAsyncAdapter.from_standalone_url(...) as cache:`** or **`await cache.close()`** when done (`aclose` is the same as `close`).
159
+
160
+ ### In-memory adapter (tests)
161
+
162
+ ```python
163
+ from async_redis_client import MemoryCacheSyncAdapter
164
+
165
+ cache = MemoryCacheSyncAdapter() # no Fernet/redis; TTL args are ignored on memory adapters
166
+ cache.set_json("x", {"n": 1})
167
+ assert cache.get_json("x") == {"n": 1}
168
+ ```
169
+
170
+ ### Errors
171
+
172
+ - **`CacheError`** — missing key / bootstrap issues (for example unset Fernet key).
173
+ - **`DecryptionError`** — invalid Fernet token.
174
+ - **`SerializationError`** — wraps Pydantic validation problems after decryption.
175
+
176
+ Public exports are documented in **`async_redis_client.__init__.__all__`** (ports, adapters, errors, and **`SyncCachePort` / `AsyncCachePort`** aliases).
177
+
178
+ ## Development
179
+
180
+ ```bash
181
+ uv sync # deps + dev (pytest, fakeredis, …)
182
+ uv run pytest
183
+ ```
184
+
185
+ More design notes and module layout: [docs/PROJECT_CONTEXT.md](docs/PROJECT_CONTEXT.md) and [docs/PLAN.md](docs/PLAN.md).
186
+
187
+ ## Publish to PyPI
188
+
189
+ Distribution name on PyPI: **`async-redis-client`** (see `version` in `pyproject.toml`). Builds use **`uv_build`**; publish with **[uv](https://docs.astral.sh/uv/)**.
190
+
191
+ ### One-time setup
192
+
193
+ 1. Create an account on [pypi.org](https://pypi.org/) (and optionally [test.pypi.org](https://test.pypi.org/) for dry runs).
194
+ 2. Under **Account settings → API tokens**, create a token scoped to this project (or the whole account for the first upload).
195
+ 3. Export the token (do not commit it):
196
+
197
+ ```bash
198
+ export UV_PUBLISH_TOKEN="pypi-…" # uv reads this for `uv publish`
199
+ ```
200
+
201
+ Alternatively, pass `--token` on each `uv publish` invocation.
202
+
203
+ ### Release checklist
204
+
205
+ 1. Bump **`version`** in `pyproject.toml` (PyPI rejects re-uploading the same version).
206
+ 2. Run checks:
207
+
208
+ ```bash
209
+ uv sync
210
+ uv run pytest
211
+ uv run task lint
212
+ ```
213
+
214
+ 3. Build artifacts into `dist/`:
215
+
216
+ ```bash
217
+ uv build
218
+ ```
219
+
220
+ This produces a wheel (`.whl`) and source distribution (`.tar.gz`).
221
+
222
+ 4. Upload to **TestPyPI** first (optional but recommended):
223
+
224
+ ```bash
225
+ uv publish --publish-url https://test.pypi.org/legacy/
226
+ ```
227
+
228
+ Smoke-test install:
229
+
230
+ ```bash
231
+ pip install -i https://test.pypi.org/simple/ async-redis-client
232
+ ```
233
+
234
+ 5. Publish to **production PyPI**:
235
+
236
+ ```bash
237
+ uv publish
238
+ ```
239
+
240
+ After release, consumers can install with:
241
+
242
+ ```bash
243
+ uv add async-redis-client
244
+ # or
245
+ pip install async-redis-client
246
+ ```
247
+
248
+ ### Notes
249
+
250
+ - **First upload**: the PyPI project name must match `name` in `pyproject.toml` (`async-redis-client`). The first publish creates the project; later publishes require a token with upload rights.
251
+ - **Trusted publishing**: for CI, you can configure [PyPI trusted publishers](https://docs.pypi.org/trusted-publishers/) (e.g. GitHub Actions) instead of long-lived API tokens; this repo does not include a publish workflow yet.
252
+ - **Alternative**: `python -m build` plus `twine upload dist/*` works if you prefer not to use `uv publish`; keep using the same `pyproject.toml` / `uv_build` backend.
@@ -0,0 +1,117 @@
1
+ [project]
2
+ name = "async-redis-client"
3
+ version = "0.1.0"
4
+ description = "Hexagonal cache library with sync/async Redis adapters, Fernet encryption, and Pydantic JSON."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "mato777", email = "matias@landjourney.ai" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.11"
12
+ keywords = ["redis", "cache", "async", "fernet", "pydantic", "hexagonal"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Database",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "cryptography>=48.0.0",
26
+ "pydantic>=2.13.4",
27
+ "redis>=7.4.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/mato777/redis-adapter"
32
+ Repository = "https://github.com/mato777/redis-adapter"
33
+ Documentation = "https://github.com/mato777/redis-adapter#readme"
34
+ Issues = "https://github.com/mato777/redis-adapter/issues"
35
+
36
+ [build-system]
37
+ requires = ["uv_build>=0.11.14,<0.12.0"]
38
+ build-backend = "uv_build"
39
+
40
+ [tool.uv]
41
+ package = true
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "fakeredis>=2.35.1",
46
+ "pytest>=9.0.3",
47
+ "pytest-asyncio>=1.3.0",
48
+ "pytest-cov>=7.1.0",
49
+ "pytest-sugar>=1.1.1",
50
+ "ruff>=0.14.15",
51
+ "taskipy>=1.14.1",
52
+ "testcontainers[redis]>=4.14.2",
53
+ "ty>=0.0.14",
54
+ ]
55
+
56
+ [tool.taskipy.tasks]
57
+ lint = "uv run ruff check src tests"
58
+ format = "uv run ruff format src tests"
59
+ format-check = "uv run ruff format --check src tests"
60
+ test = "uv run pytest -m \"not e2e\""
61
+ test-all = "uv run pytest"
62
+ test-cov = "uv run pytest --cov=async_redis_client --cov-report=term-missing --cov-report=html"
63
+ install-git-hooks = "git config core.hooksPath .githooks"
64
+
65
+ [tool.pytest.ini_options]
66
+ asyncio_mode = "auto"
67
+ asyncio_default_fixture_loop_scope = "function"
68
+ markers = [
69
+ "e2e: spins up Redis in Docker via testcontainers",
70
+ ]
71
+
72
+ [tool.coverage.run]
73
+ source_pkgs = ["async_redis_client"]
74
+ branch = true
75
+
76
+ [tool.coverage.report]
77
+ show_missing = true
78
+
79
+ [tool.coverage.html]
80
+ directory = "htmlcov"
81
+
82
+ [tool.ruff]
83
+ target-version = "py311"
84
+ line-length = 88
85
+ src = ["src"]
86
+
87
+ [tool.ruff.lint]
88
+ extend-select = [
89
+ "B", # flake8-bugbear
90
+ "C4", # flake8-comprehensions
91
+ "I", # isort
92
+ "SIM", # flake8-simplify
93
+ "UP", # pyupgrade
94
+ ]
95
+ ignore = []
96
+
97
+ [tool.ruff.lint.isort]
98
+ known-first-party = ["async_redis_client"]
99
+
100
+ [tool.ty.environment]
101
+ python-version = "3.11"
102
+ root = ["./src"]
103
+
104
+ [tool.ty.src]
105
+ include = ["src", "tests"]
106
+
107
+ [[tool.ty.overrides]]
108
+ include = [
109
+ "src/async_redis_client/adapters/redis/**/*.py",
110
+ "tests/**/*.py",
111
+ ]
112
+
113
+ [tool.ty.overrides.rules]
114
+ invalid-argument-type = "ignore"
115
+ not-iterable = "ignore"
116
+ unsupported-operator = "ignore"
117
+ unused-ignore-comment = "ignore"