cachekit 0.10.0__tar.gz → 0.10.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.
Files changed (92) hide show
  1. {cachekit-0.10.0 → cachekit-0.10.1}/Cargo.lock +1 -1
  2. {cachekit-0.10.0 → cachekit-0.10.1}/PKG-INFO +1 -1
  3. {cachekit-0.10.0 → cachekit-0.10.1}/pyproject.toml +1 -1
  4. {cachekit-0.10.0 → cachekit-0.10.1}/rust/Cargo.toml +1 -1
  5. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/__init__.py +1 -1
  6. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/cache_handler.py +6 -1
  7. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/auto_serializer.py +64 -31
  8. {cachekit-0.10.0 → cachekit-0.10.1}/Cargo.toml +0 -0
  9. {cachekit-0.10.0 → cachekit-0.10.1}/LICENSE +0 -0
  10. {cachekit-0.10.0 → cachekit-0.10.1}/README.md +0 -0
  11. {cachekit-0.10.0 → cachekit-0.10.1}/rust/Makefile +0 -0
  12. {cachekit-0.10.0 → cachekit-0.10.1}/rust/README.md +0 -0
  13. {cachekit-0.10.0 → cachekit-0.10.1}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  14. {cachekit-0.10.0 → cachekit-0.10.1}/rust/src/lib.rs +0 -0
  15. {cachekit-0.10.0 → cachekit-0.10.1}/rust/src/python_bindings.rs +0 -0
  16. {cachekit-0.10.0 → cachekit-0.10.1}/rust/supply-chain/audits.toml +0 -0
  17. {cachekit-0.10.0 → cachekit-0.10.1}/rust/supply-chain/config.toml +0 -0
  18. {cachekit-0.10.0 → cachekit-0.10.1}/rust/supply-chain/imports.lock +0 -0
  19. {cachekit-0.10.0 → cachekit-0.10.1}/rust/tsan_suppressions.txt +0 -0
  20. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/__init__.py +0 -0
  21. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/base.py +0 -0
  22. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/base_config.py +0 -0
  23. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  24. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/backend.py +0 -0
  25. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/client.py +0 -0
  26. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/config.py +0 -0
  27. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  28. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/cachekitio/session.py +0 -0
  29. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/errors.py +0 -0
  30. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/file/__init__.py +0 -0
  31. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/file/backend.py +0 -0
  32. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/file/config.py +0 -0
  33. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/memcached/__init__.py +0 -0
  34. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/memcached/backend.py +0 -0
  35. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/memcached/config.py +0 -0
  36. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/memcached/error_handler.py +0 -0
  37. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/provider.py +0 -0
  38. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/__init__.py +0 -0
  39. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/backend.py +0 -0
  40. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/client.py +0 -0
  41. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/config.py +0 -0
  42. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/error_handler.py +0 -0
  43. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/backends/redis/provider.py +0 -0
  44. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/__init__.py +0 -0
  45. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/decorator.py +0 -0
  46. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/nested.py +0 -0
  47. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/settings.py +0 -0
  48. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/singleton.py +0 -0
  49. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/config/validation.py +0 -0
  50. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/__init__.py +0 -0
  51. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/intent.py +0 -0
  52. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/local_wrapper.py +0 -0
  53. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/main.py +0 -0
  54. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/orchestrator.py +0 -0
  55. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/session.py +0 -0
  56. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/stats_context.py +0 -0
  57. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/tenant_context.py +0 -0
  58. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/utils/__init__.py +0 -0
  59. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/decorators/wrapper.py +0 -0
  60. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/di.py +0 -0
  61. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/hash_utils.py +0 -0
  62. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/health.py +0 -0
  63. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/hiredis_compat.py +0 -0
  64. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/imports.py +0 -0
  65. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/invalidation/__init__.py +0 -0
  66. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/invalidation/channel.py +0 -0
  67. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/invalidation/event.py +0 -0
  68. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/invalidation/redis_channel.py +0 -0
  69. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/key_generator.py +0 -0
  70. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/l1_cache.py +0 -0
  71. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/logging.py +0 -0
  72. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/monitoring/__init__.py +0 -0
  73. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/monitoring/correlation_tracking.py +0 -0
  74. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/monitoring/pool_monitor.py +0 -0
  75. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/monitoring/protocols.py +0 -0
  76. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/object_cache.py +0 -0
  77. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/py.typed +0 -0
  78. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/__init__.py +0 -0
  79. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/adaptive_timeout.py +0 -0
  80. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/async_metrics.py +0 -0
  81. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/circuit_breaker.py +0 -0
  82. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/error_classification.py +0 -0
  83. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/load_control.py +0 -0
  84. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/metrics_collection.py +0 -0
  85. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/reliability/profiles.py +0 -0
  86. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/__init__.py +0 -0
  87. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/arrow_serializer.py +0 -0
  88. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/base.py +0 -0
  89. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/encryption_wrapper.py +0 -0
  90. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/orjson_serializer.py +0 -0
  91. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/standard_serializer.py +0 -0
  92. {cachekit-0.10.0 → cachekit-0.10.1}/src/cachekit/serializers/wrapper.py +0 -0
