cache-sync 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cache_sync/__init__.py +80 -0
- cache_sync/core.py +256 -0
- cache_sync/decorators.py +86 -0
- cache_sync/distributed_cache.py +16 -0
- cache_sync/invalidation.py +111 -0
- cache_sync/providers/__init__.py +1 -0
- cache_sync/providers/kafka/__init__.py +7 -0
- cache_sync/providers/kafka/invalidation_bus.py +173 -0
- cache_sync/providers/postgres/__init__.py +7 -0
- cache_sync/providers/postgres/invalidation_bus.py +129 -0
- cache_sync/providers/rabbitmq/__init__.py +7 -0
- cache_sync/providers/rabbitmq/invalidation_bus.py +168 -0
- cache_sync/providers/redis/__init__.py +9 -0
- cache_sync/providers/redis/cache.py +52 -0
- cache_sync/providers/redis/invalidation_bus.py +181 -0
- cache_sync/py.typed +1 -0
- cache_sync/serializers.py +83 -0
- cache_sync-0.3.1.dist-info/METADATA +140 -0
- cache_sync-0.3.1.dist-info/RECORD +20 -0
- cache_sync-0.3.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from redis.asyncio import Redis
|
|
11
|
+
from redis.exceptions import ResponseError
|
|
12
|
+
from redis.typing import EncodableT, FieldT
|
|
13
|
+
|
|
14
|
+
from cache_sync.invalidation import (
|
|
15
|
+
ClearLocal,
|
|
16
|
+
InvalidationAction,
|
|
17
|
+
InvalidationMessage,
|
|
18
|
+
RemoveLocal,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type RedisFields = Mapping[bytes | str, bytes | str]
|
|
22
|
+
type RedisMessage = tuple[bytes | str, RedisFields]
|
|
23
|
+
type RedisStreamResponse = list[tuple[bytes | str, list[RedisMessage]]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RedisStreamsInvalidationBus:
|
|
27
|
+
"""Invalidation bus backed by Redis Streams consumer groups."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
redis: Redis,
|
|
32
|
+
*,
|
|
33
|
+
stream_name: str = "cache-sync:invalidations",
|
|
34
|
+
node_name: str | None = None,
|
|
35
|
+
max_length: int = 10_000,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Create a Redis Streams invalidation bus."""
|
|
38
|
+
|
|
39
|
+
self._redis = redis
|
|
40
|
+
self._stream_name = stream_name
|
|
41
|
+
self._source_id = str(uuid.uuid4())
|
|
42
|
+
self._node_name = node_name or f"{socket.gethostname()}-{self._source_id}"
|
|
43
|
+
self._group_name = f"cache-sync-node:{self._node_name}"
|
|
44
|
+
self._consumer_name = self._node_name
|
|
45
|
+
self._max_length = max_length
|
|
46
|
+
self._remove_local: RemoveLocal | None = None
|
|
47
|
+
self._clear_local: ClearLocal | None = None
|
|
48
|
+
self._listener_task: asyncio.Task[None] | None = None
|
|
49
|
+
self._stopped = asyncio.Event()
|
|
50
|
+
|
|
51
|
+
async def start(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
remove_local: RemoveLocal,
|
|
55
|
+
clear_local: ClearLocal,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Create the consumer group if needed and start the listener task."""
|
|
58
|
+
|
|
59
|
+
if self._listener_task is not None:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
self._remove_local = remove_local
|
|
63
|
+
self._clear_local = clear_local
|
|
64
|
+
self._stopped.clear()
|
|
65
|
+
await self._ensure_group()
|
|
66
|
+
self._listener_task = asyncio.create_task(self._listen())
|
|
67
|
+
|
|
68
|
+
async def stop(self) -> None:
|
|
69
|
+
"""Cancel the listener task and clear local callbacks."""
|
|
70
|
+
|
|
71
|
+
self._stopped.set()
|
|
72
|
+
|
|
73
|
+
if self._listener_task is None:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self._listener_task.cancel()
|
|
77
|
+
|
|
78
|
+
with suppress(asyncio.CancelledError):
|
|
79
|
+
await self._listener_task
|
|
80
|
+
|
|
81
|
+
self._listener_task = None
|
|
82
|
+
self._remove_local = None
|
|
83
|
+
self._clear_local = None
|
|
84
|
+
|
|
85
|
+
async def invalidate(self, key: str) -> None:
|
|
86
|
+
"""Publish a key-removal message to the stream."""
|
|
87
|
+
|
|
88
|
+
await self._publish(InvalidationMessage.remove(key))
|
|
89
|
+
|
|
90
|
+
async def clear(self) -> None:
|
|
91
|
+
"""Publish a clear-all message to the stream."""
|
|
92
|
+
|
|
93
|
+
await self._publish(InvalidationMessage.clear())
|
|
94
|
+
|
|
95
|
+
async def _publish(self, message: InvalidationMessage) -> None:
|
|
96
|
+
fields: dict[FieldT, EncodableT] = {
|
|
97
|
+
"action": message.action,
|
|
98
|
+
"source_id": self._source_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if message.key is not None:
|
|
102
|
+
fields["key"] = message.key
|
|
103
|
+
|
|
104
|
+
await self._redis.xadd(
|
|
105
|
+
self._stream_name,
|
|
106
|
+
fields,
|
|
107
|
+
maxlen=self._max_length,
|
|
108
|
+
approximate=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def _ensure_group(self) -> None:
|
|
112
|
+
try:
|
|
113
|
+
await self._redis.xgroup_create(
|
|
114
|
+
self._stream_name,
|
|
115
|
+
self._group_name,
|
|
116
|
+
id="$",
|
|
117
|
+
mkstream=True,
|
|
118
|
+
)
|
|
119
|
+
except ResponseError as ex:
|
|
120
|
+
if "BUSYGROUP" not in str(ex):
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
async def _listen(self) -> None:
|
|
124
|
+
while not self._stopped.is_set():
|
|
125
|
+
response = cast(
|
|
126
|
+
RedisStreamResponse,
|
|
127
|
+
await self._redis.xreadgroup(
|
|
128
|
+
groupname=self._group_name,
|
|
129
|
+
consumername=self._consumer_name,
|
|
130
|
+
streams={self._stream_name: ">"},
|
|
131
|
+
count=25,
|
|
132
|
+
block=5_000,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for _, messages in response:
|
|
137
|
+
for message_id, fields in messages:
|
|
138
|
+
await self._process_message(message_id, fields)
|
|
139
|
+
|
|
140
|
+
async def _process_message(
|
|
141
|
+
self,
|
|
142
|
+
message_id: bytes | str,
|
|
143
|
+
fields: RedisFields,
|
|
144
|
+
) -> None:
|
|
145
|
+
source_id = self._get_field(fields, "source_id")
|
|
146
|
+
|
|
147
|
+
if source_id != self._source_id:
|
|
148
|
+
self._apply_message(self._to_message(fields))
|
|
149
|
+
|
|
150
|
+
await self._redis.xack(
|
|
151
|
+
self._stream_name,
|
|
152
|
+
self._group_name,
|
|
153
|
+
message_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _apply_message(self, message: InvalidationMessage) -> None:
|
|
157
|
+
if message.action == "remove" and message.key is not None:
|
|
158
|
+
remove_local = self._remove_local
|
|
159
|
+
if remove_local is not None:
|
|
160
|
+
remove_local(message.key)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if message.action == "clear":
|
|
164
|
+
clear_local = self._clear_local
|
|
165
|
+
if clear_local is not None:
|
|
166
|
+
clear_local()
|
|
167
|
+
|
|
168
|
+
def _to_message(self, fields: RedisFields) -> InvalidationMessage:
|
|
169
|
+
action = cast(InvalidationAction, self._get_field(fields, "action"))
|
|
170
|
+
|
|
171
|
+
if action == "remove":
|
|
172
|
+
return InvalidationMessage.remove(self._get_field(fields, "key"))
|
|
173
|
+
|
|
174
|
+
return InvalidationMessage.clear()
|
|
175
|
+
|
|
176
|
+
def _get_field(self, fields: RedisFields, key: str) -> str:
|
|
177
|
+
value = fields.get(key)
|
|
178
|
+
if value is None:
|
|
179
|
+
value = fields[key.encode("utf-8")]
|
|
180
|
+
|
|
181
|
+
return value.decode("utf-8") if isinstance(value, bytes) else value
|
cache_sync/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pickle
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Generic, Protocol, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Serializer(Protocol):
|
|
12
|
+
"""Protocol for converting distributed-cache values to and from bytes."""
|
|
13
|
+
|
|
14
|
+
def dumps(self, value: object) -> bytes: ...
|
|
15
|
+
|
|
16
|
+
def loads(self, value: bytes) -> object: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PickleSerializer:
|
|
20
|
+
"""Serialize arbitrary trusted Python objects with pickle."""
|
|
21
|
+
|
|
22
|
+
def dumps(self, value: object) -> bytes:
|
|
23
|
+
"""Serialize a Python object to bytes."""
|
|
24
|
+
|
|
25
|
+
return pickle.dumps(value)
|
|
26
|
+
|
|
27
|
+
def loads(self, value: bytes) -> object:
|
|
28
|
+
"""Deserialize bytes into a Python object."""
|
|
29
|
+
|
|
30
|
+
return pickle.loads(value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JsonSerializer:
|
|
34
|
+
"""Serialize JSON-compatible values as UTF-8 JSON bytes."""
|
|
35
|
+
|
|
36
|
+
def dumps(self, value: object) -> bytes:
|
|
37
|
+
"""Serialize a JSON-compatible value to bytes."""
|
|
38
|
+
|
|
39
|
+
return json.dumps(value).encode("utf-8")
|
|
40
|
+
|
|
41
|
+
def loads(self, value: bytes) -> object:
|
|
42
|
+
"""Deserialize UTF-8 JSON bytes."""
|
|
43
|
+
|
|
44
|
+
return json.loads(value.decode("utf-8"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PydanticSerializer(Generic[T]):
|
|
48
|
+
"""Serialize and deserialize Pydantic model instances."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, model_type: type[T]) -> None:
|
|
51
|
+
"""Create a serializer for the supplied Pydantic model type."""
|
|
52
|
+
|
|
53
|
+
self._model_type = model_type
|
|
54
|
+
|
|
55
|
+
def dumps(self, value: object) -> bytes:
|
|
56
|
+
"""Serialize a Pydantic model instance to JSON bytes."""
|
|
57
|
+
|
|
58
|
+
model_dump_json = getattr(value, "model_dump_json", None)
|
|
59
|
+
if callable(model_dump_json):
|
|
60
|
+
return cast(Callable[[], str], model_dump_json)().encode("utf-8")
|
|
61
|
+
|
|
62
|
+
json_method = getattr(value, "json", None)
|
|
63
|
+
if callable(json_method):
|
|
64
|
+
return cast(Callable[[], str], json_method)().encode("utf-8")
|
|
65
|
+
|
|
66
|
+
msg = "PydanticSerializer can only dump Pydantic model instances"
|
|
67
|
+
raise TypeError(msg)
|
|
68
|
+
|
|
69
|
+
def loads(self, value: bytes) -> T:
|
|
70
|
+
"""Deserialize JSON bytes into the configured Pydantic model type."""
|
|
71
|
+
|
|
72
|
+
raw = value.decode("utf-8")
|
|
73
|
+
|
|
74
|
+
model_validate_json = getattr(self._model_type, "model_validate_json", None)
|
|
75
|
+
if callable(model_validate_json):
|
|
76
|
+
return cast(Callable[[str], T], model_validate_json)(raw)
|
|
77
|
+
|
|
78
|
+
parse_raw = getattr(self._model_type, "parse_raw", None)
|
|
79
|
+
if callable(parse_raw):
|
|
80
|
+
return cast(Callable[[str], T], parse_raw)(raw)
|
|
81
|
+
|
|
82
|
+
msg = "PydanticSerializer requires a Pydantic model type"
|
|
83
|
+
raise TypeError(msg)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cache-sync
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Async hybrid Python cache with in-memory L1, distributed L2 providers, pluggable invalidation, stampede protection, and typed decorators.
|
|
5
|
+
Keywords: async,cache,redis,invalidation,stampede-protection
|
|
6
|
+
Author: Peter Cinibulk
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: redis>=5.0.0 ; extra == 'all'
|
|
20
|
+
Requires-Dist: aio-pika>=9.0.0 ; extra == 'all'
|
|
21
|
+
Requires-Dist: aiokafka>=0.10.0 ; extra == 'all'
|
|
22
|
+
Requires-Dist: asyncpg>=0.29.0 ; extra == 'all'
|
|
23
|
+
Requires-Dist: pydantic>=1.10.0 ; extra == 'all'
|
|
24
|
+
Requires-Dist: aiokafka>=0.10.0 ; extra == 'kafka'
|
|
25
|
+
Requires-Dist: asyncpg>=0.29.0 ; extra == 'postgres'
|
|
26
|
+
Requires-Dist: pydantic>=1.10.0 ; extra == 'pydantic'
|
|
27
|
+
Requires-Dist: aio-pika>=9.0.0 ; extra == 'rabbitmq'
|
|
28
|
+
Requires-Dist: redis>=5.0.0 ; extra == 'redis'
|
|
29
|
+
Requires-Python: >=3.12
|
|
30
|
+
Project-URL: Changelog, https://github.com/petercinibulk/cache-sync/blob/main/CHANGELOG.md
|
|
31
|
+
Project-URL: Documentation, https://petercinibulk.github.io/cache-sync/
|
|
32
|
+
Project-URL: Issues, https://github.com/petercinibulk/cache-sync/issues
|
|
33
|
+
Project-URL: Repository, https://github.com/petercinibulk/cache-sync
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Provides-Extra: kafka
|
|
36
|
+
Provides-Extra: postgres
|
|
37
|
+
Provides-Extra: pydantic
|
|
38
|
+
Provides-Extra: rabbitmq
|
|
39
|
+
Provides-Extra: redis
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# cache-sync
|
|
43
|
+
|
|
44
|
+
Async hybrid Python cache with in-memory L1 caching, optional Redis L2 caching, pluggable invalidation, stampede protection, fail-safe stale values, and typed decorators.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- Async-first API for Python 3.12 and newer.
|
|
49
|
+
- Fast in-process L1 cache with optional Redis-backed L2 storage.
|
|
50
|
+
- Pluggable invalidation buses for Redis Streams, RabbitMQ, Kafka, and PostgreSQL.
|
|
51
|
+
- Request stampede protection with per-key refresh coordination.
|
|
52
|
+
- Fail-safe stale reads for short backend outages.
|
|
53
|
+
- Typed decorators that preserve the wrapped function signature.
|
|
54
|
+
- Serializer choices for JSON, pickle, and Pydantic models.
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
The end-user documentation is published at <https://petercinibulk.github.io/cache-sync/> and is built from [`docs/`](docs/index.md) with Zensical.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv add cache-sync
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Install optional providers only when your application uses them:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv add "cache-sync[redis]"
|
|
70
|
+
uv add "cache-sync[rabbitmq]"
|
|
71
|
+
uv add "cache-sync[kafka]"
|
|
72
|
+
uv add "cache-sync[postgres]"
|
|
73
|
+
uv add "cache-sync[all]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Extra | Installs | Use when |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `redis` | `redis` | You need Redis L2 storage or Redis Streams invalidation. |
|
|
79
|
+
| `rabbitmq` | `aio-pika` | You use RabbitMQ as the invalidation bus. |
|
|
80
|
+
| `kafka` | `aiokafka` | You use Kafka as the invalidation bus. |
|
|
81
|
+
| `postgres` | `asyncpg` | You use PostgreSQL `LISTEN`/`NOTIFY` for invalidation. |
|
|
82
|
+
| `pydantic` | `pydantic` | You want Pydantic model serialization helpers. |
|
|
83
|
+
| `all` | all provider dependencies | You want every optional provider available. |
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from cache_sync import CacheOptions, CacheSync
|
|
89
|
+
|
|
90
|
+
cache = CacheSync(
|
|
91
|
+
options=CacheOptions(
|
|
92
|
+
ttl_seconds=60,
|
|
93
|
+
fail_safe_seconds=300,
|
|
94
|
+
hard_timeout_seconds=5,
|
|
95
|
+
jitter_seconds=5,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
await cache.start()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@cache.cached(lambda user_id: f"user:{user_id}")
|
|
103
|
+
async def get_user(user_id: str) -> dict[str, str]:
|
|
104
|
+
return {"id": user_id, "name": "Peter"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
user = await get_user("123")
|
|
108
|
+
await get_user.remove_cached("123")
|
|
109
|
+
await cache.stop()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Redis L2 Example
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from redis.asyncio import Redis
|
|
116
|
+
|
|
117
|
+
from cache_sync import CacheOptions, CacheSync, RedisDistributedCache
|
|
118
|
+
|
|
119
|
+
redis = Redis.from_url("redis://localhost:6379/0")
|
|
120
|
+
|
|
121
|
+
cache = CacheSync(
|
|
122
|
+
distributed_cache=RedisDistributedCache(redis),
|
|
123
|
+
options=CacheOptions(ttl_seconds=60, fail_safe_seconds=300),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await cache.start()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@cache.cached(lambda product_id: f"product:{product_id}")
|
|
130
|
+
async def get_product(product_id: str) -> dict[str, str]:
|
|
131
|
+
return {"id": product_id}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For a complete walkthrough with shared values and cross-instance invalidation, see the [get started tutorial](https://petercinibulk.github.io/cache-sync/tutorials/get-started/).
|
|
135
|
+
|
|
136
|
+
## Project
|
|
137
|
+
|
|
138
|
+
- License: MIT
|
|
139
|
+
- Source: <https://github.com/petercinibulk/cache-sync>
|
|
140
|
+
- Issues: <https://github.com/petercinibulk/cache-sync/issues>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cache_sync/__init__.py,sha256=av8jQbh1cKp0hXgxrBg8cJ_o3nEswH7OZ4M6xcshdd0,2272
|
|
2
|
+
cache_sync/core.py,sha256=XPBSn4cBfjeT-UXgs_4j6-TJXLbu2VLPgZ8cMdonLw4,7964
|
|
3
|
+
cache_sync/decorators.py,sha256=EbZkDiertdt5RYHO4dKe643-RDjme0RldqREBtkiAB4,2917
|
|
4
|
+
cache_sync/distributed_cache.py,sha256=x6US0OCJXNpJBvLGrVAQDGonekI3RcEjWF_AK-Zr5So,481
|
|
5
|
+
cache_sync/invalidation.py,sha256=vhYvpWstVXRCbDhKcHGSSjHnve7DYMDD3mciitgBJhw,3308
|
|
6
|
+
cache_sync/providers/__init__.py,sha256=vFj6aHry3F1rSqDOAUzlo_weG-ymXsXTPalNMhU4PMQ,47
|
|
7
|
+
cache_sync/providers/kafka/__init__.py,sha256=PGhdXK_FMLKKrJeq0zvIOxcvFuwvnXzgH-SgSsjU_XI,151
|
|
8
|
+
cache_sync/providers/kafka/invalidation_bus.py,sha256=LiNIzdomR50gPDxcTrCTDaX9UEhWG9mmsTIU4oOEXFE,5620
|
|
9
|
+
cache_sync/providers/postgres/__init__.py,sha256=10deqZGvUbRUQ3EZfsHMTq0Vb-Lp5TlNOWy0cX-sFSk,177
|
|
10
|
+
cache_sync/providers/postgres/invalidation_bus.py,sha256=D2XwceJvHJXOj58cnXhLu_BlZTbMSSODlnjrKfzjU78,3803
|
|
11
|
+
cache_sync/providers/rabbitmq/__init__.py,sha256=RmdeHVqoRqDaQil6tjMG0heV3htgxgzgaUYNwAvbUhA,163
|
|
12
|
+
cache_sync/providers/rabbitmq/invalidation_bus.py,sha256=X57JkfMHU3FjBslLT5aYKPnTwh0xrpsRVBeJvoH_T2E,5613
|
|
13
|
+
cache_sync/providers/redis/__init__.py,sha256=L5HFwdqHdjURLjR0rx4r_FleFiLaIedZ6KjIC1SlKeA,261
|
|
14
|
+
cache_sync/providers/redis/cache.py,sha256=cl7Q12vydsKLy_LfK26eqjS0bsFOxY1caELX6Lb2L00,1467
|
|
15
|
+
cache_sync/providers/redis/invalidation_bus.py,sha256=XDO6NdgIXhxsUOsZF7j0BmuBzn0faOWQjWWCW-MdvKA,5523
|
|
16
|
+
cache_sync/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
17
|
+
cache_sync/serializers.py,sha256=Ga3VzqVmMIK_dSYhF4YyQZhQDOup93wrnB1ZrBgHLbM,2552
|
|
18
|
+
cache_sync-0.3.1.dist-info/WHEEL,sha256=oBsDExVIEya4llboy9Ce1l6on8xt3GrtT29y6pYVypw,81
|
|
19
|
+
cache_sync-0.3.1.dist-info/METADATA,sha256=kcyDd3_fKmINbSNwUGOXHyXUYHQ16VbWIG8nAbmdAVo,4732
|
|
20
|
+
cache_sync-0.3.1.dist-info/RECORD,,
|