nanocached 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Takashi Yamashina
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,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: nanocached
3
+ Version: 0.1.0
4
+ Summary: asyncio client SDK for nanocached, a tiny distributed cache with client-side replication
5
+ Author: Takashi Yamashina
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://nanocached.org
8
+ Project-URL: Repository, https://github.com/nanocached/nanocached
9
+ Project-URL: Issues, https://github.com/nanocached/nanocached/issues
10
+ Keywords: nanocached,cache,distributed-cache,rendezvous-hashing,client,sdk
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # nanocached (Python)
22
+
23
+ asyncio client SDK for [nanocached](https://github.com/nanocached/nanocached),
24
+ a tiny distributed cache. Talks to either a single `nanocached-node` or a
25
+ `nanocached-discovery`-fronted cluster — the SDK figures out which from the
26
+ server's own handshake, so the calling code is identical either way.
27
+
28
+ Requires Python 3.11+. No runtime dependencies.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ pip install nanocached
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import asyncio
40
+ from nanocached import NanocachedClient
41
+
42
+ async def main():
43
+ # Point at a single node, or at a discovery server fronting a
44
+ # cluster — same call either way. `addresses` always takes a list;
45
+ # a one-element list is the single-target case.
46
+ client = await NanocachedClient.connect([("127.0.0.1", 8357)])
47
+
48
+ await client.set("greeting", "hello", ttl_seconds=60)
49
+ value = await client.get("greeting") # str | None
50
+ print(value) # hello
51
+ existed = await client.delete("greeting") # bool
52
+
53
+ client.close()
54
+
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ Or use it as an async context manager, which closes the client for you:
59
+
60
+ ```python
61
+ async with await NanocachedClient.connect([("127.0.0.1", 8357)]) as client:
62
+ await client.set("greeting", "hello")
63
+ print(await client.get("greeting"))
64
+ ```
65
+
66
+ Keys may be `str` (encoded as UTF-8) or `bytes`; values may likewise be
67
+ `str` or `bytes` on the way in. `get(key)` strictly decodes the stored
68
+ value as UTF-8 and returns `str | None` — a value that isn't valid UTF-8
69
+ raises `UnicodeDecodeError` rather than silently mangling it. Use
70
+ `get_bytes(key) -> bytes | None` for the raw bytes.
71
+
72
+ ## Discovery replicas
73
+
74
+ When the cluster runs more than one discovery server, pass them all in
75
+ `addresses`; both the initial connect and every node-list refresh try them
76
+ in order. An address that is warming up after a restart (answers `B`) is
77
+ skipped like an unreachable one; if every address is warming up, `connect()`
78
+ raises `DiscoveryBusyError` — retry shortly.
79
+
80
+ ```python
81
+ client = await NanocachedClient.connect([("10.0.0.1", 8357), ("10.0.0.2", 8357)])
82
+ ```
83
+
84
+ ## Replication
85
+
86
+ The cluster's replication factor R rides along with the node list, so the
87
+ SDK needs no configuration: `set`/`delete` fan out to all R owners of a
88
+ key (the primary's result decides; a dead replica never fails a write),
89
+ and `get` asks the primary, falling over to the next owner only when the
90
+ holder is unreachable. `client.replication` exposes the factor in use.
91
+
92
+ ## Fire-and-forget replica writes
93
+
94
+ Off by default. `set`/`delete` normally wait for every replica leg to
95
+ finish, same as the primary. Enabling `fire_and_forget_replicas` returns
96
+ as soon as the primary acks, letting replica legs finish in the
97
+ background (doc/adr/0014-*.md):
98
+
99
+ ```python
100
+ client = await NanocachedClient.connect(
101
+ [("cache.internal", 8357)],
102
+ fire_and_forget_replicas=True,
103
+ )
104
+ ```
105
+
106
+ Unlike `compress`, this is a pure latency/durability trade for this
107
+ client's own writes — it carries no wire format, and different clients
108
+ may use different settings freely. At most 32 replica writes across the
109
+ whole client run in the background at once; past that cap, further
110
+ replica legs run synchronously exactly as with the option off (a
111
+ graceful degrade, not a queue or a drop). `close()` gives any
112
+ still-in-flight background replica writes a chance to finish before
113
+ tearing down their connections.
114
+
115
+ ## Read repair
116
+
117
+ Off by default. A clean miss (the key's first-reached owner reports it
118
+ missing) is normally accepted as-is. Enabling `read_repair` probes the
119
+ remaining owners before accepting that, and repairs the primary in the
120
+ background if one still has the value (doc/adr/0015-*.md):
121
+
122
+ ```python
123
+ client = await NanocachedClient.connect(
124
+ [("cache.internal", 8357)],
125
+ read_repair=True,
126
+ )
127
+ ```
128
+
129
+ Closes the narrow window after a primary restart where a replica still
130
+ holds a key its (fresh) primary doesn't, at the cost of extra reads only
131
+ on the misses that hit that window. The repair write carries no TTL —
132
+ the wire protocol's `G` response never returns one to preserve — and,
133
+ unlike fire-and-forget replica writes, is uncapped and not drained on
134
+ `close()`: this only fires on an already-rare clean miss, and losing one
135
+ costs nothing beyond staying in the window for one more read.
136
+
137
+ ## Reconnect and keep-alive
138
+
139
+ `nanocached-node` closes connections idle for 60 seconds; the SDK keeps
140
+ its connections warm automatically, pinging any connection that real
141
+ traffic has left idle for 30 seconds — so an idle timeout never severs a
142
+ healthy client, and a request that does find its connection dead (a node
143
+ restart, a network blip) redials and retries once transparently (all
144
+ operations are idempotent). There is nothing to configure.
145
+
146
+ ## Authentication and TLS
147
+
148
+ ```python
149
+ client = await NanocachedClient.connect(
150
+ [("cache.internal", 8357)],
151
+ auth_secret="change-me", # NANOCACHED_AUTH_SECRET on the server
152
+ tls=True, # verifies against the platform trust store
153
+ )
154
+ ```
155
+
156
+ For a self-signed or private-CA server, pass `ca` — a PEM file of trusted
157
+ root certificate(s), which replaces the default trust store:
158
+
159
+ ```python
160
+ client = await NanocachedClient.connect(
161
+ [("cache.internal", 8357)],
162
+ tls=True,
163
+ ca="cluster-ca.pem",
164
+ )
165
+ ```
166
+
167
+ `ca` is only meaningful when `tls=True`; if `tls=False` it is silently
168
+ ignored. An unreadable or unparseable CA file is a connect-time error.
169
+
170
+ ## Value compression
171
+
172
+ Off by default. When enabled, values at or above `compression_threshold`
173
+ bytes are transparently DEFLATE-compressed on `set` and decompressed on
174
+ `get`/`get_bytes` (doc/adr/0013-\*.md):
175
+
176
+ ```python
177
+ client = await NanocachedClient.connect(
178
+ [("cache.internal", 8357)],
179
+ compress=True,
180
+ compression_threshold=256, # default; bytes, below which values are stored as-is
181
+ )
182
+ ```
183
+
184
+ **Every client that reads or writes a given set of keys must agree on
185
+ `compress`.** This is a per-keyspace format decision, not a per-client
186
+ preference — enabling it prefixes every value this client writes with a
187
+ one-byte marker, so a client with `compress=False` reading one of those
188
+ values gets the marker byte back as if it were part of the value (wrong,
189
+ silently), and a client with `compress=True` reading a value written
190
+ before compression was enabled anywhere risks misreading that value's
191
+ first byte as the marker (a `DecompressionError`, or — if that byte
192
+ happens to be the "uncompressed" marker by chance — a silently wrong
193
+ read). There is no dual-mode migration path: only turn this on for a
194
+ fresh keyspace, or only after every client touching an existing one has
195
+ upgraded and enabled it together. Incompressible data (already-compressed
196
+ media, random bytes) is passed through unchanged rather than bloated.
197
+
198
+ ## Notes
199
+
200
+ - Requests are pipelined per connection (doc/adr/0016-*.md), matching
201
+ the TypeScript SDK: concurrent callers on the same connection each pay
202
+ only their own network latency, not everyone else's ahead of them.
203
+ - This SDK speaks the current wire protocol (rendezvous hashing,
204
+ replication-aware `L`/`W`); it requires an up-to-date server.
205
+ - `close()` is idempotent, but calling it again on an already-closed
206
+ client prints a warning to stderr — usually a sign the client's
207
+ lifecycle was mismanaged. Likewise, calling `connect()` again for the
208
+ same single address while a previous connection to it is still open
209
+ prints a warning ("was close() forgotten?"); this check is skipped for
210
+ multi-address configs, where concurrent clients sharing an address list
211
+ are legitimate.
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,195 @@
1
+ # nanocached (Python)
2
+
3
+ asyncio client SDK for [nanocached](https://github.com/nanocached/nanocached),
4
+ a tiny distributed cache. Talks to either a single `nanocached-node` or a
5
+ `nanocached-discovery`-fronted cluster — the SDK figures out which from the
6
+ server's own handshake, so the calling code is identical either way.
7
+
8
+ Requires Python 3.11+. No runtime dependencies.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pip install nanocached
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ import asyncio
20
+ from nanocached import NanocachedClient
21
+
22
+ async def main():
23
+ # Point at a single node, or at a discovery server fronting a
24
+ # cluster — same call either way. `addresses` always takes a list;
25
+ # a one-element list is the single-target case.
26
+ client = await NanocachedClient.connect([("127.0.0.1", 8357)])
27
+
28
+ await client.set("greeting", "hello", ttl_seconds=60)
29
+ value = await client.get("greeting") # str | None
30
+ print(value) # hello
31
+ existed = await client.delete("greeting") # bool
32
+
33
+ client.close()
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ Or use it as an async context manager, which closes the client for you:
39
+
40
+ ```python
41
+ async with await NanocachedClient.connect([("127.0.0.1", 8357)]) as client:
42
+ await client.set("greeting", "hello")
43
+ print(await client.get("greeting"))
44
+ ```
45
+
46
+ Keys may be `str` (encoded as UTF-8) or `bytes`; values may likewise be
47
+ `str` or `bytes` on the way in. `get(key)` strictly decodes the stored
48
+ value as UTF-8 and returns `str | None` — a value that isn't valid UTF-8
49
+ raises `UnicodeDecodeError` rather than silently mangling it. Use
50
+ `get_bytes(key) -> bytes | None` for the raw bytes.
51
+
52
+ ## Discovery replicas
53
+
54
+ When the cluster runs more than one discovery server, pass them all in
55
+ `addresses`; both the initial connect and every node-list refresh try them
56
+ in order. An address that is warming up after a restart (answers `B`) is
57
+ skipped like an unreachable one; if every address is warming up, `connect()`
58
+ raises `DiscoveryBusyError` — retry shortly.
59
+
60
+ ```python
61
+ client = await NanocachedClient.connect([("10.0.0.1", 8357), ("10.0.0.2", 8357)])
62
+ ```
63
+
64
+ ## Replication
65
+
66
+ The cluster's replication factor R rides along with the node list, so the
67
+ SDK needs no configuration: `set`/`delete` fan out to all R owners of a
68
+ key (the primary's result decides; a dead replica never fails a write),
69
+ and `get` asks the primary, falling over to the next owner only when the
70
+ holder is unreachable. `client.replication` exposes the factor in use.
71
+
72
+ ## Fire-and-forget replica writes
73
+
74
+ Off by default. `set`/`delete` normally wait for every replica leg to
75
+ finish, same as the primary. Enabling `fire_and_forget_replicas` returns
76
+ as soon as the primary acks, letting replica legs finish in the
77
+ background (doc/adr/0014-*.md):
78
+
79
+ ```python
80
+ client = await NanocachedClient.connect(
81
+ [("cache.internal", 8357)],
82
+ fire_and_forget_replicas=True,
83
+ )
84
+ ```
85
+
86
+ Unlike `compress`, this is a pure latency/durability trade for this
87
+ client's own writes — it carries no wire format, and different clients
88
+ may use different settings freely. At most 32 replica writes across the
89
+ whole client run in the background at once; past that cap, further
90
+ replica legs run synchronously exactly as with the option off (a
91
+ graceful degrade, not a queue or a drop). `close()` gives any
92
+ still-in-flight background replica writes a chance to finish before
93
+ tearing down their connections.
94
+
95
+ ## Read repair
96
+
97
+ Off by default. A clean miss (the key's first-reached owner reports it
98
+ missing) is normally accepted as-is. Enabling `read_repair` probes the
99
+ remaining owners before accepting that, and repairs the primary in the
100
+ background if one still has the value (doc/adr/0015-*.md):
101
+
102
+ ```python
103
+ client = await NanocachedClient.connect(
104
+ [("cache.internal", 8357)],
105
+ read_repair=True,
106
+ )
107
+ ```
108
+
109
+ Closes the narrow window after a primary restart where a replica still
110
+ holds a key its (fresh) primary doesn't, at the cost of extra reads only
111
+ on the misses that hit that window. The repair write carries no TTL —
112
+ the wire protocol's `G` response never returns one to preserve — and,
113
+ unlike fire-and-forget replica writes, is uncapped and not drained on
114
+ `close()`: this only fires on an already-rare clean miss, and losing one
115
+ costs nothing beyond staying in the window for one more read.
116
+
117
+ ## Reconnect and keep-alive
118
+
119
+ `nanocached-node` closes connections idle for 60 seconds; the SDK keeps
120
+ its connections warm automatically, pinging any connection that real
121
+ traffic has left idle for 30 seconds — so an idle timeout never severs a
122
+ healthy client, and a request that does find its connection dead (a node
123
+ restart, a network blip) redials and retries once transparently (all
124
+ operations are idempotent). There is nothing to configure.
125
+
126
+ ## Authentication and TLS
127
+
128
+ ```python
129
+ client = await NanocachedClient.connect(
130
+ [("cache.internal", 8357)],
131
+ auth_secret="change-me", # NANOCACHED_AUTH_SECRET on the server
132
+ tls=True, # verifies against the platform trust store
133
+ )
134
+ ```
135
+
136
+ For a self-signed or private-CA server, pass `ca` — a PEM file of trusted
137
+ root certificate(s), which replaces the default trust store:
138
+
139
+ ```python
140
+ client = await NanocachedClient.connect(
141
+ [("cache.internal", 8357)],
142
+ tls=True,
143
+ ca="cluster-ca.pem",
144
+ )
145
+ ```
146
+
147
+ `ca` is only meaningful when `tls=True`; if `tls=False` it is silently
148
+ ignored. An unreadable or unparseable CA file is a connect-time error.
149
+
150
+ ## Value compression
151
+
152
+ Off by default. When enabled, values at or above `compression_threshold`
153
+ bytes are transparently DEFLATE-compressed on `set` and decompressed on
154
+ `get`/`get_bytes` (doc/adr/0013-\*.md):
155
+
156
+ ```python
157
+ client = await NanocachedClient.connect(
158
+ [("cache.internal", 8357)],
159
+ compress=True,
160
+ compression_threshold=256, # default; bytes, below which values are stored as-is
161
+ )
162
+ ```
163
+
164
+ **Every client that reads or writes a given set of keys must agree on
165
+ `compress`.** This is a per-keyspace format decision, not a per-client
166
+ preference — enabling it prefixes every value this client writes with a
167
+ one-byte marker, so a client with `compress=False` reading one of those
168
+ values gets the marker byte back as if it were part of the value (wrong,
169
+ silently), and a client with `compress=True` reading a value written
170
+ before compression was enabled anywhere risks misreading that value's
171
+ first byte as the marker (a `DecompressionError`, or — if that byte
172
+ happens to be the "uncompressed" marker by chance — a silently wrong
173
+ read). There is no dual-mode migration path: only turn this on for a
174
+ fresh keyspace, or only after every client touching an existing one has
175
+ upgraded and enabled it together. Incompressible data (already-compressed
176
+ media, random bytes) is passed through unchanged rather than bloated.
177
+
178
+ ## Notes
179
+
180
+ - Requests are pipelined per connection (doc/adr/0016-*.md), matching
181
+ the TypeScript SDK: concurrent callers on the same connection each pay
182
+ only their own network latency, not everyone else's ahead of them.
183
+ - This SDK speaks the current wire protocol (rendezvous hashing,
184
+ replication-aware `L`/`W`); it requires an up-to-date server.
185
+ - `close()` is idempotent, but calling it again on an already-closed
186
+ client prints a warning to stderr — usually a sign the client's
187
+ lifecycle was mismanaged. Likewise, calling `connect()` again for the
188
+ same single address while a previous connection to it is still open
189
+ prints a warning ("was close() forgotten?"); this check is skipped for
190
+ multi-address configs, where concurrent clients sharing an address list
191
+ are legitimate.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nanocached"
7
+ version = "0.1.0"
8
+ description = "asyncio client SDK for nanocached, a tiny distributed cache with client-side replication"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Takashi Yamashina" }]
13
+ keywords = ["nanocached", "cache", "distributed-cache", "rendezvous-hashing", "client", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: AsyncIO",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://nanocached.org"
24
+ Repository = "https://github.com/nanocached/nanocached"
25
+ Issues = "https://github.com/nanocached/nanocached/issues"
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ """nanocached — asyncio client SDK for the nanocached distributed cache.
2
+
3
+ See https://github.com/nanocached/nanocached for the server and protocol.
4
+ """
5
+
6
+ from ._compression import DecompressionError
7
+ from ._errors import (
8
+ AlreadyClosedError,
9
+ DiscoveryBusyError,
10
+ NanocachedError,
11
+ WrongNodeError,
12
+ )
13
+ from ._hashring import HashRing
14
+ from ._identify import DiscoveredNode
15
+ from .client import NanocachedClient
16
+
17
+ __all__ = [
18
+ "AlreadyClosedError",
19
+ "DecompressionError",
20
+ "DiscoveredNode",
21
+ "DiscoveryBusyError",
22
+ "HashRing",
23
+ "NanocachedClient",
24
+ "NanocachedError",
25
+ "WrongNodeError",
26
+ ]
27
+
28
+ __version__ = "1.0.0"
@@ -0,0 +1,92 @@
1
+ """doc/adr/0013-*.md: transparent, opt-in value compression. Values are
2
+ prefixed with a one-byte marker once ``compress`` is enabled on a client —
3
+ ``0x00`` for stored-as-is (below threshold, or compression didn't shrink
4
+ it), ``0x01`` for raw-DEFLATE-compressed (RFC 1951, no zlib/gzip
5
+ wrapper). Never present when ``compress`` is off."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import zlib
10
+
11
+ from ._errors import NanocachedError
12
+
13
+ _MARKER_RAW = 0x00
14
+ _MARKER_DEFLATE = 0x01
15
+
16
+ # Raw DEFLATE (no zlib header/checksum): wbits=-15 selects it on both the
17
+ # compress and decompress side. See doc/adr/0013-*.md for why raw DEFLATE
18
+ # was chosen over a zlib/gzip wrapper.
19
+ _WBITS_RAW_DEFLATE = -15
20
+
21
+ # Bounds a DEFLATE value's expanded output. The wire cap
22
+ # (_MAX_VALUE_LENGTH) bounds only the compressed bytes received; without
23
+ # this, a small, highly-repetitive value written by a compromised node
24
+ # could expand to gigabytes and exhaust client memory on a plain get (a
25
+ # decompression bomb). Far above any realistic cache value.
26
+ _MAX_DECOMPRESSED_LENGTH = 64 * 1024 * 1024
27
+
28
+
29
+ class DecompressionError(NanocachedError):
30
+ """Raised by get/get_bytes when a value with ``compress`` enabled
31
+ can't be interpreted — almost always a ``compress`` mismatch between
32
+ clients sharing this key (doc/adr/0013-*.md's compatibility caveat:
33
+ every client touching a given keyspace must agree on ``compress``),
34
+ not a transient failure."""
35
+
36
+
37
+ def compress_value(value: bytes, threshold: int) -> bytes:
38
+ """Below ``threshold``, or when compressing doesn't actually shrink
39
+ the value (incompressible data), the marker byte alone is added and
40
+ the value is stored unchanged. Always returns a value with the marker
41
+ byte prefixed."""
42
+ if len(value) < threshold:
43
+ return bytes([_MARKER_RAW]) + value
44
+
45
+ # zlib.compress() always emits a zlib-wrapped stream; compressobj()
46
+ # with a negative wbits is what produces the header-less raw DEFLATE
47
+ # form this SDK's wire format uses (see the module docstring).
48
+ compressor = zlib.compressobj(6, zlib.DEFLATED, _WBITS_RAW_DEFLATE)
49
+ compressed = compressor.compress(value) + compressor.flush()
50
+
51
+ if len(compressed) < len(value):
52
+ return bytes([_MARKER_DEFLATE]) + compressed
53
+ return bytes([_MARKER_RAW]) + value
54
+
55
+
56
+ def decompress_value(value: bytes) -> bytes:
57
+ """The get/get_bytes counterpart to compress_value(). Only ever called
58
+ when ``compress`` is enabled on this client — see doc/adr/0013-*.md."""
59
+ if len(value) == 0:
60
+ raise DecompressionError(
61
+ "nanocached: compress is enabled but this value has no marker byte "
62
+ "(it's empty) — was it written by a client with compress disabled?"
63
+ )
64
+
65
+ marker, body = value[0], value[1:]
66
+
67
+ if marker == _MARKER_RAW:
68
+ return body
69
+
70
+ if marker == _MARKER_DEFLATE:
71
+ try:
72
+ decompressor = zlib.decompressobj(_WBITS_RAW_DEFLATE)
73
+ # max_length caps output; a non-empty unconsumed_tail (or an
74
+ # over-cap result) means more would follow — a bomb — so we stop
75
+ # rather than let flush() expand the rest into memory.
76
+ out = decompressor.decompress(body, _MAX_DECOMPRESSED_LENGTH + 1)
77
+ if decompressor.unconsumed_tail or len(out) > _MAX_DECOMPRESSED_LENGTH:
78
+ raise DecompressionError(
79
+ "nanocached: decompressed value exceeds the maximum size — "
80
+ "possible decompression bomb"
81
+ )
82
+ return out + decompressor.flush()
83
+ except zlib.error as error:
84
+ raise DecompressionError(
85
+ "nanocached: failed to decompress a value marked as compressed — "
86
+ f"did every client sharing this key enable compress (doc/adr/0013-*.md)? ({error})"
87
+ ) from error
88
+
89
+ raise DecompressionError(
90
+ f"nanocached: unrecognized compression marker byte {marker} — was this value "
91
+ "written by a client with compress disabled (doc/adr/0013-*.md)?"
92
+ )