@@ -271,7 +271,7 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "cachekit-rs"
274
- version = "0.10.0"
274
+ version = "0.10.1"
275
275
  dependencies = [
276
276
  "cachekit-core",
277
277
  "criterion",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.10.0
3
+ Version: 0.10.1
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.10.0"
7
+ version = "0.10.1"
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.10.0"
3
+ version = "0.10.1"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -68,7 +68,7 @@ Example Usage:
68
68
  ```
69
69
  """
70
70
 
71
- __version__ = "0.10.0"
71
+ __version__ = "0.10.1"
72
72
 
73
73
  from collections.abc import Callable
74
74
  from typing import Any, TypeVar
@@ -32,6 +32,11 @@ if TYPE_CHECKING:
32
32
  # Python-specific type tags that no other-language SDK can decode.
33
33
  CROSS_SDK_SERIALIZER_NAMES = ("default", "std", "standard", "orjson", "arrow")
34
34
 
35
+ # Serializer-name aliases collapsed to one canonical frame tag so interchangeable names stay
36
+ # cache-compatible: an entry written as 'auto' must read back under 'pythonic' (its documented
37
+ # alias) and vice-versa, instead of a serializer-mismatch that recomputes on every read (#167).
38
+ _SERIALIZER_NAME_ALIASES = {"std": "default", "standard": "default", "pythonic": "auto"}
39
+
35
40
  # Global DI container instance with default registrations
36
41
  container = DIContainer()
37
42
  container.register(LoggerProvider, DefaultLoggerProvider)
@@ -348,7 +353,7 @@ class CacheSerializationHandler:
348
353
  # Extract string name for metadata storage (for protocol instances, use class name)
349
354
  if isinstance(serializer_name, str):
350
355
  # Canonicalize aliases to prevent envelope mismatch on deserialize
351
- self._serializer_string_name = "default" if serializer_name in ("std", "standard") else serializer_name
356
+ self._serializer_string_name = _SERIALIZER_NAME_ALIASES.get(serializer_name, serializer_name)
352
357
  else:
353
358
  # Protocol instance - use class name for metadata
354
359
  self._serializer_string_name = type(serializer_name).__name__
@@ -26,6 +26,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional
26
26
  from uuid import UUID
27
27
 
28
28
  import msgpack
29
+ import xxhash
29
30
 
30
31
  if TYPE_CHECKING:
31
32
  import numpy as np
@@ -295,7 +296,8 @@ def _auto_object_hook(obj: Any) -> Any:
295
296
  raise SerializationError("Cannot deserialize numpy array: numpy is not installed")
296
297
  if "data" not in obj or "shape" not in obj or "dtype" not in obj:
297
298
  raise SerializationError("Invalid ndarray format: missing required fields in cached data")
298
- return np.frombuffer(obj["data"], dtype=obj["dtype"]).reshape(obj["shape"])
299
+ # .copy(): writable result that does not alias the source buffer (the L1-cached bytes on a hit) — #157.
300
+ return np.frombuffer(obj["data"], dtype=obj["dtype"]).reshape(obj["shape"]).copy()
299
301
 
300
302
  return obj
301
303
 
@@ -492,15 +494,19 @@ class AutoSerializer:
492
494
  """
493
495
  # coerce unwrap's zero-copy memoryview; no-op when already bytes (enables .startswith below + Rust retrieve)
494
496
  data = bytes(data)
495
- # Check for custom NumPy format
496
- if data.startswith(b"NUMPY_RAW"):
497
+ # Custom NumPy format — raw [NUMPY_RAW...] or checksummed [8-byte xxHash3-64][NUMPY_RAW...].
498
+ # Detect by structure (like ArrowSerializer) so it is caught here, before the lossy
499
+ # retrieve()/msgpack fallback below — _deserialize_numpy strips + verifies the optional
500
+ # checksum and fails closed on mismatch (#155), even when no metadata is supplied.
501
+ if data.startswith(b"NUMPY_RAW") or (len(data) >= 17 and data[8:17] == b"NUMPY_RAW"):
497
502
  return self._deserialize_numpy(data)
498
503
 
499
504
  # Use metadata for format detection if available
500
505
  if metadata and hasattr(metadata, "original_type"):
501
506
  detected_format = metadata.original_type
502
507
 
503
- # For specialized formats, call type-specific deserializers
508
+ # For specialized formats, call type-specific deserializers.
509
+ # _deserialize_numpy strips + verifies the optional xxHash3-64 checksum prefix itself.
504
510
  if detected_format == "numpy":
505
511
  return self._deserialize_numpy(data)
506
512
  elif detected_format == "arrow":
@@ -513,31 +519,31 @@ class AutoSerializer:
513
519
  "Install with: pip install 'cachekit[data]'"
514
520
  )
515
521
  elif detected_format == "dataframe":
516
- # For DataFrame and Series, we need to handle both Rust envelope and direct data
517
522
  if self.enable_integrity_checking and len(data) > 4:
523
+ # Unwrap the ByteStorage envelope. A checksum mismatch (raised by retrieve) fails
524
+ # closed with a clear corruption error instead of being swallowed and re-parsed as
525
+ # raw msgpack, which lost the diagnostic and produced a confusing error (#156).
526
+ # The unpack/build sits OUTSIDE this guard so a genuine post-retrieve error surfaces
527
+ # as itself rather than being mistaken for corruption.
518
528
  try:
519
- # Try Rust envelope first
520
529
  original_data, _ = self._byte_storage.retrieve(data)
521
- unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
522
- return self._deserialize_dataframe(unpacked_data)
523
- except Exception as e:
524
- # Fall back to direct unpacking
525
- logger.debug(f"Rust envelope parsing failed for DataFrame, falling back to direct unpacking: {e}")
526
- # Direct msgpack data
530
+ except (ValueError, SerializationError) as e:
531
+ raise SerializationError(f"DataFrame integrity check failed (corrupted cache entry): {e}") from e
532
+ unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
533
+ return self._deserialize_dataframe(unpacked_data)
534
+ # Integrity off: data is direct msgpack (no envelope)
527
535
  unpacked_data = msgpack.unpackb(data, **self._msgpack_unpack_opts)
528
536
  return self._deserialize_dataframe(unpacked_data)
529
537
  elif detected_format == "series":
530
- # For DataFrame and Series, we need to handle both Rust envelope and direct data
531
538
  if self.enable_integrity_checking and len(data) > 4:
539
+ # Same fail-closed contract as the DataFrame branch above (#156).
532
540
  try:
533
- # Try Rust envelope first
534
541
  original_data, _ = self._byte_storage.retrieve(data)
535
- unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
536
- return self._deserialize_series(unpacked_data)
537
- except Exception as e:
538
- # Fall back to direct unpacking
539
- logger.debug(f"Rust envelope parsing failed for Series, falling back to direct unpacking: {e}")
540
- # Direct msgpack data
542
+ except (ValueError, SerializationError) as e:
543
+ raise SerializationError(f"Series integrity check failed (corrupted cache entry): {e}") from e
544
+ unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
545
+ return self._deserialize_series(unpacked_data)
546
+ # Integrity off: data is direct msgpack (no envelope)
541
547
  unpacked_data = msgpack.unpackb(data, **self._msgpack_unpack_opts)
542
548
  return self._deserialize_series(unpacked_data)
543
549
 
@@ -603,17 +609,20 @@ class AutoSerializer:
603
609
  return self._deserialize_numpy(data)
604
610
 
605
611
  def _serialize_numpy(self, arr: np.ndarray) -> bytes: # type: ignore[name-defined]
606
- """Serialize NumPy array with metadata, bypassing Rust ByteStorage.
612
+ """Serialize a NumPy array into the ``NUMPY_RAW`` binary format.
607
613
 
608
614
  Requires: numpy installed (HAS_NUMPY=True)
609
615
 
610
616
  Raises:
611
617
  RuntimeError: If numpy not installed
612
618
 
613
- Skips Rust layer because:
614
- - Compression ineffective on NumPy's random data patterns
615
- - Native serialization already fast (~2ms)
616
- - Rust overhead adds ~8ms with minimal benefit
619
+ When ``enable_integrity_checking`` is on (the default / ``@cache``), an 8-byte
620
+ xxHash3-64 checksum is prepended to the ``NUMPY_RAW`` payload with NO compression,
621
+ mirroring ``ArrowSerializer`` ([checksum][payload]). The numpy branch used to return
622
+ these bytes *unchecked*, so a corrupted entry was reconstructed as silently-wrong data
623
+ on read (#155). Compression is deliberately skipped: numpy is large, often-incompressible
624
+ binary, and LZ4 here measured ~100x slower with no size benefit. When integrity is off
625
+ (``@cache.minimal``), the raw payload is returned without a checksum (msgpack-off parity).
617
626
  """
618
627
  if not HAS_NUMPY:
619
628
  raise RuntimeError("NumPy not installed. Install with: pip install cachekit[data]")
@@ -627,7 +636,18 @@ class AutoSerializer:
627
636
  shape_len = len(shape_data).to_bytes(2, byteorder="little")
628
637
 
629
638
  # Combine: header + raw numpy bytes (zero-copy from NumPy)
630
- return b"NUMPY_RAW" + dtype_len + dtype_str + shape_len + shape_data + arr.tobytes()
639
+ raw = b"NUMPY_RAW" + dtype_len + dtype_str + shape_len + shape_data + arr.tobytes()
640
+
641
+ if self.enable_integrity_checking:
642
+ # Checksum-only envelope: prepend the 8-byte xxHash3-64 of the payload, NO compression.
643
+ # numpy arrays are large, often-incompressible binary; routing them through ByteStorage's
644
+ # LZ4 measured ~100x slower to serialize, ~400x slower to read, and inflated incompressible
645
+ # data ~1.56x. This mirrors ArrowSerializer's [8-byte xxHash3-64][payload] scheme, giving
646
+ # the #155 integrity guarantee at ~0 cost. The read side strips + verifies in
647
+ # _deserialize_numpy. (No LZ4 here means numpy is genuinely uncompressed, so the
648
+ # metadata.compressed=False set in serialize() is correct — sidesteps #166 entirely.)
649
+ return xxhash.xxh3_64_digest(raw) + raw
650
+ return raw
631
651
 
632
652
  def _deserialize_numpy(self, data: bytes) -> np.ndarray:
633
653
  """Deserialize NumPy array from NUMPY_RAW binary format.
@@ -641,6 +661,16 @@ class AutoSerializer:
641
661
  if not HAS_NUMPY:
642
662
  raise RuntimeError("NumPy not installed. Install with: pip install cachekit[data]")
643
663
 
664
+ # Strip + verify the optional 8-byte xxHash3-64 checksum prefix written by integrity-on
665
+ # serialization. Detect by structure (like ArrowSerializer): a checksummed entry is
666
+ # [8-byte checksum][NUMPY_RAW...]; a raw entry (integrity-off / legacy) is [NUMPY_RAW...].
667
+ # A mismatch fails closed (#155) — never reconstructs the corrupted array.
668
+ if not data.startswith(b"NUMPY_RAW") and len(data) >= 17 and data[8:17] == b"NUMPY_RAW":
669
+ body = data[8:]
670
+ if xxhash.xxh3_64_digest(body) != data[:8]:
671
+ raise SerializationError("NumPy integrity check failed: xxHash3-64 checksum mismatch (corrupted cache entry)")
672
+ data = body
673
+
644
674
  if not data.startswith(b"NUMPY_RAW"):
645
675
  raise SerializationError("Invalid NumPy data format - expected NUMPY_RAW header")
646
676
 
@@ -666,9 +696,11 @@ class AutoSerializer:
666
696
  shape.append(dim)
667
697
  shape = tuple(shape)
668
698
 
669
- # Extract raw numpy bytes and reconstruct
699
+ # Extract raw numpy bytes and reconstruct. .copy() so the result is writable and does
700
+ # not alias the source bytes (the L1-cached buffer on a hit) — see #157. frombuffer alone
701
+ # returns a read-only view aliasing the input.
670
702
  raw_bytes = data[offset:]
671
- arr = np.frombuffer(raw_bytes, dtype=dtype_str)
703
+ arr = np.frombuffer(raw_bytes, dtype=dtype_str).copy()
672
704
  return arr.reshape(shape)
673
705
  except (ValueError, IndexError, UnicodeDecodeError) as e:
674
706
  raise SerializationError(f"Failed to deserialize NumPy array: {e}") from e
@@ -729,8 +761,8 @@ class AutoSerializer:
729
761
  columns_data = {}
730
762
  for col, col_info in serialized["data"].items():
731
763
  if col_info["type"] == "numeric":
732
- # Reconstruct from NumPy bytes
733
- arr = np.frombuffer(col_info["data"], dtype=col_info["dtype"])
764
+ # Reconstruct from NumPy bytes; .copy() → writable, non-aliasing column (#157).
765
+ arr = np.frombuffer(col_info["data"], dtype=col_info["dtype"]).copy()
734
766
  columns_data[col] = arr
735
767
  else:
736
768
  # Use object data directly
@@ -793,7 +825,8 @@ class AutoSerializer:
793
825
  serialized = msgpack.unpackb(data, **self._msgpack_unpack_opts)
794
826
 
795
827
  if serialized["type"] == "numeric":
796
- values = np.frombuffer(serialized["data"], dtype=serialized["dtype"])
828
+ # .copy() → writable Series values that do not alias the source buffer (#157).
829
+ values = np.frombuffer(serialized["data"], dtype=serialized["dtype"]).copy()
797
830
  else:
798
831
  values = serialized["data"]
799
832
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes