ddredis 0.0.1__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.
ddredis-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artem Davydov
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.
ddredis-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: ddredis
3
+ Version: 0.0.1
4
+ Summary: Redis helpers: generic JSON cache and distributed lock
5
+ Keywords: python,redis,cache,lock,ddredis
6
+ Author: davyddd
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Typing :: Typed
14
+ Requires-Dist: redis>=5.0,<9
15
+ Requires-Dist: ddutils>=0.2.2,<0.3.0
16
+ Requires-Python: >=3.12, <3.15
17
+ Project-URL: Repository, https://github.com/davyddd/ddredis
18
+ Description-Content-Type: text/markdown
19
+
20
+ # ddredis
21
+
22
+ [![pypi](https://img.shields.io/pypi/v/ddredis.svg)](https://pypi.python.org/pypi/ddredis)
23
+ [![downloads](https://static.pepy.tech/badge/ddredis/month)](https://pepy.tech/project/ddredis)
24
+ [![versions](https://img.shields.io/pypi/pyversions/ddredis.svg)](https://github.com/davyddd/ddredis)
25
+ [![codecov](https://codecov.io/gh/davyddd/ddredis/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddredis)
26
+ [![license](https://img.shields.io/github/license/davyddd/ddredis.svg)](https://github.com/davyddd/ddredis/blob/main/LICENSE)
27
+
28
+ Redis helpers: generic JSON cache and distributed lock
29
+
30
+ ## Installation
31
+
32
+ Install the library using pip:
33
+ ```bash
34
+ pip install ddredis
35
+ ```
36
+
37
+ ## Development
38
+
39
+ The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
40
+
41
+ ```bash
42
+ fab build # build the dev image
43
+ fab tests # run pytest
44
+ fab linters # run ruff (with --fix), ty and complexipy
45
+ fab shell # IPython inside the container
46
+ fab bash # bash inside the container
47
+ ```
48
+
49
+ This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
50
+ run `copier update` to pull in template updates.
51
+
52
+ ## GenericCache
53
+
54
+ Caches domain objects in Redis as JSON. The domain class must provide `model_validate_json` and
55
+ `model_dump_json` (pydantic models do), keys are prefixed with the snake-cased domain name, every entry gets
56
+ the class `ttl` with ±10% jitter so a burst of writes does not expire at once, and Redis errors turn into
57
+ "not found" instead of failing the caller.
58
+
59
+ ```python
60
+ from typing import ClassVar
61
+
62
+ from pydantic import BaseModel
63
+ from redis.asyncio import Redis
64
+
65
+ from ddredis.cache import GenericCache
66
+
67
+ redis_client = Redis.from_url('redis://localhost/0')
68
+
69
+
70
+ class Profile(BaseModel):
71
+ profile_id: int
72
+ name: str
73
+
74
+
75
+ class ProfileCache(GenericCache[Profile]):
76
+ ttl = 10 * 60 # seconds
77
+ redis_client: ClassVar[Redis] = redis_client
78
+
79
+
80
+ cache = ProfileCache()
81
+
82
+ await cache.create(key=1, value=Profile(profile_id=1, name='John')) # SET :profile:1 ... EX ttl±10%
83
+ profile = await cache.get(key=1) # Profile | None
84
+ await cache.update(key=1, value=profile) # alias for create
85
+ await cache.delete(key=1)
86
+
87
+ await cache.get_list(key_prefix='team-a') # every :profile:team-a* entry
88
+ await cache.delete_by_filters(key_prefix='team-a')
89
+ ```
90
+
91
+ Redis errors are swallowed on purpose: for a cache, "unavailable" and "miss" lead to the same fallback. Do
92
+ not use the cache for state where `None` has a meaning of its own. Transient failures (a worker that was just
93
+ reloaded, a dropped connection) belong to the client, not to the cache: redis-py retries the errors listed in
94
+ `retry_on_error` with its default `Retry` (3 attempts, exponential backoff with jitter), so a production client
95
+ looks like this:
96
+
97
+ ```python
98
+ from redis.asyncio import Redis
99
+ from redis.exceptions import ConnectionError, TimeoutError
100
+
101
+ redis_client = Redis.from_url(
102
+ 'redis://localhost/0',
103
+ max_connections=20,
104
+ socket_connect_timeout=5,
105
+ socket_timeout=1,
106
+ retry_on_error=[ConnectionError, TimeoutError],
107
+ decode_responses=True,
108
+ )
109
+ ```
110
+
111
+ ## RedisLock
112
+
113
+ Distributed lock on `SET NX PX` with a Lua release (redis-py's `Lock`, `thread_local=False`). Acquiring a
114
+ held lock raises `AlreadyAcquiredError` unless `blocking=True`, in which case it waits up to `timeout`.
115
+
116
+ ```python
117
+ from ddredis.lock import AlreadyAcquiredError, RedisLock
118
+
119
+ try:
120
+ async with RedisLock(redis_client, 'sync-profiles', timeout=30):
121
+ await sync_profiles()
122
+ except AlreadyAcquiredError:
123
+ pass # another worker is on it
124
+ ```
125
+
126
+ `timeout` is the Redis-side TTL of the key, not a client-side cancellation: if the block runs longer, Redis
127
+ drops the key and a competitor may enter while the first holder is still running. Pick a timeout that
128
+ comfortably exceeds the worst-case runtime, or call `extend()` periodically as a heartbeat. `extend()`
129
+ replaces the TTL with the full `timeout` counted from now rather than adding to the remaining time, so a
130
+ long-lived holder beating steadily never pushes its expiry further out. `release()` tolerates a lock that
131
+ already expired.
132
+
133
+ ## suppress_redis_errors
134
+
135
+ Decorator for async functions: `RedisError` subclasses become a `None` result, everything else propagates.
136
+ `GenericCache` methods use it; apply it to your own Redis reads that should degrade instead of failing.
137
+
@@ -0,0 +1,118 @@
1
+ # ddredis
2
+
3
+ [![pypi](https://img.shields.io/pypi/v/ddredis.svg)](https://pypi.python.org/pypi/ddredis)
4
+ [![downloads](https://static.pepy.tech/badge/ddredis/month)](https://pepy.tech/project/ddredis)
5
+ [![versions](https://img.shields.io/pypi/pyversions/ddredis.svg)](https://github.com/davyddd/ddredis)
6
+ [![codecov](https://codecov.io/gh/davyddd/ddredis/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddredis)
7
+ [![license](https://img.shields.io/github/license/davyddd/ddredis.svg)](https://github.com/davyddd/ddredis/blob/main/LICENSE)
8
+
9
+ Redis helpers: generic JSON cache and distributed lock
10
+
11
+ ## Installation
12
+
13
+ Install the library using pip:
14
+ ```bash
15
+ pip install ddredis
16
+ ```
17
+
18
+ ## Development
19
+
20
+ The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
21
+
22
+ ```bash
23
+ fab build # build the dev image
24
+ fab tests # run pytest
25
+ fab linters # run ruff (with --fix), ty and complexipy
26
+ fab shell # IPython inside the container
27
+ fab bash # bash inside the container
28
+ ```
29
+
30
+ This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
31
+ run `copier update` to pull in template updates.
32
+
33
+ ## GenericCache
34
+
35
+ Caches domain objects in Redis as JSON. The domain class must provide `model_validate_json` and
36
+ `model_dump_json` (pydantic models do), keys are prefixed with the snake-cased domain name, every entry gets
37
+ the class `ttl` with ±10% jitter so a burst of writes does not expire at once, and Redis errors turn into
38
+ "not found" instead of failing the caller.
39
+
40
+ ```python
41
+ from typing import ClassVar
42
+
43
+ from pydantic import BaseModel
44
+ from redis.asyncio import Redis
45
+
46
+ from ddredis.cache import GenericCache
47
+
48
+ redis_client = Redis.from_url('redis://localhost/0')
49
+
50
+
51
+ class Profile(BaseModel):
52
+ profile_id: int
53
+ name: str
54
+
55
+
56
+ class ProfileCache(GenericCache[Profile]):
57
+ ttl = 10 * 60 # seconds
58
+ redis_client: ClassVar[Redis] = redis_client
59
+
60
+
61
+ cache = ProfileCache()
62
+
63
+ await cache.create(key=1, value=Profile(profile_id=1, name='John')) # SET :profile:1 ... EX ttl±10%
64
+ profile = await cache.get(key=1) # Profile | None
65
+ await cache.update(key=1, value=profile) # alias for create
66
+ await cache.delete(key=1)
67
+
68
+ await cache.get_list(key_prefix='team-a') # every :profile:team-a* entry
69
+ await cache.delete_by_filters(key_prefix='team-a')
70
+ ```
71
+
72
+ Redis errors are swallowed on purpose: for a cache, "unavailable" and "miss" lead to the same fallback. Do
73
+ not use the cache for state where `None` has a meaning of its own. Transient failures (a worker that was just
74
+ reloaded, a dropped connection) belong to the client, not to the cache: redis-py retries the errors listed in
75
+ `retry_on_error` with its default `Retry` (3 attempts, exponential backoff with jitter), so a production client
76
+ looks like this:
77
+
78
+ ```python
79
+ from redis.asyncio import Redis
80
+ from redis.exceptions import ConnectionError, TimeoutError
81
+
82
+ redis_client = Redis.from_url(
83
+ 'redis://localhost/0',
84
+ max_connections=20,
85
+ socket_connect_timeout=5,
86
+ socket_timeout=1,
87
+ retry_on_error=[ConnectionError, TimeoutError],
88
+ decode_responses=True,
89
+ )
90
+ ```
91
+
92
+ ## RedisLock
93
+
94
+ Distributed lock on `SET NX PX` with a Lua release (redis-py's `Lock`, `thread_local=False`). Acquiring a
95
+ held lock raises `AlreadyAcquiredError` unless `blocking=True`, in which case it waits up to `timeout`.
96
+
97
+ ```python
98
+ from ddredis.lock import AlreadyAcquiredError, RedisLock
99
+
100
+ try:
101
+ async with RedisLock(redis_client, 'sync-profiles', timeout=30):
102
+ await sync_profiles()
103
+ except AlreadyAcquiredError:
104
+ pass # another worker is on it
105
+ ```
106
+
107
+ `timeout` is the Redis-side TTL of the key, not a client-side cancellation: if the block runs longer, Redis
108
+ drops the key and a competitor may enter while the first holder is still running. Pick a timeout that
109
+ comfortably exceeds the worst-case runtime, or call `extend()` periodically as a heartbeat. `extend()`
110
+ replaces the TTL with the full `timeout` counted from now rather than adding to the remaining time, so a
111
+ long-lived holder beating steadily never pushes its expiry further out. `release()` tolerates a lock that
112
+ already expired.
113
+
114
+ ## suppress_redis_errors
115
+
116
+ Decorator for async functions: `RedisError` subclasses become a `None` result, everything else propagates.
117
+ `GenericCache` methods use it; apply it to your own Redis reads that should degrade instead of failing.
118
+
@@ -0,0 +1,3 @@
1
+ from . import cache, decorators, lock
2
+
3
+ __all__ = ('cache', 'decorators', 'lock')
@@ -0,0 +1,141 @@
1
+ from abc import ABC
2
+ from functools import cached_property
3
+ from random import randint
4
+ from typing import ClassVar, Protocol, Self, TypeVar, cast
5
+
6
+ from ddutils.class_helpers import get_generic_base_argument
7
+ from ddutils.convertors import convert_camel_case_to_snake_case
8
+ from redis.asyncio import Redis
9
+
10
+ from ddredis.decorators import suppress_redis_errors
11
+
12
+ JITTER_PERCENT = 10
13
+
14
+
15
+ class Serializable(Protocol):
16
+ """Protocol for objects that can be serialized/deserialized to/from JSON."""
17
+
18
+ @classmethod
19
+ def model_validate_json(cls, json_data: str | bytes) -> Self: ...
20
+
21
+ def model_dump_json(self) -> str: ...
22
+
23
+
24
+ class Stringable(Protocol):
25
+ def __str__(self) -> str: ...
26
+
27
+
28
+ class GenericCache[DomainT: Serializable](ABC):
29
+ """
30
+ GenericCache: An async generic caching repository for managing domain objects in Redis.
31
+
32
+ This class provides a base implementation for caching domain objects in Redis.
33
+ It supports CRUD operations (`get`, `create`, `update`, `delete`)
34
+ and uses JSON serialization/deserialization to store and retrieve data.
35
+
36
+ Domain class must implement `model_validate_json` and `model_dump_json` methods
37
+ (compatible with Pydantic, msgspec, or custom implementations).
38
+
39
+ Class Attributes:
40
+ ttl (int): Time-to-live for cache entries in seconds. Default is 5 minutes.
41
+ redis_client (Redis): Redis client instance.
42
+
43
+ Example Usage:
44
+ redis_client = Redis.from_url('redis://localhost/0')
45
+
46
+ class MyDomain(BaseModel):
47
+ id: int
48
+ name: str
49
+
50
+ class MyDomainCache(GenericCache[MyDomain]):
51
+ ttl = 10 * 60 # 10 minutes
52
+ redis_client = redis_client
53
+
54
+ cache = MyDomainCache()
55
+
56
+ domain = MyDomain(id=1, name="Test")
57
+ await cache.create(key=domain.id, value=domain)
58
+ retrieved = await cache.get(key=1)
59
+ """
60
+
61
+ _domain_class: ClassVar[type[Serializable]]
62
+
63
+ ttl: ClassVar[int] = 5 * 60 # 5 minutes
64
+ redis_client: ClassVar[Redis]
65
+
66
+ def __init_subclass__(cls, **kwargs):
67
+ super().__init_subclass__(**kwargs)
68
+
69
+ domain_class = get_generic_base_argument(cls, GenericCache)
70
+ if domain_class is None or isinstance(domain_class, TypeVar):
71
+ raise TypeError(f'{cls.__name__} must specify domain type: class {cls.__name__}(GenericCache[YourDomain])')
72
+
73
+ cls._domain_class = domain_class
74
+
75
+ @cached_property
76
+ def _key_prefix(self) -> str:
77
+ return convert_camel_case_to_snake_case(self._domain_class.__name__)
78
+
79
+ @cached_property
80
+ def _jitter(self) -> int:
81
+ return self.ttl * JITTER_PERCENT // 100
82
+
83
+ def _generate_key(self, key: Stringable) -> str:
84
+ """Generates a Redis key for the given identifier with the domain-specific prefix."""
85
+ return f':{self._key_prefix}:{key!s}'
86
+
87
+ def _generate_ttl(self) -> int:
88
+ """Generates a TTL value with jitter."""
89
+ return self.ttl + randint(-self._jitter, self._jitter)
90
+
91
+ async def _get(self, key: str) -> DomainT | None:
92
+ """Reads and deserializes a domain object by its already-prefixed Redis key."""
93
+ cached_data = await self.redis_client.get(key)
94
+ if not cached_data:
95
+ return None
96
+ try:
97
+ return cast('DomainT', self._domain_class.model_validate_json(cached_data))
98
+ except Exception: # noqa: BLE001
99
+ return None
100
+
101
+ @suppress_redis_errors
102
+ async def get(self, key: Stringable) -> DomainT | None:
103
+ """Retrieves and deserializes a domain object from the cache by its key."""
104
+ return await self._get(self._generate_key(key))
105
+
106
+ @suppress_redis_errors
107
+ async def create(self, key: Stringable, value: DomainT) -> None:
108
+ """Caches a new domain object or overwrites an existing one. Sets a TTL for the entry."""
109
+ _key = self._generate_key(key)
110
+ await self.redis_client.set(_key, value.model_dump_json(), ex=self._generate_ttl())
111
+
112
+ @suppress_redis_errors
113
+ async def update(self, key: Stringable, value: DomainT) -> None:
114
+ """Updates a domain object in the cache. Alias for `create`."""
115
+ await self.create(key=key, value=value)
116
+
117
+ @suppress_redis_errors
118
+ async def delete(self, key: Stringable) -> None:
119
+ """Removes a domain object from the cache by its key."""
120
+ await self.redis_client.delete(self._generate_key(key))
121
+
122
+ @suppress_redis_errors
123
+ async def get_list(self, key_prefix: Stringable) -> list[DomainT]:
124
+ """Returns all cached domain objects whose keys start with the given prefix."""
125
+ pattern = f'{self._generate_key(key_prefix)}*'
126
+ result: list[DomainT] = []
127
+ async for redis_key in self.redis_client.scan_iter(match=pattern):
128
+ item = await self._get(redis_key)
129
+ if item is not None:
130
+ result.append(item)
131
+ return result
132
+
133
+ @suppress_redis_errors
134
+ async def delete_by_filters(self, key_prefix: Stringable) -> None:
135
+ """Removes all cached domain objects whose keys start with the given prefix."""
136
+ pattern = f'{self._generate_key(key_prefix)}*'
137
+ async for redis_key in self.redis_client.scan_iter(match=pattern):
138
+ await self.redis_client.delete(redis_key)
139
+
140
+
141
+ __all__ = ('GenericCache', 'Serializable', 'Stringable')
@@ -0,0 +1,21 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from functools import wraps
3
+ from typing import Any
4
+
5
+ from redis.exceptions import RedisError
6
+
7
+
8
+ def suppress_redis_errors[T](func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T | None]]:
9
+ """Decorator to suppress Redis exceptions and return None on failure."""
10
+
11
+ @wraps(func)
12
+ async def wrapper(*args: Any, **kwargs: Any) -> T | None:
13
+ try:
14
+ return await func(*args, **kwargs)
15
+ except RedisError:
16
+ return None
17
+
18
+ return wrapper
19
+
20
+
21
+ __all__ = ('suppress_redis_errors',)
@@ -0,0 +1,65 @@
1
+ import contextlib
2
+ from typing import Self
3
+
4
+ from redis.asyncio import Redis
5
+ from redis.asyncio.lock import Lock, LockNotOwnedError
6
+
7
+
8
+ class AlreadyAcquiredError(Exception): ...
9
+
10
+
11
+ class RedisLock:
12
+ """
13
+ Distributed lock backed by Redis (`SET NX PX` + Lua release).
14
+
15
+ Caveat: `timeout` is the Redis-side TTL of the key — not a client-side cancellation
16
+ timer. If the protected code runs longer than `timeout`, Redis silently removes the
17
+ key and a competing caller can acquire the same lock and execute in parallel,
18
+ violating mutual exclusion. The original code keeps running unaware. `release()`
19
+ swallows `LockNotOwnedError` for this case, so the cleanup is silent — but the
20
+ invariant has already been broken upstream.
21
+
22
+ To avoid this, either pick a `timeout` that comfortably exceeds the worst-case
23
+ runtime of the block, or call `extend()` periodically (heartbeat) for long
24
+ operations whose duration is hard to predict.
25
+ """
26
+
27
+ def __init__(self, redis_client: Redis, key: str, timeout: float = 10.0, blocking: bool = False):
28
+ blocking_timeout = timeout if blocking else None
29
+ self.timeout = timeout
30
+ self.lock = Lock(
31
+ redis=redis_client,
32
+ name=key,
33
+ timeout=timeout,
34
+ blocking=blocking,
35
+ blocking_timeout=blocking_timeout,
36
+ thread_local=False,
37
+ )
38
+
39
+ async def acquire(self) -> None:
40
+ acquired = await self.lock.acquire()
41
+
42
+ if not acquired:
43
+ raise AlreadyAcquiredError('Lock is already acquired')
44
+
45
+ async def release(self) -> None:
46
+ with contextlib.suppress(LockNotOwnedError):
47
+ await self.lock.release()
48
+
49
+ async def extend(self) -> None:
50
+ # `replace_ttl=True` because every caller means this as a heartbeat: the lock should
51
+ # outlive the holder by `timeout`, counted from now. redis-py's default ADDS to the
52
+ # remaining TTL, so a long-lived holder beating steadily would push the expiry further
53
+ # out with every beat — and a lock whose holder is killed would then sit unreleased for
54
+ # as long as it had been beating.
55
+ await self.lock.extend(self.timeout, replace_ttl=True)
56
+
57
+ async def __aenter__(self) -> Self:
58
+ await self.acquire()
59
+ return self
60
+
61
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
62
+ await self.release()
63
+
64
+
65
+ __all__ = ('AlreadyAcquiredError', 'RedisLock')
File without changes
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "ddredis"
3
+ description = "Redis helpers: generic JSON cache and distributed lock"
4
+ version = "0.0.1"
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ requires-python = ">=3.12,<3.15"
9
+ keywords = [
10
+ "python",
11
+ "redis",
12
+ "cache",
13
+ "lock",
14
+ "ddredis",
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "redis>=5.0,<9",
25
+ "ddutils>=0.2.2,<0.3.0",
26
+ ]
27
+
28
+ [[project.authors]]
29
+ name = "davyddd"
30
+
31
+ [project.urls]
32
+ Repository = "https://github.com/davyddd/ddredis"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "ipdb==0.13.13",
37
+ "ipython==9.16.1",
38
+ ]
39
+ test = [
40
+ "pytest==9.0.3",
41
+ "pytest-cov==5.0.0",
42
+ "parameterized==0.9.0",
43
+ "fakeredis[lua]>=2.26,<3",
44
+ "pydantic>=2.0,<3",
45
+ ]
46
+ linter = [
47
+ "ruff==0.15.10",
48
+ "ty==0.0.15",
49
+ "complexipy==8.0.1",
50
+ ]
51
+
52
+ [tool.uv.build-backend]
53
+ module-root = ""
54
+
55
+ [build-system]
56
+ requires = ["uv_build>=0.12.0,<0.13.0"]
57
+ build-backend = "uv_build"
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "ddredis"
3
+ description = "Redis helpers: generic JSON cache and distributed lock"
4
+ version = "0.0.1"
5
+ authors = [{ name = "davyddd" }]
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ readme = "README.md"
9
+ requires-python = ">=3.12,<3.15"
10
+ keywords = ["python", "redis", "cache", "lock", "ddredis"]
11
+ # uv_build does not derive these from requires-python (poetry did); PyPI and the pyversions badge read them
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = [
20
+ "redis>=5.0,<9",
21
+ "ddutils>=0.2.2,<0.3.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/davyddd/ddredis"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "ipdb==0.13.13",
30
+ "ipython==9.16.1",
31
+ ]
32
+ test = [
33
+ "pytest==9.0.3",
34
+ "pytest-cov==5.0.0",
35
+ "parameterized==0.9.0",
36
+ "fakeredis[lua]>=2.26,<3",
37
+ "pydantic>=2.0,<3", # the cache is tested with pydantic models, the library itself only needs the Serializable protocol
38
+ ]
39
+ linter = [
40
+ "ruff==0.15.10",
41
+ "ty==0.0.15",
42
+ "complexipy==8.0.1",
43
+ ]
44
+
45
+ [tool.uv.build-backend]
46
+ module-root = ""
47
+
48
+ [build-system]
49
+ requires = ["uv_build>=0.12.0,<0.13.0"]
50
+ build-backend = "uv_build"