dialcache 0.25.0__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.
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.5
2
+ Name: dialcache
3
+ Version: 0.25.0
4
+ Summary: Explicitly enabled async caching with runtime policies, coalescing, and tracked Redis invalidation.
5
+ Project-URL: Repository, https://github.com/lan17/DialCache
6
+ Project-URL: Documentation, https://github.com/lan17/DialCache/tree/main/python
7
+ Author: Lev Neiman
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: zstandard<1,>=0.23
19
+ Provides-Extra: redis
20
+ Requires-Dist: redis<7,>=5; extra == 'redis'
21
+ Provides-Extra: test
22
+ Requires-Dist: coverage<8,>=7.11; extra == 'test'
23
+ Requires-Dist: jsonschema<5,>=4.23; extra == 'test'
24
+ Requires-Dist: pytest-asyncio<2,>=0.24; extra == 'test'
25
+ Requires-Dist: pytest<10,>=8; extra == 'test'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # DialCache for Python
29
+
30
+ An asyncio port of DialCache for Python 3.11 and later. Each use case declares
31
+ its identity and policy; an enabled request scope opts into request memoization,
32
+ local storage, Redis, and concurrent request sharing. The behavioral contract is
33
+ the repository's [portable specification](https://github.com/lan17/DialCache/blob/main/formal/SPEC.md).
34
+
35
+ Once the first PyPI release is available, install it with:
36
+
37
+ ```sh
38
+ python3 -m pip install 'dialcache[redis]'
39
+ ```
40
+
41
+ Until that release is published, install from the root of a repository checkout:
42
+
43
+ ```sh
44
+ python3 -m pip install './python[redis]'
45
+ ```
46
+
47
+ For local-only caching, omit the `redis` extra. Zstandard is included for the
48
+ portable Redis payload format. The application owns the asyncio event loop and
49
+ any Redis client connections.
50
+
51
+ ## A cached function
52
+
53
+ ```python
54
+ from dialcache import DialCache, Policy
55
+
56
+ cache = DialCache(namespace="my-service")
57
+
58
+
59
+ @cache.cached(
60
+ use_case="user-profile",
61
+ key_type="user",
62
+ id_arg="user_id",
63
+ default_config=Policy(ttl_sec={"local": 5}, request_local=True),
64
+ )
65
+ async def get_profile(user_id: str) -> dict:
66
+ return await database.fetch_profile(user_id)
67
+
68
+
69
+ async def handle_request(user_id: str) -> dict:
70
+ async with cache.enable():
71
+ first = await get_profile(user_id)
72
+ second = await get_profile(user_id) # Same request memo.
73
+ return second
74
+ ```
75
+
76
+ Calls outside `enable()` go directly to the source. They do not construct cache
77
+ keys, resolve policy, share concurrent work, or apply a DialCache source
78
+ deadline. Both `with cache.enable():` and `async with cache.enable():` are valid;
79
+ the wrapped function is always awaitable. Synchronous loaders are accepted and
80
+ run on the event loop, so use async loaders for blocking I/O.
81
+
82
+ Nested enabled scopes share the live outer request memo. A nested
83
+ `cache.disable()` temporarily bypasses caching without deleting that memo.
84
+ Closing the outer scope clears the memo and prevents late publication. Async
85
+ tasks that inherited a scope use pass-through behavior for calls made after
86
+ that scope closes. Cache instances keep independent contexts.
87
+
88
+ ## Policies and runtime changes
89
+
90
+ No layer is enabled by default. A positive TTL enables that shared layer, with
91
+ a default rollout percentage of 100. Request memoization defaults to false;
92
+ concurrent same-key sharing defaults to true.
93
+
94
+ ```python
95
+ policy = Policy(
96
+ ttl_sec={"local": 5, "remote": 60},
97
+ ramp={"remote": 25},
98
+ request_local=True,
99
+ coalesce=True,
100
+ remote_read_timeout_ms=50,
101
+ stale_on_error_max_age_sec=120,
102
+ )
103
+ ```
104
+
105
+ TTLs are integer seconds from 1 through 31,536,000. Rollout percentages are
106
+ finite numbers from 0 through 100. Sampling is stable per exact key and layer,
107
+ using the same cohort algorithm as the TypeScript, Go, and Rust ports.
108
+
109
+ Pass a synchronous or asynchronous `policy_provider` to `DialCache` to resolve
110
+ runtime settings once per enabled invocation. It receives the structured key
111
+ and returns a `Policy`, a mapping, or `None`:
112
+
113
+ ```python
114
+ async def policy_provider(key):
115
+ if key.use_case == "user-profile":
116
+ return {"ramp": {"remote": 50}}
117
+ return None
118
+
119
+
120
+ cache = DialCache(policy_provider=policy_provider)
121
+ ```
122
+
123
+ Runtime replies are sparse: an omitted field inherits the operation default.
124
+ A whole reply of `None` inherits the complete operation policy. An explicit
125
+ `None` leaf is malformed and cannot silently inherit a valid setting. Python
126
+ snake_case names and the shared corpus's camelCase mapping names are accepted.
127
+ Policy objects snapshot their input maps so later mutation cannot alter an
128
+ already admitted invocation.
129
+
130
+ `Policy.disabled()` explicitly disables inherited request memoization, local
131
+ and remote serving, recovery, and shadow work. It does not cancel work that
132
+ was already admitted or disable explicit invalidation. `Policy.enabled(ttl)`
133
+ enables local and remote TTLs; it does not opt into request memoization.
134
+
135
+ Invalid static defaults raise `ConfigError` at registration. At runtime,
136
+ invalid TTLs or ramps disable their own layer; malformed boolean switches,
137
+ read deadlines, containers, or provider failures bypass caching for that
138
+ enabled invocation. Optional recovery and shadow failures leave ordinary
139
+ serving available.
140
+
141
+ ## Redis and tracked invalidation
142
+
143
+ ```python
144
+ from redis.asyncio import Redis
145
+ from dialcache import DialCache, Policy
146
+ from dialcache.redis import RedisAdapter
147
+
148
+ client = Redis.from_url(
149
+ "redis://localhost:6379",
150
+ decode_responses=False,
151
+ socket_connect_timeout=0.5,
152
+ socket_timeout=0.5,
153
+ )
154
+ cache = DialCache(redis=RedisAdapter(client))
155
+
156
+
157
+ @cache.cached(
158
+ use_case="user-profile",
159
+ key_type="user",
160
+ id_arg="user_id",
161
+ track_for_invalidation=True,
162
+ default_config=Policy(ttl_sec={"remote": 60}),
163
+ )
164
+ async def get_profile(user_id):
165
+ return await database.fetch_profile(user_id)
166
+
167
+
168
+ async def update_profile(user_id, changes):
169
+ await database.update_profile(user_id, changes)
170
+ await cache.invalidate_remote("user", user_id)
171
+ ```
172
+
173
+ The adapter borrows a `redis.asyncio.Redis` or `RedisCluster` client; close it
174
+ with `await client.aclose()` when your application shuts down. Configure
175
+ finite connection, socket, and retry budgets on the client. Tracked reads
176
+ atomically read the value and watermark from a primary. For tracked Cluster
177
+ reads, use a dedicated client constructed with primary-only defaults:
178
+ `read_from_replicas=False`, `load_balancing_strategy=None` where supported, and no custom
179
+ connection hook. Keep its configuration and connection mode unchanged while
180
+ borrowed. Do not repurpose a previously `READONLY` pool by resetting flags;
181
+ create a new primary-only client. Unsafe tracked reads raise `RedisProtocolError`
182
+ at the adapter boundary and ordinary cache calls fail open to the source.
183
+ Replica-enabled clients remain usable for untracked reads and maintenance.
184
+ Keys for one tracked entity share a Redis Cluster hash tag.
185
+
186
+ Each write stores a complete version-1 frame using one native `SET`. A tracked
187
+ frame is readable only if its writer timestamp is strictly greater than the
188
+ invalidation watermark. Value writes never create or extend watermarks.
189
+ Tracked physical value TTLs are capped at one hour. Invalidation raises on
190
+ mutation failure; ordinary cache plumbing fails open to the source.
191
+
192
+ Local storage is process-local. Remote invalidation does not synchronously
193
+ clear already warmed local entries or request memos on any instance. Choose
194
+ local TTLs with that explicit consistency limit in mind.
195
+
196
+ ## Deadlines, recovery, and observability
197
+
198
+ The default source deadline is 60,000 ms for enabled calls. The default Redis
199
+ read deadline is 50 ms and can be overridden by operation or runtime policy.
200
+ Deadline budgets are integer milliseconds from 1 through 2,147,483,647; an
201
+ explicit `fallback_timeout_ms=None` disables the source deadline. Timing uses
202
+ the monotonic clock, while Redis frames and invalidation use wall time.
203
+
204
+ Deadline expiration stops the caller's wait. It cannot retract a source
205
+ operation or a Redis command that already started. Late results cannot
206
+ publish through an expired source execution. Caller cancellation likewise
207
+ must not cancel another caller's shared execution.
208
+
209
+ Stale recovery is optional and requires a maximum age strictly greater than
210
+ the remote TTL. A valid candidate is retained from the original remote read;
211
+ an eligible source rejection can use it only before the exclusive maximum
212
+ age. The default recovery predicate admits DialCache's own
213
+ `FallbackTimeoutError`. Recovered values may memoize in still-open request
214
+ scopes; recovery does not refresh Redis or local storage.
215
+
216
+ Pass a synchronous `metrics` callback or an object with `observe(event)` to
217
+ receive the backend-neutral diagnostic event dictionaries. Their label names
218
+ match the shared contract, including `cacheNamespace`, `useCase`, `keyType`,
219
+ and `layer`. Observer failures do not alter cache results. Local capacity
220
+ defaults to 10,000 entries; zero capacity disables storage while preserving
221
+ eligible concurrent sharing.
222
+
223
+ ## Relationship to gcache
224
+
225
+ The Python API takes inspiration from [Galileo gcache](https://github.com/rungalileo/gcache):
226
+ decorated functions, argument-based identity, explicit context managers, and
227
+ pluggable serializers. DialCache follows its own portable
228
+ specification for behavior and wire compatibility.
229
+ The [API design notes](https://github.com/lan17/DialCache/blob/main/python/API-DESIGN.md) record the source-reviewed gcache revision
230
+ and the native API choices made for this port.
231
+
232
+ This binding exposes awaitable operations. It does not introduce a global
233
+ singleton, implicitly run synchronous I/O in a thread pool, serialize with
234
+ pickle, take ownership of Redis connections, or change the rollout cohort
235
+ randomly. Direct `put`, `delete`, and `flush` cache APIs from gcache are outside
236
+ DialCache's portable contract; writes come from successful source loads and
237
+ entity-level invalidation is explicit.
238
+
239
+ ## Development and conformance
240
+
241
+ From the repository root:
242
+
243
+ ```sh
244
+ python3 -m venv python/.venv
245
+ python/.venv/bin/python -m pip install -e './python[test,redis]'
246
+ python/.venv/bin/python -m pytest python/tests
247
+ ```
248
+
249
+ The native tests cover Python API behavior, policy validation, scope lifetime,
250
+ local expiry, cancellation, and wire boundaries. Shared replay runs the real
251
+ Python API through the repository's Node coordinator. Its inputs and expected
252
+ observations come from the same Quint-generated histories used by the other
253
+ ports; Node is a development dependency, not a runtime dependency of the
254
+ Python library. See [the porting guide](https://github.com/lan17/DialCache/blob/main/formal/PORTING.md) for the completion
255
+ and settlement requirements and [the feature map](https://github.com/lan17/DialCache/blob/main/formal/FEATURE-COVERAGE.md)
256
+ for portable behavior versus native adapter obligations.
@@ -0,0 +1,17 @@
1
+ dialcache/__init__.py,sha256=yq0319rG7aQnOIDXOClAfXdhWJwmPj8htcMcDp9loKo,956
2
+ dialcache/cache.py,sha256=68rfJwgHl-OGU5ueRhlwj6ptIm4R4x0LEU6sQH1zo0g,48995
3
+ dialcache/clock.py,sha256=U70P4pSlmDPUImK2NZTX9-dbtun5o8VFMdXOUZT4HS4,1603
4
+ dialcache/config.py,sha256=lnzlk4BppycDYgVaLO5DFXHdOkYpxOXrwYo6nnHevjo,10868
5
+ dialcache/context.py,sha256=5vRqEEAAaQ0abwelqUfEPMdrOxi0h6FbEjic_reWppg,4092
6
+ dialcache/errors.py,sha256=a95vzZCQGF7KdH59sJ4grlg_meKqt5oYF0gXfJ7aEUY,1580
7
+ dialcache/key.py,sha256=RBFPwX2gYEUaahkb8Aec7vBR0hMNta_R4vinAJF6uy4,5845
8
+ dialcache/local.py,sha256=y5hom-IVgaRY31N79RJjIiSkUSJNMG2LeNQ5RguW-8U,2526
9
+ dialcache/metrics.py,sha256=E9ufl9y1pmsMDkB0IjoMhzCfVR3Lz3aknnwxm5S9fhg,1290
10
+ dialcache/protocol.py,sha256=VFVfvE2dKil9G6IijpOIC_Q80VZNmA_2KqTn8jiDcJg,10510
11
+ dialcache/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ dialcache/redis.py,sha256=nhViSD9dV5BWM0_wglTWWHCvx0-nhCMgTWFHKRls1G0,8011
13
+ dialcache/serializer.py,sha256=kAXE6vv_OAPXW1ZN-ZtGliqQTUGLQR3mtKstrBJ4iVQ,1881
14
+ dialcache-0.25.0.dist-info/METADATA,sha256=8e-Lfv4cW1RRgP2ThHoSRVYgm38icTB9yk7ovOorBLk,10913
15
+ dialcache-0.25.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
+ dialcache-0.25.0.dist-info/licenses/LICENSE,sha256=I4t1F0sQnk9jCP2lTvTijI388Y-gGwHwXRcKCNp35tM,1082
17
+ dialcache-0.25.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Galileo Technologies Inc.
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.