cachekit 0.7.0__tar.gz → 0.8.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 (92) hide show
  1. {cachekit-0.7.0 → cachekit-0.8.0}/Cargo.lock +127 -9
  2. {cachekit-0.7.0 → cachekit-0.8.0}/PKG-INFO +1 -1
  3. {cachekit-0.7.0 → cachekit-0.8.0}/pyproject.toml +1 -1
  4. {cachekit-0.7.0 → cachekit-0.8.0}/rust/Cargo.toml +2 -2
  5. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/__init__.py +1 -1
  6. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/base.py +19 -3
  7. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/backend.py +10 -3
  8. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/provider.py +63 -14
  9. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/provider.py +9 -2
  10. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/wrapper.py +8 -7
  11. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/adaptive_timeout.py +10 -8
  12. {cachekit-0.7.0 → cachekit-0.8.0}/Cargo.toml +0 -0
  13. {cachekit-0.7.0 → cachekit-0.8.0}/LICENSE +0 -0
  14. {cachekit-0.7.0 → cachekit-0.8.0}/README.md +0 -0
  15. {cachekit-0.7.0 → cachekit-0.8.0}/rust/Makefile +0 -0
  16. {cachekit-0.7.0 → cachekit-0.8.0}/rust/README.md +0 -0
  17. {cachekit-0.7.0 → cachekit-0.8.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  18. {cachekit-0.7.0 → cachekit-0.8.0}/rust/src/lib.rs +0 -0
  19. {cachekit-0.7.0 → cachekit-0.8.0}/rust/src/python_bindings.rs +0 -0
  20. {cachekit-0.7.0 → cachekit-0.8.0}/rust/supply-chain/audits.toml +0 -0
  21. {cachekit-0.7.0 → cachekit-0.8.0}/rust/supply-chain/config.toml +0 -0
  22. {cachekit-0.7.0 → cachekit-0.8.0}/rust/supply-chain/imports.lock +0 -0
  23. {cachekit-0.7.0 → cachekit-0.8.0}/rust/tsan_suppressions.txt +0 -0
  24. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/__init__.py +0 -0
  25. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/base_config.py +0 -0
  26. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  27. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/client.py +0 -0
  28. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/config.py +0 -0
  29. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  30. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  31. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/errors.py +0 -0
  32. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/file/__init__.py +0 -0
  33. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/file/backend.py +0 -0
  34. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/file/config.py +0 -0
  35. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  36. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/memcached/backend.py +0 -0
  37. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/memcached/config.py +0 -0
  38. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  39. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/__init__.py +0 -0
  40. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/backend.py +0 -0
  41. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/client.py +0 -0
  42. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/config.py +0 -0
  43. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/backends/redis/error_handler.py +0 -0
  44. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/cache_handler.py +0 -0
  45. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/__init__.py +0 -0
  46. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/decorator.py +0 -0
  47. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/nested.py +0 -0
  48. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/settings.py +0 -0
  49. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/singleton.py +0 -0
  50. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/config/validation.py +0 -0
  51. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/__init__.py +0 -0
  52. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/intent.py +0 -0
  53. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  54. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/main.py +0 -0
  55. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/orchestrator.py +0 -0
  56. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/session.py +0 -0
  57. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/stats_context.py +0 -0
  58. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/tenant_context.py +0 -0
  59. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  60. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/di.py +0 -0
  61. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/hash_utils.py +0 -0
  62. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/health.py +0 -0
  63. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/hiredis_compat.py +0 -0
  64. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/imports.py +0 -0
  65. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/invalidation/__init__.py +0 -0
  66. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/invalidation/channel.py +0 -0
  67. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/invalidation/event.py +0 -0
  68. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/invalidation/redis_channel.py +0 -0
  69. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/key_generator.py +0 -0
  70. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/l1_cache.py +0 -0
  71. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/logging.py +0 -0
  72. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/monitoring/__init__.py +0 -0
  73. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
  74. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  75. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/monitoring/protocols.py +0 -0
  76. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/object_cache.py +0 -0
  77. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/py.typed +0 -0
  78. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/__init__.py +0 -0
  79. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/async_metrics.py +0 -0
  80. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/circuit_breaker.py +0 -0
  81. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/error_classification.py +0 -0
  82. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/load_control.py +0 -0
  83. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/metrics_collection.py +0 -0
  84. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/reliability/profiles.py +0 -0
  85. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/__init__.py +0 -0
  86. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
  87. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/auto_serializer.py +0 -0
  88. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/base.py +0 -0
  89. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
  90. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
  91. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/standard_serializer.py +0 -0
  92. {cachekit-0.7.0 → cachekit-0.8.0}/src/cachekit/serializers/wrapper.py +0 -0
@@ -17,6 +17,43 @@ version = "2.0.1"
17
17
  source = "registry+https://github.com/rust-lang/crates.io-index"
18
18
  checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
19
19
 
20
+ [[package]]
21
+ name = "aead"
22
+ version = "0.5.2"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
25
+ dependencies = [
26
+ "crypto-common",
27
+ "generic-array",
28
+ ]
29
+
30
+ [[package]]
31
+ name = "aes"
32
+ version = "0.8.4"
33
+ source = "registry+https://github.com/rust-lang/crates.io-index"
34
+ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
35
+ dependencies = [
36
+ "cfg-if",
37
+ "cipher",
38
+ "cpufeatures",
39
+ "zeroize",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "aes-gcm"
44
+ version = "0.10.3"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
47
+ dependencies = [
48
+ "aead",
49
+ "aes",
50
+ "cipher",
51
+ "ctr",
52
+ "ghash",
53
+ "subtle",
54
+ "zeroize",
55
+ ]
56
+
20
57
  [[package]]
21
58
  name = "ahash"
22
59
  version = "0.8.12"
@@ -208,14 +245,17 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
208
245
 
209
246
  [[package]]
210
247
  name = "cachekit-core"
211
- version = "0.1.1"
248
+ version = "0.2.0"
212
249
  source = "registry+https://github.com/rust-lang/crates.io-index"
213
- checksum = "3e264be91367e582a103017104f2fcc6f6f12ca59ded81f53016bc60752a94ae"
250
+ checksum = "9622b3c3e47fcf7abfde287c557e6171674e37842a2dd26fda03f63720542f17"
214
251
  dependencies = [
252
+ "aes",
253
+ "aes-gcm",
215
254
  "byteorder",
216
255
  "bytes",
217
256
  "cbindgen",
218
257
  "generic-array",
258
+ "getrandom 0.2.17",
219
259
  "hkdf",
220
260
  "hmac",
221
261
  "lz4_flex",
@@ -224,14 +264,14 @@ dependencies = [
224
264
  "serde",
225
265
  "serde_bytes",
226
266
  "sha2",
227
- "thiserror 1.0.69",
267
+ "thiserror 2.0.18",
228
268
  "xxhash-rust",
229
269
  "zeroize",
230
270
  ]
231
271
 
232
272
  [[package]]
233
273
  name = "cachekit-rs"
234
- version = "0.7.0"
274
+ version = "0.8.0"
235
275
  dependencies = [
236
276
  "cachekit-core",
237
277
  "criterion",
@@ -312,6 +352,16 @@ dependencies = [
312
352
  "half",
313
353
  ]
314
354
 
355
+ [[package]]
356
+ name = "cipher"
357
+ version = "0.4.4"
358
+ source = "registry+https://github.com/rust-lang/crates.io-index"
359
+ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
360
+ dependencies = [
361
+ "crypto-common",
362
+ "inout",
363
+ ]
364
+
315
365
  [[package]]
316
366
  name = "clap"
317
367
  version = "4.6.0"
@@ -444,6 +494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
444
494
  checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
445
495
  dependencies = [
446
496
  "generic-array",
497
+ "rand_core 0.6.4",
447
498
  "typenum",
448
499
  ]
449
500
 
@@ -463,6 +514,15 @@ version = "0.0.6"
463
514
  source = "registry+https://github.com/rust-lang/crates.io-index"
464
515
  checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
465
516
 
517
+ [[package]]
518
+ name = "ctr"
519
+ version = "0.9.2"
520
+ source = "registry+https://github.com/rust-lang/crates.io-index"
521
+ checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
522
+ dependencies = [
523
+ "cipher",
524
+ ]
525
+
466
526
  [[package]]
467
527
  name = "debugid"
468
528
  version = "0.8.0"
@@ -639,8 +699,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
639
699
  checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
640
700
  dependencies = [
641
701
  "cfg-if",
702
+ "js-sys",
642
703
  "libc",
643
704
  "wasi",
705
+ "wasm-bindgen",
644
706
  ]
645
707
 
646
708
  [[package]]
@@ -668,6 +730,16 @@ dependencies = [
668
730
  "wasip3",
669
731
  ]
670
732
 
733
+ [[package]]
734
+ name = "ghash"
735
+ version = "0.5.1"
736
+ source = "registry+https://github.com/rust-lang/crates.io-index"
737
+ checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
738
+ dependencies = [
739
+ "opaque-debug",
740
+ "polyval",
741
+ ]
742
+
671
743
  [[package]]
672
744
  name = "gimli"
673
745
  version = "0.32.3"
@@ -820,6 +892,15 @@ dependencies = [
820
892
  "str_stack",
821
893
  ]
822
894
 
895
+ [[package]]
896
+ name = "inout"
897
+ version = "0.1.4"
898
+ source = "registry+https://github.com/rust-lang/crates.io-index"
899
+ checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
900
+ dependencies = [
901
+ "generic-array",
902
+ ]
903
+
823
904
  [[package]]
824
905
  name = "is-terminal"
825
906
  version = "0.4.17"
@@ -909,9 +990,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
909
990
 
910
991
  [[package]]
911
992
  name = "lz4_flex"
912
- version = "0.11.6"
993
+ version = "0.12.2"
913
994
  source = "registry+https://github.com/rust-lang/crates.io-index"
914
- checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
995
+ checksum = "90071f8077f8e40adfc4b7fe9cd495ce316263f19e75c2211eeff3fdf475a3d9"
915
996
  dependencies = [
916
997
  "twox-hash",
917
998
  ]
@@ -1006,6 +1087,12 @@ version = "11.1.5"
1006
1087
  source = "registry+https://github.com/rust-lang/crates.io-index"
1007
1088
  checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
1008
1089
 
1090
+ [[package]]
1091
+ name = "opaque-debug"
1092
+ version = "0.3.1"
1093
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1094
+ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
1095
+
1009
1096
  [[package]]
1010
1097
  name = "plotters"
1011
1098
  version = "0.3.7"
@@ -1034,6 +1121,18 @@ dependencies = [
1034
1121
  "plotters-backend",
1035
1122
  ]
1036
1123
 
1124
+ [[package]]
1125
+ name = "polyval"
1126
+ version = "0.6.2"
1127
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1128
+ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
1129
+ dependencies = [
1130
+ "cfg-if",
1131
+ "cpufeatures",
1132
+ "opaque-debug",
1133
+ "universal-hash",
1134
+ ]
1135
+
1037
1136
  [[package]]
1038
1137
  name = "portable-atomic"
1039
1138
  version = "1.13.1"
@@ -1290,7 +1389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1290
1389
  checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
1291
1390
  dependencies = [
1292
1391
  "rand_chacha",
1293
- "rand_core",
1392
+ "rand_core 0.9.5",
1294
1393
  ]
1295
1394
 
1296
1395
  [[package]]
@@ -1300,7 +1399,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1300
1399
  checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1301
1400
  dependencies = [
1302
1401
  "ppv-lite86",
1303
- "rand_core",
1402
+ "rand_core 0.9.5",
1403
+ ]
1404
+
1405
+ [[package]]
1406
+ name = "rand_core"
1407
+ version = "0.6.4"
1408
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1409
+ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1410
+ dependencies = [
1411
+ "getrandom 0.2.17",
1304
1412
  ]
1305
1413
 
1306
1414
  [[package]]
@@ -1318,7 +1426,7 @@ version = "0.4.0"
1318
1426
  source = "registry+https://github.com/rust-lang/crates.io-index"
1319
1427
  checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
1320
1428
  dependencies = [
1321
- "rand_core",
1429
+ "rand_core 0.9.5",
1322
1430
  ]
1323
1431
 
1324
1432
  [[package]]
@@ -1804,6 +1912,16 @@ version = "0.2.4"
1804
1912
  source = "registry+https://github.com/rust-lang/crates.io-index"
1805
1913
  checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
1806
1914
 
1915
+ [[package]]
1916
+ name = "universal-hash"
1917
+ version = "0.5.1"
1918
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1919
+ checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
1920
+ dependencies = [
1921
+ "crypto-common",
1922
+ "subtle",
1923
+ ]
1924
+
1807
1925
  [[package]]
1808
1926
  name = "untrusted"
1809
1927
  version = "0.9.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Production-ready Redis caching for Python with intelligent reliability features and Rust-powered performance"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -20,7 +20,7 @@ crate-type = ["cdylib", "rlib"]
20
20
 
21
21
  [dependencies]
22
22
  # Compression, checksums, encryption (https://crates.io/crates/cachekit-core)
23
- cachekit-core = { version = "=0.1.1", features = ["compression", "checksum", "messagepack", "encryption"] }
23
+ cachekit-core = { version = "0.2.0", features = ["compression", "checksum", "messagepack", "encryption"] }
24
24
 
25
25
  # Python integration - optional for Rust-only builds
26
26
  pyo3 = { workspace = true, optional = true }
@@ -68,7 +68,7 @@ Example Usage:
68
68
  ```
69
69
  """
70
70
 
71
- __version__ = "0.7.0"
71
+ __version__ = "0.8.0"
72
72
 
73
73
  from typing import Any, Callable, TypeVar
74
74
 
@@ -188,10 +188,23 @@ class LockableBackend(Protocol):
188
188
  - Local-only: SQLite, FileSystem (single-process locking)
189
189
  - Not supported: HTTP (stateless), Memcached, S3
190
190
 
191
+ Contract — bare cache key:
192
+ ``acquire_lock`` receives the **bare cache key**, identical to what
193
+ ``get``/``set``/``delete`` receive. Callers MUST NOT append suffixes
194
+ like ``:lock`` before passing the key. Each backend owns its own
195
+ lock-namespace derivation:
196
+
197
+ - Redis: derives ``<key>:lock`` internally for the on-wire lock name.
198
+ - CachekitIO (SaaS): the lock endpoint is ``POST /v1/cache/{key}/lock`` —
199
+ no key derivation needed.
200
+
201
+ This convergence keeps the Python SDK byte-for-byte compatible with
202
+ the cachekit-rs and cachekit-ts SDKs at the protocol boundary.
203
+
191
204
  Example:
192
205
  >>> # Distributed locking pattern (async context):
193
206
  >>> # if hasattr(backend, 'acquire_lock'):
194
- >>> # async with backend.acquire_lock("lock:compute", timeout=30) as acquired:
207
+ >>> # async with backend.acquire_lock("user:123:profile", timeout=30) as acquired:
195
208
  >>> # if acquired:
196
209
  >>> # result = expensive_computation()
197
210
  """
@@ -205,7 +218,10 @@ class LockableBackend(Protocol):
205
218
  """Acquire a distributed lock on key.
206
219
 
207
220
  Args:
208
- key: Lock key (e.g., "lock:user:123")
221
+ key: Bare cache key (e.g., ``"user:123"``) — the SAME shape passed
222
+ to ``get``/``set``/``delete``. Implementations are responsible
223
+ for any internal lock-namespace derivation; callers MUST NOT
224
+ pre-suffix the key.
209
225
  timeout: How long to hold the lock (seconds) before auto-release
210
226
  blocking_timeout: Max time to wait for lock acquisition (None = non-blocking)
211
227
 
@@ -217,7 +233,7 @@ class LockableBackend(Protocol):
217
233
  BackendError: If backend operation fails
218
234
 
219
235
  Example:
220
- >>> async with backend.acquire_lock("lock:key", timeout=30, blocking_timeout=5) as acquired: # doctest: +SKIP
236
+ >>> async with backend.acquire_lock("user:123:profile", timeout=30, blocking_timeout=5) as acquired: # doctest: +SKIP
221
237
  ... if acquired: # doctest: +SKIP
222
238
  ... # Lock held, safe to proceed
223
239
  ... pass # doctest: +SKIP
@@ -553,14 +553,21 @@ class CachekitIOBackend:
553
553
  async def _try_acquire_lock(self, lock_key: str, timeout: float) -> str | None:
554
554
  """Single attempt at the SaaS lock endpoint. Returns lock_id, or None if held.
555
555
 
556
+ ``lock_key`` is the bare cache key (LockableBackend contract). The SaaS lock
557
+ endpoint is ``POST /v1/cache/{key}/lock`` — no internal derivation is needed
558
+ because lock semantics live in the URL path, not the key namespace. Appending
559
+ ``:lock`` here would push the SaaS validator past its 7-segment canonical key
560
+ budget and trigger a 400 at the edge.
561
+
556
562
  Non-finite (NaN / ±inf) or non-positive ``timeout`` is clamped to 1ms — without
557
563
  the guard, ``int(NaN)`` / ``int(inf)`` would raise outside ``BackendError`` and
558
564
  escape the wrapper's degrade-to-no-lock branch.
559
565
 
560
566
  Raises:
561
- BackendError: For AUTHENTICATION and PERMANENT failures (bad key, bad lock_key
562
- format) — polling won't recover and the wrapper degrades to no-lock execution.
563
- TRANSIENT/TIMEOUT/UNKNOWN are swallowed as None so the polling loop can retry.
567
+ BackendError: For AUTHENTICATION and PERMANENT failures (bad key, bad key
568
+ format rejected by SaaS validator) — polling won't recover and the wrapper
569
+ degrades to no-lock execution. TRANSIENT/TIMEOUT/UNKNOWN are swallowed as
570
+ None so the polling loop can retry.
564
571
  """
565
572
  # Clamp non-positive / non-finite timeouts before the int conversion.
566
573
  # int(NaN) / int(inf) raise ValueError/OverflowError that aren't BackendError, so
@@ -104,36 +104,85 @@ class DefaultCacheClientProvider(CacheClientProvider):
104
104
  class DefaultBackendProvider(BackendProviderInterface):
105
105
  """Default backend provider with env-based auto-detection.
106
106
 
107
- Priority order (first matching env var wins):
108
- 1. CACHEKIT_API_KEY → CachekitIOBackend (SaaS)
109
- 2. CACHEKIT_REDIS_URL or REDIS_URL → RedisBackend
110
-
111
- For single-tenant deployments (default), sets tenant_context to "default".
112
- For multi-tenant deployments, tenant_context must be set externally.
107
+ Selection is by a single, unambiguous environment signal. Priority order:
108
+ 1. CACHEKIT_API_KEY → CachekitIOBackend (SaaS)
109
+ 2. CACHEKIT_REDIS_URL → RedisBackend
110
+ 3. CACHEKIT_MEMCACHED_SERVERS → MemcachedBackend
111
+ 4. CACHEKIT_FILE_CACHE_DIR → FileBackend
112
+ 5. REDIS_URL, or nothing set RedisBackend (12-factor / localhost default)
113
+
114
+ Setting more than one of the four prefixed selectors (1-4) raises
115
+ ``ConfigurationError`` — auto-detection must be unambiguous; pass
116
+ ``backend=`` explicitly to override. The non-prefixed ``REDIS_URL`` is only a
117
+ fallback and never counts as a conflict (12-factor convention).
118
+
119
+ CachekitIO/Memcached/File backends are stateless singletons (cached). Redis
120
+ backends are per-request tenant-scoped wrappers (not cached —
121
+ RedisBackendProvider.get_backend() reads the tenant_context ContextVar). For
122
+ single-tenant deployments (default), tenant_context is set to "default".
113
123
  """
114
124
 
125
+ # Prefixed selectors in priority order. REDIS_URL is the implicit fallback
126
+ # and intentionally excluded so it never triggers a conflict.
127
+ _SELECTORS = (
128
+ ("CACHEKIT_API_KEY", "cachekitio"),
129
+ ("CACHEKIT_REDIS_URL", "redis"),
130
+ ("CACHEKIT_MEMCACHED_SERVERS", "memcached"),
131
+ ("CACHEKIT_FILE_CACHE_DIR", "file"),
132
+ )
133
+
115
134
  def __init__(self):
116
135
  self._cachekitio_backend = None
117
136
  self._redis_provider = None
137
+ self._memcached_backend = None
138
+ self._file_backend = None
118
139
 
119
- def get_backend(self):
120
- """Get backend instance, auto-detected from environment on first call.
140
+ def _detect(self):
141
+ """Return the chosen backend key, or None to use the Redis fallback.
121
142
 
122
- CachekitIO backends are stateless singletons (cached).
123
- Redis backends are per-request tenant-scoped wrappers (not cached —
124
- RedisBackendProvider.get_backend() reads tenant_context ContextVar).
143
+ Raises ConfigurationError if more than one prefixed selector is set.
125
144
  """
126
145
  import os
127
146
 
128
- # Priority 1: CachekitIO SaaS backend (stateless, safe to cache)
129
- if os.environ.get("CACHEKIT_API_KEY"):
147
+ matched = [(env_var, key) for env_var, key in self._SELECTORS if os.environ.get(env_var)]
148
+ if len(matched) > 1:
149
+ from cachekit.config.validation import ConfigurationError
150
+
151
+ names = ", ".join(env_var for env_var, _ in matched)
152
+ raise ConfigurationError(
153
+ f"Ambiguous backend auto-detection: multiple selectors set ({names}). "
154
+ "Set exactly one of CACHEKIT_API_KEY / CACHEKIT_REDIS_URL / "
155
+ "CACHEKIT_MEMCACHED_SERVERS / CACHEKIT_FILE_CACHE_DIR, or pass backend= explicitly."
156
+ )
157
+ return matched[0][1] if matched else None
158
+
159
+ def get_backend(self):
160
+ """Get backend instance, auto-detected from environment on first call."""
161
+ choice = self._detect()
162
+
163
+ if choice == "cachekitio":
130
164
  if self._cachekitio_backend is None:
131
165
  from cachekit.backends.cachekitio import CachekitIOBackend
132
166
 
133
167
  self._cachekitio_backend = CachekitIOBackend()
134
168
  return self._cachekitio_backend
135
169
 
136
- # Priority 2: Redis backend (tenant-scoped, call provider each time)
170
+ if choice == "memcached":
171
+ if self._memcached_backend is None:
172
+ from cachekit.backends.memcached import MemcachedBackend, MemcachedBackendConfig
173
+
174
+ self._memcached_backend = MemcachedBackend(MemcachedBackendConfig.from_env())
175
+ return self._memcached_backend
176
+
177
+ if choice == "file":
178
+ if self._file_backend is None:
179
+ from cachekit.backends.file import FileBackend, FileBackendConfig
180
+
181
+ self._file_backend = FileBackend(FileBackendConfig.from_env())
182
+ return self._file_backend
183
+
184
+ # choice == "redis" (explicit CACHEKIT_REDIS_URL) or None (REDIS_URL / localhost fallback).
185
+ # Tenant-scoped: call the provider each time so it re-reads tenant_context.
137
186
  if self._redis_provider is None:
138
187
  from cachekit.backends.redis.config import RedisBackendConfig
139
188
  from cachekit.backends.redis.provider import RedisBackendProvider, tenant_context
@@ -327,7 +327,10 @@ class PerRequestRedisBackend:
327
327
  """Acquire distributed lock (LockableBackend protocol).
328
328
 
329
329
  Args:
330
- key: Lock key (will be tenant-scoped)
330
+ key: Bare cache key (will be tenant-scoped and ``:lock``-suffixed internally).
331
+ The LockableBackend protocol passes the same key as ``get``/``set``/``delete``;
332
+ the ``:lock`` namespace is a Redis-backend implementation detail kept
333
+ on-wire for zero-migration compatibility with existing deployments.
331
334
  timeout: How long to hold lock (seconds) before auto-release
332
335
  blocking_timeout: Max time to wait for lock (None = non-blocking)
333
336
 
@@ -343,7 +346,11 @@ class PerRequestRedisBackend:
343
346
  """
344
347
  import asyncio
345
348
 
346
- scoped_key = self._scoped_key(key)
349
+ # Derive the on-wire Redis lock name from the bare cache key: ``<scoped_key>:lock``.
350
+ # Keeping this suffix on the wire preserves compatibility with existing Redis
351
+ # deployments — the lock identity didn't change, only the protocol boundary
352
+ # (the wrapper no longer pollutes the cache_key passed in).
353
+ scoped_key = f"{self._scoped_key(key)}:lock"
347
354
  lock = None
348
355
  try:
349
356
  from redis.lock import Lock
@@ -1060,8 +1060,11 @@ def create_cache_wrapper(
1060
1060
  )
1061
1061
 
1062
1062
  # CACHE MISS - Use distributed lock to prevent thundering herd
1063
- # This ensures only one request executes the function while others wait
1064
- lock_key = f"{cache_key}:lock"
1063
+ # This ensures only one request executes the function while others wait.
1064
+ # LockableBackend protocol contract: pass the bare cache_key. Each backend
1065
+ # owns its internal lock-namespace derivation (Redis: ``<key>:lock``; SaaS:
1066
+ # ``POST /v1/cache/{key}/lock``). Appending here would pollute the SaaS
1067
+ # 7-segment canonical key and 400 at the edge.
1065
1068
  lock_timeout = 30.0 # Lock expires after 30 seconds to prevent deadlock
1066
1069
  blocking_timeout = 5.0 # Wait up to 5 seconds to acquire lock
1067
1070
 
@@ -1070,7 +1073,7 @@ def create_cache_wrapper(
1070
1073
  try:
1071
1074
  # Use backend's async lock protocol
1072
1075
  async with _backend.acquire_lock(
1073
- lock_key,
1076
+ cache_key,
1074
1077
  timeout=lock_timeout,
1075
1078
  blocking_timeout=blocking_timeout,
1076
1079
  ) as lock_acquired:
@@ -1102,9 +1105,7 @@ def create_cache_wrapper(
1102
1105
  else:
1103
1106
  # Lock timeout - double-check cache before giving up
1104
1107
  # Another request may have populated it while we waited
1105
- logger().warning(
1106
- f"Failed to acquire lock for {cache_key} (lock_key={lock_key}) after {blocking_timeout}s, checking cache"
1107
- )
1108
+ logger().warning(f"Failed to acquire lock for {cache_key} after {blocking_timeout}s, checking cache")
1108
1109
  try:
1109
1110
  cached_data = await operation_handler.cache_handler.get_async(cache_key) # type: ignore[attr-defined]
1110
1111
  if cached_data is not None:
@@ -1202,7 +1203,7 @@ def create_cache_wrapper(
1202
1203
  raise e.original_exception from e
1203
1204
 
1204
1205
  # Lock operation failed - execute without lock
1205
- logger().warning(f"Lock operation failed for {cache_key} (lock_key={lock_key}), executing without lock: {e}")
1206
+ logger().warning(f"Lock operation failed for {cache_key}, executing without lock: {e}")
1206
1207
  # Fall through to execute without locking
1207
1208
 
1208
1209
  # Execute without locking (either backend doesn't support it or lock failed)
@@ -161,11 +161,12 @@ class AdaptiveTimeoutManager:
161
161
  # Get adaptive timeouts for lock operations
162
162
  lock_timeout, blocking_timeout = timeout_manager.get_lock_timeouts()
163
163
 
164
- redis_lock = redis_client.lock(
165
- lock_key,
164
+ async with backend.acquire_lock(
165
+ cache_key,
166
166
  timeout=lock_timeout,
167
- blocking_timeout=blocking_timeout
168
- )
167
+ blocking_timeout=blocking_timeout,
168
+ ) as acquired:
169
+ ...
169
170
 
170
171
  Examples:
171
172
  Create manager with defaults:
@@ -360,11 +361,12 @@ class AdaptiveTimeoutManager:
360
361
 
361
362
  Example:
362
363
  lock_timeout, blocking_timeout = manager.get_lock_timeouts()
363
- redis_lock = redis_client.lock(
364
- lock_key,
364
+ async with backend.acquire_lock(
365
+ cache_key,
365
366
  timeout=lock_timeout,
366
- blocking_timeout=blocking_timeout
367
- )
367
+ blocking_timeout=blocking_timeout,
368
+ ) as acquired:
369
+ ...
368
370
  """
369
371
  with self._lock:
370
372
  return (self._current_lock_timeout, self._current_blocking_timeout)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes