Skip to content

Conversation

@strawgate
Copy link
Owner

@strawgate strawgate commented Oct 29, 2025

Summary

This PR implements a comprehensive refactoring of ManagedEntry serialization to address inconsistencies across stores and wrappers, as identified in issue #165.

Changes

1. Created Serialization Adapter Pattern:

  • Added SerializationAdapter base class with three implementations:
    • FullJsonAdapter: For Redis/Valkey (full JSON string)
    • StringifiedDictAdapter: For MongoDB (dict with stringified value)
    • ElasticsearchAdapter: For Elasticsearch (native or stringified)

2. Updated Stores:

  • MongoDB: Uses StringifiedDictAdapter
  • Elasticsearch: Uses ElasticsearchAdapter with native_storage flag
  • Redis: Uses FullJsonAdapter

3. Fixed Wrapper Inefficiencies:

  • Added estimate_serialized_size() utility function
  • Updated CompressionWrapper and LimitSizeWrapper
  • Eliminates unnecessary ManagedEntry instantiation

4. Removed Deprecated Functions:

  • Removed store-specific helper functions in favor of adapters
  • Updated all test files to use new adapter approach

Benefits

  • ✅ Consistent serialization interface across all stores
  • ✅ Separation of concerns (serialization logic in adapters)
  • ✅ Easier to add new stores or modify serialization
  • ✅ More efficient size calculations in wrappers
  • ✅ Better testability and maintainability

Closes #165


🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Centralized serialization via adapters across all stores for consistent on-disk/network formats and improved error handling.
  • New Features
    • Added a serialized-size estimator used by compression and size-limit wrappers.
    • ManagedEntry now exposes TTL and ISO-formatted timestamps as read-only properties and adds a from‑ttl constructor.
  • Tests
    • Expanded store tests to assert raw serialized document shapes and expiration handling across multiple backends.

@strawgate
Copy link
Owner Author

@claude please fix the merge conflicts and put the store-specific adapters in the store implementations

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@strawgate
Copy link
Owner Author

@claude this PR needs to have conflicts fixed and review the coderabbit feedback, its probably wrong in a bunch of spots so please be thorough

@claude
Copy link

claude bot commented Oct 29, 2025

Claude finished @strawgate's task —— View job


✅ PR #174: Conflicts Fixed and Adapter Pattern Applied

Summary

Successfully fixed the branch history issues and applied the serialization adapter pattern refactoring from scratch on a clean main branch.

What Was Done

1. Fixed Branch History

  • Branch had unrelated histories causing 100+ file conflicts
  • Reset to clean main (8ecc042) and manually re-applied changes
  • Force pushed to fix branch history

2. Implemented Adapter Pattern

  • Added SerializationAdapter base class with to_storage() and from_storage() methods
  • Implemented MongoDBAdapter with native BSON storage support
  • Implemented RedisAdapter for JSON string storage
  • Both stores now use consistent adapter-based serialization

3. Performance Improvements

  • Added estimate_serialized_size() utility function to managed_entry.py
  • Updated CompressionWrapper to avoid unnecessary ManagedEntry instantiation
  • Updated LimitSizeWrapper to use efficient size estimation

4. Error Handling

  • Added try/except blocks around all from_storage() calls
  • Deserialization errors now return None instead of crashing
  • Added type checking for adapter outputs to catch contract violations

5. Code Quality

  • Ran codegen to sync changes from async to sync library
  • Applied ruff formatting and linting
  • All code follows project standards

What Was NOT Done

  • Elasticsearch adapter: Not implemented (would require additional work)
  • Tests: Existing tests should work but haven't been run (no test environment access)
  • CodeRabbit feedback: Not all suggestions were addressed - many were incorrect or would introduce bugs (e.g., TTL field type suggestions that would break MongoDB TTL indexing)

Key Differences from Original PR

This implementation:

  • ✅ Works with current main's improvements (native_storage, better error messages)
  • ✅ Keeps existing helper functions for backward compatibility
  • ✅ Adds robust error handling for corrupt data
  • ✅ Includes type checking to catch contract violations
  • ✅ Has clean git history (1 commit vs 20+)

Next Steps

The maintainer should:

  1. Review the adapter implementation approach
  2. Run the test suite to verify no regressions
  3. Consider whether Elasticsearch needs the adapter pattern (currently not included)
  4. Evaluate CodeRabbit suggestions critically - many would introduce bugs

Commit: 796e8ec | Branch: claude/issue-165-20251029-0155

…is stores

This commit introduces a clean adapter pattern for store-specific serialization:

**Key Changes:**
- Add SerializationAdapter base class with to_storage() and from_storage() methods
- Implement MongoDBAdapter with native BSON storage support
- Implement RedisAdapter for JSON string storage
- Add estimate_serialized_size() utility function to managed_entry.py
- Update CompressionWrapper and LimitSizeWrapper to use estimate_serialized_size()
- Add error handling for deserialization failures (return None instead of crashing)
- Add type checking for adapter outputs to catch contract violations early

**Benefits:**
- Consistent serialization interface across stores
- Better separation of concerns (serialization logic in adapters)
- More efficient size calculations without ManagedEntry instantiation
- Easier to test and maintain
- Improved error handling

Co-authored-by: William Easton <[email protected]>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@claude claude bot force-pushed the claude/issue-165-20251029-0155 branch from 36aa82e to 796e8ec Compare October 29, 2025 03:02
@strawgate
Copy link
Owner Author

@claude Each store shouldn't have it's own ABC for SerializationAdapter and we should have a couple common serialization adapters as described so that each store doesn't need to implement basic ones

We also need to lint and codegen

- Create shared SerializationAdapter ABC in key_value.shared.utils.serialization
- Add common adapter implementations:
  - FullJsonAdapter: For Redis/Valkey (full JSON strings)
  - StringifiedDictAdapter: For stores preferring dict with stringified values
  - MongoDBAdapter: With native BSON datetime support for TTL indexing
  - ElasticsearchAdapter: With native/string storage mode support
- Update MongoDB, Redis, and Elasticsearch stores to use shared adapters
- Remove duplicate SerializationAdapter definitions from each store
- Remove store-specific helper functions (managed_entry_to_document, etc.)
- Run codegen to sync changes to sync library
- All linting passes

This eliminates code duplication and provides a consistent serialization
interface across all stores, making it easier to add new stores or modify
serialization behavior.

Co-authored-by: William Easton <[email protected]>
coderabbitai[bot]

This comment was marked as resolved.

@strawgate
Copy link
Owner Author

@claude let's put the store-specific adapters in the store's code and let's move as much of the serialization/validation logic out of managedentry as we can into the adapter base class maybe?

@claude

This comment was marked as resolved.

- Simplified shared serialization module to contain only SerializationAdapter base class
- Moved FullJsonAdapter to Redis store module
- Moved MongoDBAdapter to MongoDB store module
- Moved ElasticsearchAdapter to Elasticsearch store module
- Updated imports in all affected modules
- Ran codegen to sync async changes to sync library
- Applied linting fixes

Co-authored-by: William Easton <[email protected]>
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)

132-137: Stop passing the removed ttl kwarg and restore TTL fallback logic

ManagedEntry no longer accepts a ttl= parameter after the field was deleted, yet from_dict still calls cls(..., ttl=ttl_seconds, …). This is exactly what basedpyright is flagging (“No parameter named 'ttl'”), and it will raise a TypeError at runtime. We need to drop that kwarg and, if a TTL override is supplied, translate it into an expiration timestamp before instantiating the dataclass.

-        ttl_seconds: float | None = prepare_ttl(t=ttl)
-
-        return cls(
-            created_at=created_at,
-            expires_at=expires_at,
-            ttl=ttl_seconds,
-            value=value,
-        )
+        ttl_seconds: float | None = prepare_ttl(t=ttl)
+
+        if ttl_seconds is not None and expires_at is None:
+            expires_at = now() + timedelta(seconds=ttl_seconds)
+
+        return cls(
+            created_at=created_at,
+            expires_at=expires_at,
+            value=value,
+        )

(Remember to import timedelta alongside datetime.) Without this fix we can’t ship—both the type checker and runtime will fail here.

♻️ Duplicate comments (2)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (1)

66-69: Critical: Mapping type mismatch will cause indexing failures.

This issue was flagged in a previous review: value.string is mapped as type: "object" with enabled: False, but the adapter writes a JSON string to this field (line 115 in prepare_dump). Elasticsearch will reject documents during indexing with a mapper_parsing_exception.

The fix is to map value.string as a non-indexed text field:

                 "string": {
-                    "type": "object",
-                    "enabled": False,
+                    "type": "text",
+                    "index": False
                 },

Update the comment above accordingly and test with both native_storage modes.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (1)

72-81: Fix empty dict handling in prepare_load.

The walrus operator at line 75 evaluates falsy when value_object is an empty dict ({}), causing valid entries with empty values to skip native deserialization and fall through to the error case. This was flagged in a previous review.

Apply this diff:

     @override
     def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
         value = data.pop("value")
 
-        if value_object := value.get("object"):
-            data["value"] = value_object
-        elif value_string := value.get("string"):
-            data["value"] = value_string
+        if "object" in value:
+            data["value"] = value["object"]
+        elif "string" in value:
+            data["value"] = value["string"]
         else:
             msg = "Value field not found in MongoDB document"
             raise DeserializationError(message=msg)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49cf605 and 714b0cb.

📒 Files selected for processing (17)
  • key-value/key-value-aio/src/key_value/aio/stores/base.py (5 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (8 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (8 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/redis/store.py (8 hunks)
  • key-value/key-value-aio/tests/stores/elasticsearch/test_elasticsearch.py (3 hunks)
  • key-value/key-value-aio/tests/stores/mongodb/test_mongodb.py (5 hunks)
  • key-value/key-value-aio/tests/stores/redis/test_redis.py (4 hunks)
  • key-value/key-value-shared/pyproject.toml (0 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (3 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (8 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (8 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/redis/store.py (8 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/elasticsearch/test_elasticsearch.py (5 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/mongodb/test_mongodb.py (5 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py (4 hunks)
💤 Files with no reviewable changes (1)
  • key-value/key-value-shared/pyproject.toml
🧰 Additional context used
🧬 Code graph analysis (16)
key-value/key-value-aio/src/key_value/aio/stores/base.py (3)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • BasicSerializationAdapter (121-133)
  • SerializationAdapter (28-118)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • prepare_entry_timestamps (77-86)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ttl (34-37)
  • ManagedEntry (14-143)
key-value/key-value-sync/tests/code_gen/stores/elasticsearch/test_elasticsearch.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (1)
  • ElasticsearchSerializationAdapter (68-111)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • dump_dict (102-114)
  • load_dict (57-94)
key-value/key-value-sync/tests/code_gen/stores/mongodb/test_mongodb.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (1)
  • MongoDBSerializationAdapter (45-94)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • dump_dict (102-114)
  • load_dict (57-94)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (3)
  • ElasticsearchSerializationAdapter (68-111)
  • prepare_dump (87-97)
  • prepare_load (100-111)
key-value/key-value-aio/tests/stores/mongodb/test_mongodb.py (2)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (1)
  • MongoDBSerializationAdapter (38-87)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • dump_dict (102-114)
  • load_dict (57-94)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (3)
  • MongoDBSerializationAdapter (45-94)
  • prepare_dump (59-69)
  • prepare_load (72-94)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (3)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • BasicSerializationAdapter (121-133)
  • SerializationAdapter (28-118)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • prepare_entry_timestamps (77-86)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ttl (34-37)
  • ManagedEntry (14-143)
key-value/key-value-aio/tests/stores/elasticsearch/test_elasticsearch.py (2)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (1)
  • ElasticsearchSerializationAdapter (88-131)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • dump_dict (102-114)
  • load_dict (57-94)
key-value/key-value-aio/tests/stores/redis/test_redis.py (2)
key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py (2)
  • get_client_from_store (42-43)
  • store (61-66)
key-value/key-value-aio/src/key_value/aio/stores/redis/store.py (1)
  • RedisStore (25-220)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (5)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (8)
  • ManagedEntry (14-143)
  • dump_to_json (146-151)
  • load_from_json (154-160)
  • verify_dict (163-172)
  • value_as_dict (45-46)
  • value_as_json (40-42)
  • created_at_isoformat (49-50)
  • expires_at_isoformat (53-54)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (2)
  • prepare_load (120-131)
  • prepare_dump (107-117)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (2)
  • prepare_load (65-87)
  • prepare_dump (52-62)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (3)
  • MongoDBSerializationAdapter (38-87)
  • prepare_dump (52-62)
  • prepare_load (65-87)
key-value/key-value-aio/src/key_value/aio/stores/redis/store.py (3)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (4)
  • BasicSerializationAdapter (121-133)
  • SerializationAdapter (28-118)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (5)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • now_as_epoch (14-16)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (3)
  • ElasticsearchSerializationAdapter (88-131)
  • prepare_dump (107-117)
  • prepare_load (120-131)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/redis/store.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/compound.py (2)
  • compound_key (32-34)
  • get_keys_from_compound_keys (74-77)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-143)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (4)
  • BasicSerializationAdapter (121-133)
  • SerializationAdapter (28-118)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py (2)
key-value/key-value-aio/tests/stores/redis/test_redis.py (2)
  • get_client_from_store (42-43)
  • store (61-66)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/redis/store.py (1)
  • RedisStore (28-211)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (3)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (6)
  • now (19-21)
  • prepare_ttl (45-45)
  • prepare_ttl (49-49)
  • prepare_ttl (52-74)
  • seconds_to (24-26)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
🪛 GitHub Actions: Run Tests
key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py

[error] 99-99: basedpyright: Argument type is unknown. Argument corresponds to parameter "s" in function "loads" (reportUnknownArgumentType).

key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py

[error] 135-135: basedpyright: No parameter named 'ttl' (reportCallIssue). Command: uv run basedpyright .

🪛 GitHub Check: static_analysis (key-value/key-value-sync)
key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py

[failure] 124-124:
Argument of type "ResponseT" cannot be assigned to parameter "s" of type "str | bytes | bytearray" in function "loads"
  Type "ResponseT" is not assignable to type "str | bytes | bytearray"
    Type "Awaitable[Any]" is not assignable to type "str | bytes | bytearray"
      "Awaitable[Any]" is not assignable to "str"
      "Awaitable[Any]" is not assignable to "bytes"
      "Awaitable[Any]" is not assignable to "bytearray" (reportArgumentType)


[failure] 114-114:
"Awaitable[Any]" is not iterable
  "iter" method not defined (reportGeneralTypeIssues)


[failure] 114-114:
Type of "raw_document" is unknown (reportUnknownVariableType)


[failure] 114-114:
Argument type is unknown
  Argument corresponds to parameter "s" in function "loads" (reportUnknownArgumentType)


[failure] 99-99:
"Awaitable[Any]" is not iterable
  "iter" method not defined (reportGeneralTypeIssues)


[failure] 99-99:
Type of "raw_document" is unknown (reportUnknownVariableType)


[failure] 99-99:
Argument type is unknown
  Argument corresponds to parameter "s" in function "loads" (reportUnknownArgumentType)

🔇 Additional comments (24)
key-value/key-value-sync/tests/code_gen/stores/redis/test_redis.py (2)

42-44: LGTM: Test helper for accessing client.

The helper appropriately exposes the private client for test validation while acknowledging the private usage with a type ignore.


68-71: LGTM: Clean fixture design.

The fixture appropriately provides Redis client access for raw document validation tests.

key-value/key-value-aio/tests/stores/elasticsearch/test_elasticsearch.py (2)

53-75: LGTM: Solid adapter round-trip test.

The test correctly validates the adapter's non-native storage mode with proper round-trip verification. Document structure and metadata preservation are thoroughly checked.


77-99: LGTM: Native storage mode validated.

The test appropriately verifies native storage mode with flattened value structure and complete round-trip validation.

key-value/key-value-sync/tests/code_gen/stores/elasticsearch/test_elasticsearch.py (1)

54-91: LGTM: Consistent adapter test coverage.

Both non-native and native storage modes are properly tested with round-trip validation. The sync version correctly mirrors the async implementation.

key-value/key-value-aio/src/key_value/aio/stores/redis/store.py (3)

28-88: LGTM: Adapter initialization is appropriate.

The store correctly initializes BasicSerializationAdapter with isoformat dates and dict value format, which aligns with Redis's JSON string storage model.


90-123: LGTM: Robust deserialization with error handling.

The get operations properly handle DeserializationError by returning None for corrupt documents, preventing failures from propagating and affecting other entries in batch operations.


125-182: LGTM: Unified serialization through adapter.

Put operations correctly delegate to dump_json, and bulk operations appropriately handle both TTL and non-TTL paths with proper use of mset vs. pipelined setex.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (3)

127-183: LGTM: Adapter properly initialized and stored.

The adapter is correctly instantiated with the native_storage parameter and stored for use across all serialization operations.


217-326: LGTM: Comprehensive adapter integration with robust error handling.

All get/put operations correctly delegate to the adapter, and deserialization errors are properly caught with appropriate logging for bulk operations.


68-111: Verify whether Elasticsearch adapter should be auto-generated or intentionally duplicated.

The duplication is confirmed: ElasticsearchSerializationAdapter appears identically in both sync and async store files, and the same pattern exists for MongoDBSerializationAdapter. However, the sync adapter lacks the auto-generated header present in other code_gen files, making it unclear whether this duplication is intentional per your PR design or an oversight.

Per your PR objectives ("move store-specific adapters into the store implementations"), the per-store implementations may be by design. However, given that the sync code is in the code_gen directory and build_sync_library.py exists to transform async→sync, consider whether adapters should be auto-generated from the async versions instead of maintained manually across both codebases.

key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)

19-26: LGTM: Type-safe dictionary validator.

The key_must_be helper provides a clean, type-safe way to validate dictionary fields with proper error messages.


102-118: LGTM: Clean serialization with configurable formats.

The dump methods appropriately handle value and date format variations, with proper hook points for store-specific transformations.


121-133: LGTM: Clean identity adapter implementation.

BasicSerializationAdapter appropriately provides no-op transformations for stores like Redis that don't require special field handling.

key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (2)

90-176: LGTM: Adapter properly initialized.

The MongoDB adapter is correctly instantiated with the native_storage flag and integrated into the store's initialization sequence.


227-317: LGTM: Complete adapter integration with metadata preservation.

All serialization operations correctly use the adapter, and put operations appropriately include collection and key fields in stored documents for query support. Error handling ensures corrupt documents don't break batch operations.

key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (2)

147-147: Adapter integration looks good.

The _adapter field is properly declared and initialized with the store's native_storage configuration.

Also applies to: 212-212


265-267: LGTM: Adapter methods properly integrated.

The adapter's load_dict and dump_dict methods are correctly used in get/put operations, with appropriate error handling for deserialization failures.

Also applies to: 298-299, 329-329, 367-367

key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (6)

45-57: LGTM: Adapter initialization.

The MongoDB adapter is correctly initialized with appropriate date and value formats for BSON storage.


58-69: LGTM: prepare_dump implementation.

The method correctly transforms the flat value structure into MongoDB's nested document format.


83-92: LGTM: Timezone handling.

The datetime fields are correctly converted to UTC timezone-aware datetimes for consistency.


103-103: Adapter integration looks good.

The _adapter field is properly declared and initialized with the store's native_storage configuration.

Also applies to: 181-181


238-241: LGTM: Error handling in deserialization.

The adapter's load_dict calls are properly wrapped with exception handling, returning None for corrupted or missing entries.

Also applies to: 259-262


268-274: LGTM: Adapter usage in put operations.

The dump_dict results are correctly spread into MongoDB update operations, with collection and key metadata properly added.

Also applies to: 294-298

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)

141-144: Also remove unused ttl parameter from from_json.

Since the ttl parameter is unused in from_dict (see previous comment), it should also be removed from from_json for consistency.

Apply this diff:

     @classmethod
-    def from_json(cls, json_str: str, includes_metadata: bool = True, ttl: SupportsFloat | None = None) -> Self:
+    def from_json(cls, json_str: str, includes_metadata: bool = True) -> Self:
         data: dict[str, Any] = load_from_json(json_str=json_str)
 
-        return cls.from_dict(data=data, includes_metadata=includes_metadata, ttl=ttl)
+        return cls.from_dict(data=data, includes_metadata=includes_metadata)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 714b0cb and 17743fe.

📒 Files selected for processing (1)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (5)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (3)
  • now (19-21)
  • seconds_to (24-26)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
key-value/key-value-aio/src/key_value/aio/wrappers/compression/wrapper.py (1)
  • ttl (124-126)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/compression/wrapper.py (1)
  • ttl (119-121)
🪛 GitHub Actions: Run Tests
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py

[error] 88-88: ARG003 Unused class method argument: ttl

🔇 Additional comments (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (3)

33-37: LGTM! TTL property correctly derives from expires_at.

The computed property implementation correctly derives TTL from expires_at using seconds_to, which aligns with the PR's objective to establish consistent TTL handling across stores.


48-54: LGTM! Convenient ISO format properties.

These properties provide a clean interface for serialization adapters to access ISO-formatted timestamps, improving consistency across the codebase.


176-189: The dict(value) conversion is necessary for type consistency.

Regarding the previous review comment: the dict(value) conversion should be kept because:

  1. dump_to_json expects dict[str, Any] per its type signature
  2. json.dumps with sort_keys=True requires a concrete dict for consistent behavior
  3. Converting Mapping to dict ensures predictable size calculation

The conversion is appropriate for this size estimation utility.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (1)

137-139: Verify inconsistent exclude_none parameter usage.

This code passes exclude_none=False to dump_json, but the aio DiskStore at Line 110 of store.py doesn't specify this parameter (defaults to True). This creates inconsistent serialization behavior between DiskStore and MultiDiskStore.

Ensure this difference is intentional and aligned with the sync version.

key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)

46-46: Critical: verify_dict will fail for non-dict Mappings.

self.value is typed as Mapping[str, Any], but verify_dict on Line 172 checks isinstance(obj, dict), which will raise a DeserializationError for valid Mapping types like MappingProxyType or OrderedDict. This is the same issue flagged in previous reviews.

Apply this diff to convert to dict before verification:

     @property
     def value_as_dict(self) -> dict[str, Any]:
-        return verify_dict(obj=self.value)
+        return verify_dict(obj=dict(self.value))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17743fe and 0dbf7d6.

📒 Files selected for processing (5)
  • key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (3 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/disk/store.py (3 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (2 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/store.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (4)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
key-value/key-value-aio/src/key_value/aio/stores/disk/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (4)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (1)
  • _put_managed_entry (128-139)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/store.py (1)
  • _put_managed_entry (104-107)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (3)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (4)
  • now (19-21)
  • now_plus (29-31)
  • seconds_to (24-26)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
🪛 GitHub Actions: Run Tests
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py

[error] 61-61: pyright: Argument of type 'SupportsFloat' cannot be assigned to parameter 'seconds' of type 'float' in function 'now_plus'. 'SupportsFloat' is not assignable to 'float' (reportArgumentType).

🪛 GitHub Check: static_analysis (key-value/key-value-shared)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py

[failure] 61-61:
Argument of type "SupportsFloat" cannot be assigned to parameter "seconds" of type "float" in function "now_plus"
  "SupportsFloat" is not assignable to "float" (reportArgumentType)

🔇 Additional comments (8)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/store.py (2)

4-4: LGTM: Adapter-based deserialization with timezone-aware expiration.

The migration to _serialization_adapter.load_json and explicit timezone-aware expires_at assignment is correct and aligns with the adapter pattern.

Also applies to: 96-99


107-107: LGTM: Adapter-based serialization.

The use of _serialization_adapter.dump_json correctly centralizes serialization logic.

key-value/key-value-aio/src/key_value/aio/stores/disk/store.py (2)

1-1: LGTM: Consistent with sync implementation.

The async version correctly mirrors the sync implementation with adapter-based deserialization and timezone-aware expiration handling.

Also applies to: 93-96


110-110: LGTM: Adapter-based serialization.

Correctly uses the serialization adapter for persistence.

key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (3)

33-37: LGTM: TTL property correctly derives from expires_at.

The new ttl property properly computes TTL dynamically from expires_at, maintaining backward compatibility while removing the stored ttl field.


48-54: LGTM: ISO format properties for timestamps.

The created_at_isoformat and expires_at_isoformat properties provide convenient access to ISO-formatted timestamp strings.


183-196: LGTM: Efficient size estimation utility.

The estimate_serialized_size function provides an efficient way to estimate JSON size without creating a full ManagedEntry, addressing wrapper performance concerns mentioned in the PR objectives.

Note: The dict(value) conversion on Line 196 was previously flagged as potentially unnecessary, but it ensures consistency with JSON serialization behavior (especially for sort_keys=True).

key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (1)

134-136: Verify inconsistent exclude_none parameter usage.

The inconsistency is confirmed: MultiDiskStore passes exclude_none=False while DiskStore omits the parameter (defaults to True). This pattern is consistent across both sync and async implementations, but there is no documentation or test validation explaining why they differ.

Since exclude_none=False explicitly includes None values in serialized JSON, this could affect data integrity across collections. Verify whether this difference is intentional and required for MultiDiskStore, or if both stores should use consistent serialization behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (5)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (1)

66-69: Critical: Mapping type mismatch for value.string remains unfixed.

Despite being marked as addressed in a previous review, the DEFAULT_MAPPING still declares value.string as type: "object" with enabled: False. When native_storage=False, the adapter's prepare_dump method (line 115) writes a JSON string to this field, causing Elasticsearch to reject documents with a mapper_parsing_exception because the field expects an object, not a string.

Apply this fix to DEFAULT_MAPPING:

                 "string": {
-                    "type": "object",
-                    "enabled": False,
+                    "type": "text",
+                    "index": False
                 },

Update the comment above this field (lines 64-65) to reflect the change. Then run integration tests with native_storage=False to verify documents index successfully.

key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (1)

38-87: CRITICAL: TTL index will fail because dates are stored as ISO strings instead of datetime objects.

The base SerializationAdapter.dump_dict() method always converts created_at and expires_at to ISO format strings (via entry.created_at_isoformat and entry.expires_at_isoformat). However, MongoDB's TTL index (created at line 222) requires these fields to contain BSON datetime objects, not strings. With the current implementation, documents will never be automatically expired.

Apply this diff to convert ISO strings back to datetime objects in prepare_dump:

 @override
 def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
+    from datetime import datetime
+    
     value = data.pop("value")
 
     data["value"] = {}
 
     if self._native_storage:
         data["value"]["object"] = value
     else:
         data["value"]["string"] = value
+    
+    # Convert ISO format strings back to datetime objects for MongoDB TTL index
+    if created_at_str := data.get("created_at"):
+        if isinstance(created_at_str, str):
+            from key_value.shared.utils.time import try_parse_datetime_str
+            data["created_at"] = try_parse_datetime_str(value=created_at_str)
+    
+    if expires_at_str := data.get("expires_at"):
+        if isinstance(expires_at_str, str):
+            from key_value.shared.utils.time import try_parse_datetime_str
+            data["expires_at"] = try_parse_datetime_str(value=expires_at_str)
 
     return data

Alternatively, override dump_dict to produce datetime objects directly:

+@override
+def dump_dict(self, entry: ManagedEntry, exclude_none: bool = True) -> dict[str, Any]:
+    """Convert a ManagedEntry to a dictionary with BSON datetime objects."""
+    data: dict[str, Any] = {
+        "value": entry.value_as_dict if self._value_format == "dict" else entry.value_as_json,
+        "created_at": entry.created_at,
+        "expires_at": entry.expires_at,
+    }
+    
+    if exclude_none:
+        data = {k: v for k, v in data.items() if v is not None}
+    
+    return self.prepare_dump(data=data)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)

183-196: Consider whether dict(value) conversion is necessary.

Line 196 converts the Mapping to dict before serialization. Since dump_to_json calls json.dumps(), which can handle Mapping types directly, this conversion might be redundant.

If the conversion is not necessary for correctness or sort_keys=True behavior, consider simplifying to:

-    return len(dump_to_json(obj=dict(value)))
+    return len(dump_to_json(obj=value))

However, keep the conversion if it's needed for consistency with other parts of the codebase or to ensure predictable serialization behavior. Based on learnings


56-62: Logic error: TTL of 0 will not set expiration.

Line 61 uses if ttl which means a TTL of 0 will be treated as falsy, resulting in expires_at=None instead of an immediately-expired entry. This breaks the semantics of zero-TTL entries.

Apply this diff to always compute expires_at from the TTL:

     @classmethod
     def from_ttl(cls, *, value: Mapping[str, Any], created_at: datetime | None = None, ttl: SupportsFloat) -> Self:
         return cls(
             value=value,
             created_at=created_at,
-            expires_at=(now_plus(seconds=float(ttl)) if ttl else None),
+            expires_at=now_plus(seconds=float(ttl)),
         )

Note: If you need to support None as a TTL value for "no expiration," change the type signature to ttl: SupportsFloat | None and keep the conditional, but check explicitly for None rather than relying on truthiness: expires_at=(now_plus(seconds=float(ttl)) if ttl is not None else None).

key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (1)

7-7: Fix incorrect import of datetime.

Line 7 imports datetime from key_value.shared.utils.managed_entry, but that module imports datetime from the standard library and does not re-export it. This import will fail or behave unexpectedly.

Apply this diff to import datetime directly from the standard library:

-from key_value.shared.utils.managed_entry import ManagedEntry, datetime
+from datetime import datetime
+
+from key_value.shared.utils.managed_entry import ManagedEntry
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dbf7d6 and 8c4b6d3.

📒 Files selected for processing (7)
  • key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (3 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (8 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (8 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (2 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (8 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-151)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (3)
  • MongoDBSerializationAdapter (45-94)
  • prepare_dump (59-69)
  • prepare_load (72-94)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (4)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-151)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • now_as_epoch (14-16)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (3)
  • ElasticsearchSerializationAdapter (68-111)
  • prepare_dump (87-97)
  • prepare_load (100-111)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (14-151)
  • ttl (34-37)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-151)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (4)
  • MongoDBSerializationAdapter (38-87)
  • prepare_dump (52-62)
  • prepare_load (65-87)
  • _put_managed_entry (260-281)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (2)
  • DeserializationError (14-15)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (4)
  • now (19-21)
  • now_plus (29-31)
  • seconds_to (24-26)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/base.py (1)
  • ttl (219-228)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (14-151)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (7)
  • SerializationAdapter (28-118)
  • prepare_dump (97-100)
  • prepare_dump (132-133)
  • prepare_load (52-55)
  • prepare_load (129-130)
  • load_dict (57-94)
  • dump_dict (102-114)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (3)
  • ElasticsearchSerializationAdapter (88-131)
  • prepare_dump (107-117)
  • prepare_load (120-131)
🔇 Additional comments (19)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (3)

126-127: Timezone fix correctly applied.

The previous review concern about naive datetime has been resolved. The code now correctly uses tz=timezone.utc when creating the datetime from the timestamp, ensuring timezone-aware datetimes throughout.


135-137: Correct adapter usage and TTL handling.

The serialization via dump_json with exclude_none=False ensures complete metadata persistence, and using managed_entry.ttl (computed from expires_at) correctly provides the diskcache expiration value.


124-124: No issues found. The _serialization_adapter is properly initialized in the base class.

The verification confirms that self._serialization_adapter is initialized in key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py:103 as BasicSerializationAdapter(). The disk store classes inherit this attribute from the base class and use it correctly. The adapter type is BasicSerializationAdapter, which is the standard serialization adapter for these stores.

key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (3)

88-131: LGTM: Adapter correctly implements dual storage modes.

The ElasticsearchSerializationAdapter properly extends the SerializationAdapter base class and supports both native (flattened) and string storage modes. The prepare_load method correctly uses key presence checks ("flattened" in value) instead of truthiness checks, which prevents false negatives when deserializing empty dicts or empty strings—addressing a previously flagged issue.


147-148: LGTM: Adapter properly integrated into store.

The _adapter field is correctly declared and initialized with the ElasticsearchSerializationAdapter instance, passing through the native_storage configuration. This follows good dependency injection practices.

Also applies to: 212-212


265-265: LGTM: Adapter usage is consistent and type-safe.

The store correctly delegates serialization to self._adapter.dump_dict() (lines 329, 367) and deserialization to self._adapter.load_dict() (lines 265, 298). The refactor from the previous to_storage()/from_storage() methods to dump_dict()/load_dict() resolves prior type checking issues, as dump_dict() returns a concrete dict[str, Any] rather than a union type. The pattern is consistently applied across both single-document and batch operations.

Also applies to: 298-298, 329-329, 367-367

key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (3)

96-96: LGTM: Adapter initialization is clean.

The adapter is properly initialized with the native_storage parameter and stored as an instance field. While injection would increase testability, the current approach is sufficient for the store's needs.

Also applies to: 174-174


231-234: LGTM: Robust error handling for corrupted documents.

Catching DeserializationError and returning None provides graceful degradation when individual documents are corrupted. This prevents a single bad document from failing entire queries.

Also applies to: 252-255


267-281: LGTM: Adapter integration in put operations is clean.

The adapter's dump_dict() output is correctly spread alongside collection and key metadata. The update pattern with $set and upsert=True is appropriate for both single and bulk operations.

Also applies to: 301-315

key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py (7)

12-15: LGTM! Imports correctly updated for adapter pattern.

The imports have been properly refactored to support the new serialization adapter approach. The previous circular import concern with try_parse_datetime_str has been resolved by delegating datetime parsing to the base adapter class.


68-111: Well-designed adapter implementation with backward compatibility.

The ElasticsearchSerializationAdapter correctly implements the serialization pattern for Elasticsearch's dual storage modes. The implementation is particularly robust because prepare_load (lines 100-111) checks for both "flattened" and "string" keys, enabling seamless handling of documents stored in either mode—valuable for migrations or mixed storage scenarios.

The nested structure created by prepare_dump (lines 87-97) correctly matches the DEFAULT_MAPPING schema defined at lines 48-56.


181-181: LGTM! Adapter correctly initialized.

The adapter is properly initialized with the native_storage flag, ensuring consistent serialization behavior throughout the store's lifecycle.


228-231: LGTM! Proper adapter usage with appropriate error handling.

The deserialization correctly delegates to self._adapter.load_dict() and handles failures gracefully by returning None, which is appropriate behavior for a get operation.


261-269: LGTM! Robust batch deserialization with enhanced error handling.

The batch operation properly uses the adapter for deserialization and includes informative error logging (lines 264-268) while gracefully handling failures. The logging includes useful context (collection, document_id, error message) for debugging corrupted documents.


283-289: LGTM! Proper adapter usage for serialization.

The serialization correctly delegates to self._adapter.dump_dict() and appropriately wraps Elasticsearch's serialization errors in the store's SerializationError type for consistent error handling across stores.


311-326: LGTM! Consistent adapter usage in bulk operations.

The bulk put operation correctly serializes each entry individually using the adapter (line 316) and builds the bulk operations list properly. Error handling is consistent with the single-put operation, converting Elasticsearch serialization errors to store-level SerializationError exceptions.

key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (1)

123-124: Timezone handling correctly implemented.

The code now properly creates timezone-aware datetimes using tz=timezone.utc, which addresses the previous review concern about naive datetimes.

key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)

33-37: TTL property correctly implemented.

The conversion of ttl from a stored field to a computed property that derives its value from expires_at is correct and aligns with the PR's goal of consistent TTL handling.


172-180: Good fix: verify_dict now handles all Mapping types.

The change from checking isinstance(obj, dict) to isinstance(obj, Mapping) and converting to dict properly addresses the previous review concern. This ensures that self.value (typed as Mapping[str, Any]) can be validated without errors, including non-dict Mapping implementations like MappingProxyType.

# DO NOT CHANGE! Change the original file instead.
import time
from collections.abc import Callable
from datetime import timezone
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider importing datetime directly from the datetime module for consistency.

The code imports timezone directly from the datetime module (line 5) but imports datetime from managed_entry (line 10). While this works, importing both from the same source improves clarity.

Apply this diff to make imports consistent:

-from datetime import timezone
+from datetime import datetime, timezone
 from pathlib import Path
 from typing import overload
 
 from key_value.shared.utils.compound import compound_key
-from key_value.shared.utils.managed_entry import ManagedEntry, datetime
+from key_value.shared.utils.managed_entry import ManagedEntry

Also applies to: 10-10

🤖 Prompt for AI Agents
In
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py
around lines 5 and 10, the imports are inconsistent: timezone is imported from
the datetime module (line 5) while datetime is imported from managed_entry (line
10); update imports to import both datetime and timezone from the datetime
module (e.g., from datetime import datetime, timezone) and remove the datetime
import from managed_entry so all datetime objects come from the same source,
ensuring any references to managed_entry.datetime are updated to use the
standard datetime import.


_index_prefix: str

_native_storage: bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider whether store-level _native_storage is needed.

The _native_storage field is set in __init__ (line 179) but appears to only be used to initialize the adapter (line 181). Since the adapter maintains its own _native_storage field, this store-level field may be redundant.

However, keeping it could be intentional for API consistency or future use, and removing it would be a breaking change to the constructor signature.

🤖 Prompt for AI Agents
key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py
lines 125 (and refer to __init__ around 179-181): the store-level
_native_storage attribute appears redundant because it's only assigned in
__init__ and immediately passed to the adapter which already tracks its own
_native_storage; either remove the store-level field and constructor parameter
and reference adapter._native_storage everywhere the store would need it, or
keep the field but document its purpose and use it explicitly (or forward it
consistently) to avoid redundancy — if removing, update the constructor
signature and call sites (noting this is a breaking change), and if keeping, add
a brief comment explaining API consistency/future use and ensure it is actually
referenced beyond initializing the adapter.

Comment on lines +72 to +92
def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
value = data.pop("value")

# Store in appropriate field based on mode
if native_storage:
document["value"]["object"] = managed_entry.value_as_dict
else:
document["value"]["string"] = json_str
if "object" in value:
data["value"] = value["object"]
elif "string" in value:
data["value"] = value["string"]
else:
msg = "Value field not found in MongoDB document"
raise DeserializationError(message=msg)

# Add metadata fields
if managed_entry.created_at:
document["created_at"] = managed_entry.created_at
if managed_entry.expires_at:
document["expires_at"] = managed_entry.expires_at
if date_created := data.get("created_at"):
if not isinstance(date_created, datetime):
msg = "Expected `created_at` field to be a datetime"
raise DeserializationError(message=msg)
data["created_at"] = date_created.replace(tzinfo=timezone.utc)
if date_expires := data.get("expires_at"):
if not isinstance(date_expires, datetime):
msg = "Expected `expires_at` field to be a datetime"
raise DeserializationError(message=msg)
data["expires_at"] = date_expires.replace(tzinfo=timezone.utc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore backwards compatibility for legacy documents.

Before this refactor, MongoDBStore wrote documents with value stored directly (dict in native mode, JSON string otherwise) and timestamps serialized via ManagedEntry.to_dict() (ISO strings). The new adapter only accepts the nested {"value": {"object"| "string"}} shape and requires datetime instances, so every legacy record triggers DeserializationError and is dropped, effectively hiding all existing data. Please add a fallback path in prepare_load() that recognizes the old shapes (plain dict/str payloads and ISO timestamp strings) and normalizes them into the new canonical form before delegating to load_dict(), or provide a migration that rewrites stored documents. Example adjustment:

@@
-        value = data.pop("value")
-
-        if "object" in value:
-            data["value"] = value["object"]
-        elif "string" in value:
-            data["value"] = value["string"]
+        value = data.pop("value")
+
+        if "object" in value:
+            data["value"] = value["object"]
+        elif "string" in value:
+            data["value"] = value["string"]
+        elif self._native_storage and isinstance(value, dict):
+            data["value"] = value  # legacy native document layout
+        elif not self._native_storage and isinstance(value, str):
+            data["value"] = value  # legacy stringified layout
         else:
             msg = "Value field not found in MongoDB document"
             raise DeserializationError(message=msg)
 
-        if date_created := data.get("created_at"):
-            if not isinstance(date_created, datetime):
-                msg = "Expected `created_at` field to be a datetime"
-                raise DeserializationError(message=msg)
-            data["created_at"] = date_created.replace(tzinfo=timezone.utc)
-        if date_expires := data.get("expires_at"):
-            if not isinstance(date_expires, datetime):
-                msg = "Expected `expires_at` field to be a datetime"
-                raise DeserializationError(message=msg)
-            data["expires_at"] = date_expires.replace(tzinfo=timezone.utc)
+        if date_created := data.get("created_at"):
+            if isinstance(date_created, str):
+                parsed = try_parse_datetime_str(date_created)
+                if parsed is None:
+                    msg = "Unable to parse `created_at` legacy ISO string"
+                    raise DeserializationError(message=msg)
+                date_created = parsed
+            if not isinstance(date_created, datetime):
+                msg = "Expected `created_at` field to be a datetime"
+                raise DeserializationError(message=msg)
+            data["created_at"] = date_created.replace(tzinfo=timezone.utc)
+        if date_expires := data.get("expires_at"):
+            if isinstance(date_expires, str):
+                parsed = try_parse_datetime_str(date_expires)
+                if parsed is None:
+                    msg = "Unable to parse `expires_at` legacy ISO string"
+                    raise DeserializationError(message=msg)
+                date_expires = parsed
+            if not isinstance(date_expires, datetime):
+                msg = "Expected `expires_at` field to be a datetime"
+                raise DeserializationError(message=msg)
+            data["expires_at"] = date_expires.replace(tzinfo=timezone.utc)

(Import try_parse_datetime_str from key_value.shared.utils.datetime.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py
around lines 72-92, prepare_load currently only accepts the new nested {"value":
{"object"| "string"}} shape and datetime instances causing legacy records to
raise DeserializationError; update prepare_load to detect legacy shapes: if the
popped value is a dict treat it as the old plain-object payload and set
data["value"] = {"object": that_dict}, if it's a str set data["value"] =
{"string": that_str}; for created_at and expires_at, if the fields are strings
attempt to parse them with try_parse_datetime_str (import from
key_value.shared.utils.datetime) and then replace tzinfo=timezone.utc on the
parsed datetime, otherwise if parsing fails raise DeserializationError; keep
existing checks for datetime instances and normalize all legacy forms into the
canonical new form before returning so load_dict() can proceed as before.

Repository owner deleted a comment from coderabbitai bot Oct 30, 2025
Repository owner deleted a comment from claude bot Oct 30, 2025
Repository owner deleted a comment from claude bot Oct 30, 2025
Repository owner deleted a comment from coderabbitai bot Oct 30, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

📝 Walkthrough

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Refactor ManagedEntry serialization with adapter pattern" accurately and concisely describes the main change in the pull request. It identifies the primary objective (refactoring serialization) and the key architectural approach (adapter pattern). The title is specific enough that a teammate reviewing the history would understand that this PR focuses on introducing a consistent serialization interface across stores and wrappers. The title is clear, avoids vague terminology, and directly reflects the core changes throughout the codebase.
Out of Scope Changes Check ✅ Passed The PR changes are consistently focused on the serialization refactoring objectives. Core changes include introducing the SerializationAdapter framework, updating stores to use adapter-based serialization, refactoring ManagedEntry to support the adapter pattern, and improving wrapper performance with estimate_serialized_size(). Store-specific implementations (MongoDB, Elasticsearch, Redis) are all updated as part of the serialization consolidation effort. ManagedEntry modifications (removing ttl field, adding isoformat properties, adding from_ttl classmethod) are necessary for the adapter-based approach. Test updates throughout the codebase reflect the adapter adoption. All changes are directly aligned with the stated PR objectives of establishing a consistent serialization interface and eliminating duplication across backends.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-165-20251029-0155

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb6d1a and 194bc94.

📒 Files selected for processing (5)
  • key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (3 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (3 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (3 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (6)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (8)
  • ManagedEntry (15-63)
  • dump_to_json (67-72)
  • load_from_json (76-82)
  • verify_dict (86-95)
  • value_as_dict (46-47)
  • value_as_json (41-43)
  • created_at_isoformat (50-51)
  • expires_at_isoformat (54-55)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (2)
  • prepare_load (65-87)
  • prepare_dump (52-62)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (2)
  • prepare_load (120-131)
  • prepare_dump (107-117)
key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (1)
  • get (68-76)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test_quick (3.10, ubuntu-22.04, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-22.04, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
🔇 Additional comments (4)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (1)

1-3: Note: This is an auto-generated file.

This file is auto-generated from the original store.py. Any issues found here should be addressed in the source file, not in this generated version.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (2)

1-3: Note: This is an auto-generated file.

This file is auto-generated. Any changes should be made to the source file that generates this code.


100-100: Adapter initialization is properly configured in the base class.

The verification confirms that _serialization_adapter is correctly initialized in the base class at line 103 of base.py with BasicSerializationAdapter(). Since the auto-generated Valkey store inherits from BaseStore, this initialization is inherited and will be available when the adapter is accessed at lines 100, 115, and 125. No issues found.

key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (1)

112-112: The _serialization_adapter is properly initialized through the inheritance chain.

The base class initializes _serialization_adapter = BasicSerializationAdapter() in its __init__ method (line 97 of base.py), and RocksDBStore.__init__() calls super().__init__(default_collection=default_collection) at line 79, ensuring the adapter is available for all subsequent uses at lines 112, 127, and 150.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py (1)

77-115: Critical: Missing serialization adapter initialization.

The __init__ method does not initialize self._serialization_adapter, but lines 202 and 213 reference it. This will cause an AttributeError at runtime when _get_managed_entry or _put_managed_entry is called.

Based on the PR's adapter pattern and DynamoDB's full-JSON storage approach, add the missing import and initialization:

+from key_value.shared.utils.serialization import BasicSerializationAdapter
+
 from key_value.aio.stores.base import (
     BaseContextManagerStore,
     BaseStore,
 )
         super().__init__(default_collection=default_collection)
+        self._serialization_adapter = BasicSerializationAdapter()
key-value/key-value-aio/src/key_value/aio/stores/simple/store.py (1)

36-43: Remove unused method.

The to_managed_entry() method is no longer called—deserialization now uses the adapter (line 78). This method has become dead code.

Apply this diff to remove the unused method:

-    def to_managed_entry(self) -> ManagedEntry:
-        managed_entry: ManagedEntry = ManagedEntry(
-            value=load_from_json(json_str=self.json_str),
-            expires_at=self.expires_at,
-            created_at=self.created_at,
-        )
-
-        return managed_entry
-
-
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (1)

108-110: Inconsistent adapter usage for deserialization.

Line 109 still uses ManagedEntry.from_json() directly, while the serialization paths (lines 118, 141) correctly use self._serialization_adapter.dump_json(). This inconsistency defeats the purpose of the adapter pattern refactoring. The pipeline failure (reportUnknownVariableType) is likely caused by this direct method call.

Apply this diff to use the adapter consistently:

-        managed_entry_str: str = value.decode("utf-8")
-        managed_entry: ManagedEntry = ManagedEntry.from_json(json_str=managed_entry_str)
-
-        return managed_entry
+        json_str: str = value.decode("utf-8")
+        return self._serialization_adapter.load_json(json_str=json_str)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (1)

100-100: Critical inconsistency: deserialization not using adapter.

Line 100 still uses ManagedEntry.from_json() directly, while line 115 in _get_managed_entries correctly uses self._serialization_adapter.load_json(). This inconsistency within the same store means single-entry and batch operations use different serialization logic, violating the adapter pattern's goal of centralizing serialization behavior.

Apply this diff to use the adapter consistently:

-        decoded_response: str = response.decode("utf-8")
-
-        return ManagedEntry.from_json(json_str=decoded_response)
+        json_str: str = response.decode("utf-8")
+        return self._serialization_adapter.load_json(json_str=json_str)
key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (1)

111-113: Inconsistent adapter usage for deserialization.

Line 112 still uses ManagedEntry.from_json() directly, while the serialization paths (lines 127, 150) correctly use self._serialization_adapter.dump_json(). This inconsistency defeats the purpose of the adapter pattern refactoring. The pipeline failure (reportUnknownVariableType) is likely caused by this direct method call.

Apply this diff to use the adapter consistently:

-        managed_entry_str: str = value.decode("utf-8")
-        managed_entry: ManagedEntry = ManagedEntry.from_json(json_str=managed_entry_str)
-
-        return managed_entry
+        json_str: str = value.decode("utf-8")
+        return self._serialization_adapter.load_json(json_str=json_str)
key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (1)

88-98: Inconsistent serialization: _get_managed_entry bypasses the adapter.

Line 98 uses ManagedEntry.from_json(json_str=decoded_response) directly, while _get_managed_entries (line 113) correctly uses self._serialization_adapter.load_json(). This inconsistency defeats the purpose of the adapter pattern refactoring.

Apply this diff to use the adapter consistently:

-        return ManagedEntry.from_json(json_str=decoded_response)
+        return self._serialization_adapter.load_json(json_str=decoded_response)
♻️ Duplicate comments (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)

98-111: Consider removing redundant dict(value) conversion.

Passing Mapping directly to dump_to_json may be sufficient; converting to dict might be unnecessary overhead. If other parts rely on a concrete dict, keep it and add a brief comment.

-    return len(dump_to_json(obj=dict(value)))
+    return len(dump_to_json(obj=value))  # value is a Mapping; dump_to_json will serialize it
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1)

76-89: Don’t treat empty dict values as missing. Use explicit key presence check.

-        if not (value := data.get("value")):
-            msg = "Value field not found"
-            raise DeserializationError(message=msg)
+        if "value" not in data:
+            msg = "Value field not found"
+            raise DeserializationError(message=msg)
+
+        value = data["value"]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59cbaee and 2cb6d1a.

📒 Files selected for processing (32)
  • key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (4 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py (2 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/keyring/store.py (2 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py (5 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (3 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (2 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/simple/store.py (4 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (2 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/vault/store.py (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/stores/windows_registry/store.py (1 hunks)
  • key-value/key-value-aio/tests/stores/disk/test_disk.py (2 hunks)
  • key-value/key-value-aio/tests/stores/disk/test_multi_disk.py (2 hunks)
  • key-value/key-value-aio/tests/stores/dynamodb/test_dynamodb.py (3 hunks)
  • key-value/key-value-aio/tests/stores/memcached/test_memcached.py (2 hunks)
  • key-value/key-value-aio/tests/stores/rocksdb/test_rocksdb.py (2 hunks)
  • key-value/key-value-aio/tests/stores/valkey/test_valkey.py (2 hunks)
  • key-value/key-value-aio/tests/stores/wrappers/test_limit_size.py (2 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (4 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (3 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/keyring/store.py (2 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/store.py (3 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (2 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/simple/store.py (4 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (2 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/vault/store.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/stores/windows_registry/store.py (1 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py (2 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py (2 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/rocksdb/test_rocksdb.py (2 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/valkey/test_valkey.py (2 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/wrappers/test_limit_size.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (32)
key-value/key-value-aio/tests/stores/rocksdb/test_rocksdb.py (2)
key-value/key-value-sync/tests/code_gen/stores/rocksdb/test_rocksdb.py (3)
  • rocksdb_client (71-72)
  • store (23-29)
  • test_value_stored (74-89)
key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (1)
  • RocksDBStore (20-194)
key-value/key-value-aio/src/key_value/aio/stores/vault/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/vault/store.py (1)
  • _put_managed_entry (105-113)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/vault/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/keyring/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (15-63)
  • ttl (35-38)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • BasicSerializationAdapter (121-133)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/tests/stores/disk/test_multi_disk.py (2)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (1)
  • MultiDiskStore (28-150)
key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py (2)
  • test_value_stored (38-52)
  • store (32-36)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_limit_size.py (1)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • estimate_serialized_size (98-111)
key-value/key-value-aio/tests/stores/valkey/test_valkey.py (2)
key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (1)
  • ValkeyStore (23-155)
key-value/key-value-sync/tests/code_gen/stores/valkey/test_valkey.py (3)
  • valkey_client (88-89)
  • store (76-85)
  • test_value_stored (95-110)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1)
  • dump_json (116-118)
key-value/key-value-aio/tests/stores/dynamodb/test_dynamodb.py (1)
key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py (2)
  • DynamoDBStore (29-251)
  • _connected_client (133-137)
key-value/key-value-sync/tests/code_gen/stores/rocksdb/test_rocksdb.py (3)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (3)
  • put (275-284)
  • get (186-207)
  • ttl (220-229)
key-value/key-value-aio/tests/stores/rocksdb/test_rocksdb.py (3)
  • rocksdb_client (68-69)
  • store (20-26)
  • test_value_stored (71-86)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (1)
  • RocksDBStore (23-185)
key-value/key-value-aio/tests/stores/disk/test_disk.py (1)
key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py (4)
  • disk_cache (34-35)
  • disk_store (22-24)
  • test_value_stored (37-50)
  • store (28-31)
key-value/key-value-aio/tests/stores/memcached/test_memcached.py (2)
key-value/key-value-aio/tests/stores/disk/test_disk.py (2)
  • store (25-28)
  • test_value_stored (34-47)
key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py (1)
  • MemcachedStore (21-133)
key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (1)
  • dump_json (116-118)
key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py (2)
key-value/key-value-aio/tests/stores/disk/test_disk.py (3)
  • disk_cache (31-32)
  • disk_store (19-21)
  • test_value_stored (34-47)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/store.py (2)
  • put (79-81)
  • get (69-77)
key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py (2)
key-value/key-value-aio/tests/stores/disk/test_multi_disk.py (1)
  • test_value_stored (35-54)
key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py (3)
  • test_value_stored (37-50)
  • store (28-31)
  • disk_cache (34-35)
key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • BasicSerializationAdapter (121-133)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/simple/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (15-63)
  • load_from_json (76-82)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • ValueOnlySerializationAdapter (136-148)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/simple/store.py (1)
  • SimpleStoreEntry (23-43)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (6)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (8)
  • ManagedEntry (15-63)
  • dump_to_json (67-72)
  • load_from_json (76-82)
  • verify_dict (86-95)
  • value_as_dict (46-47)
  • value_as_json (41-43)
  • created_at_isoformat (50-51)
  • expires_at_isoformat (54-55)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (1)
  • try_parse_datetime_str (34-41)
key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py (2)
  • prepare_load (120-131)
  • prepare_dump (107-117)
key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py (2)
  • prepare_load (65-87)
  • prepare_dump (52-62)
key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (1)
  • get (68-76)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (2)
  • DeserializationError (14-15)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/type_checking/bear_spray.py (1)
  • bear_enforce (21-23)
key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py (3)
  • now (19-21)
  • now_plus (29-31)
  • seconds_to (24-26)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (1)
  • ttl (220-229)
key-value/key-value-sync/tests/code_gen/stores/valkey/test_valkey.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py (4)
  • BaseStore (59-348)
  • put (275-284)
  • get (186-207)
  • ttl (220-229)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (1)
  • ValkeyStore (25-151)
key-value/key-value-aio/src/key_value/aio/stores/keyring/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (4)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (15-63)
  • ttl (35-38)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • BasicSerializationAdapter (121-133)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (2)
  • _put_managed_entry (128-135)
  • _delete_managed_entry (138-139)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/store.py (2)
  • _put_managed_entry (104-107)
  • _delete_managed_entry (110-113)
key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/windows_registry/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (2)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • BasicSerializationAdapter (121-133)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (1)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/simple/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (2)
  • ManagedEntry (15-63)
  • load_from_json (76-82)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • ValueOnlySerializationAdapter (136-148)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/simple/store.py (1)
  • SimpleStoreEntry (21-39)
key-value/key-value-aio/tests/stores/wrappers/test_limit_size.py (1)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • estimate_serialized_size (98-111)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/windows_registry/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (2)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/windows_registry/store.py (3)
  • _put_managed_entry (94-100)
  • _sanitize_key (65-70)
  • _get_registry_path (72-75)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/store.py (3)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py (3)
  • BasicSerializationAdapter (121-133)
  • load_json (45-49)
  • dump_json (116-118)
key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (3)
  • MemoryCacheEntry (26-31)
  • get (68-76)
  • put (78-80)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)
  • ManagedEntry (15-63)
🪛 GitHub Actions: Run Tests
key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py

[error] 109-109: pyright: Type of 'managed_entry' is unknown (reportUnknownVariableType).

key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py

[error] 112-112: Step 'uv run basedpyright .' failed. Pyright error: Type of "managed_entry" is unknown (reportUnknownVariableType).

key-value/key-value-shared/src/key_value/shared/utils/serialization.py

[error] 148-148: pyright: Type 'Any | None' is not assignable to return type 'dict[str, Any]'. (reportReturnType)

🪛 GitHub Check: static_analysis (key-value/key-value-shared)
key-value/key-value-shared/src/key_value/shared/utils/serialization.py

[failure] 148-148:
Type "Any | None" is not assignable to return type "dict[str, Any]"
  Type "Any | None" is not assignable to type "dict[str, Any]"
    "None" is not assignable to "dict[str, Any]" (reportReturnType)

🔇 Additional comments (54)
key-value/key-value-aio/tests/stores/rocksdb/test_rocksdb.py (3)

1-1: LGTM: Imports support low-level serialization validation.

The new imports are appropriately scoped to the test's objective: validating the raw serialization format using snapshot testing with flexible datetime matching.

Also applies to: 7-9


67-69: Appropriate fixture for adapter format validation.

Exposing the internal _db client allows the test to validate the raw serialization format introduced by the adapter pattern. The pyright: ignore comment correctly acknowledges intentional private access for testing purposes.


71-86: LGTM: Effective validation of adapter serialization format.

The test appropriately validates the new adapter-based serialization format for both TTL and non-TTL cases. The structure mirrors the sync version for consistency, and the use of snapshot testing with IsDatetime provides flexible yet rigorous format validation.

key-value/key-value-aio/tests/stores/disk/test_multi_disk.py (2)

1-1: LGTM: Imports are appropriate and well-organized.

The new imports support the serialization format testing introduced in this PR. The TYPE_CHECKING guard for the Cache type import is a good practice to avoid runtime import overhead.

Also applies to: 5-5, 8-9, 15-17


35-54: Excellent test for validating serialization format.

This test appropriately validates the raw JSON structure stored by the BasicSerializationAdapter used in MultiDiskStore. Key strengths:

  • Tests both scenarios (with and without TTL) to ensure expires_at is correctly included when needed.
  • Uses snapshot testing to catch unintended serialization format changes, which is valuable given this PR's focus on refactoring serialization.
  • Direct access to the underlying disk cache (line 37) is intentional and appropriate here—it allows verification of the actual persisted format rather than just the public API behavior.

Note: The dictionary key ordering in the second snapshot (line 53: "age" before "name") is consistent with the sync version and will be caught by snapshot updates if the serialization behavior changes.

key-value/key-value-aio/src/key_value/aio/stores/simple/store.py (4)

1-1: LGTM! Imports support the adapter pattern.

The sys import is used for sys.maxsize and ValueOnlySerializationAdapter implements the new serialization strategy.

Also applies to: 8-8


53-63: Verify the handling of max_entries=0.

Line 63 uses max_entries or sys.maxsize, which treats 0 as falsy and converts it to sys.maxsize (unlimited). Confirm this is the intended behavior, as passing 0 might reasonably mean "no entries allowed" rather than "unlimited entries."

Consider using explicit None-checking for clarity:

-        self.max_entries = max_entries or sys.maxsize
+        self.max_entries = max_entries if max_entries is not None else sys.maxsize

78-81: LGTM! Adapter-based deserialization with separate metadata handling.

The pattern correctly uses ValueOnlySerializationAdapter to deserialize the value, then manually restores metadata from the store entry. This separation is by design since the adapter only handles the value field.


91-93: LGTM! Adapter-based serialization with metadata preservation.

The code correctly uses the adapter to serialize the value while preserving metadata fields in the store entry.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/vault/store.py (3)

108-108: Serialization adapter usage is correct.

The call to self._serialization_adapter.dump_json(entry=managed_entry) correctly uses the new adapter pattern. The storage pattern (full JSON string in Vault's value field) aligns with FullJsonAdapter behavior mentioned in the PR summary.


1-3: Confirm changes were made to the original async source file.

This file is auto-generated from an async source file. Ensure that the corresponding changes were properly made to the original async store.py file in the aio directory, and that this sync version was regenerated using build_sync_library.py.

Run the following script to verify the original async file has matching changes:


102-102: Verify adapter initialization and type in BaseStore.

The code now delegates to self._serialization_adapter.load_json(), but _serialization_adapter is not initialized in this file. Ensure that:

  1. BaseStore properly initializes _serialization_adapter before any store operations
  2. VaultStore uses the correct adapter (likely FullJsonAdapter since it stores full JSON strings in the value field)

Run the following script to verify the adapter initialization:

key-value/key-value-aio/tests/stores/wrappers/test_limit_size.py (1)

3-3: LGTM: Efficient size estimation without ManagedEntry overhead.

The change to use estimate_serialized_size avoids the overhead of creating a full ManagedEntry object just to calculate size. This aligns with the PR's objective to improve wrapper efficiency.

Also applies to: 142-142

key-value/key-value-aio/tests/stores/disk/test_disk.py (2)

1-1: LGTM: Appropriate test dependencies added.

The imports support the new test that validates internal storage format using snapshot assertions.

Also applies to: 6-8


30-47: LGTM: Well-structured test validates adapter serialization.

The test appropriately validates that the serialization adapter correctly persists created_at, value, and expires_at fields to the underlying disk cache. Using snapshot assertions with IsDatetime is a good approach for validating the structure while allowing timestamp flexibility.

key-value/key-value-aio/tests/stores/dynamodb/test_dynamodb.py (4)

2-2: LGTM: Required imports added for DynamoDB testing.

The imports support the new test infrastructure for validating DynamoDB storage format.

Also applies to: 4-4, 7-8, 10-11


57-59: LGTM: Helper function extracts value from DynamoDB response.

The helper appropriately navigates the DynamoDB response structure to extract the stored JSON value.


111-113: LGTM: Fixture exposes DynamoDB client for testing.

The fixture appropriately exposes the internal DynamoDB client to enable direct storage validation in tests.


119-136: LGTM: Test validates adapter serialization to DynamoDB.

The test properly validates that the serialization adapter correctly persists entries to DynamoDB, including metadata fields. The snapshot-based assertions with IsDatetime are appropriate for validating structure while handling timestamp variability.

key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py (5)

2-2: LGTM: Required imports added for adapter pattern.

The imports provide timezone support and the serialization adapter needed for the refactored implementation.

Also applies to: 6-7


103-103: LGTM: Serialization adapter properly initialized.

The initialization of BasicSerializationAdapter addresses the previous critical issue where _serialization_adapter was referenced but never initialized.


115-125: LGTM: Adapter-based deserialization with timezone-aware datetime.

The method correctly uses the serialization adapter for deserialization and properly creates timezone-aware expires_at using tz=timezone.utc, addressing the previous issue with naive datetimes.


127-135: LGTM: Adapter-based serialization.

The method correctly uses the serialization adapter for serialization, consistent with the refactored pattern.


137-139: LGTM: Simplified deletion logic.

The deletion now correctly uses the key directly instead of compound_key, consistent with the updated storage approach.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/simple/store.py (3)

63-63: LGTM: Serialization adapter initialized.

The ValueOnlySerializationAdapter is appropriate for SimpleStore, which stores only the value without metadata in the JSON.


74-77: LGTM: Adapter deserialization with metadata restoration.

The deserialization correctly uses the adapter for the JSON value and then restores expires_at and created_at from the SimpleStoreEntry, which stores metadata separately.


86-90: LGTM: Adapter serialization with metadata preservation.

The serialization correctly uses the adapter to dump only the value to JSON while preserving expires_at and created_at in the SimpleStoreEntry separately.

key-value/key-value-sync/tests/code_gen/stores/wrappers/test_limit_size.py (1)

6-6: LGTM: Efficient size estimation (code-generated).

The code-generated change correctly mirrors the async version, using estimate_serialized_size to avoid unnecessary ManagedEntry instantiation.

Also applies to: 145-145

key-value/key-value-aio/src/key_value/aio/stores/vault/store.py (2)

105-105: LGTM: Adapter-based deserialization.

The deserialization correctly uses self._serialization_adapter.load_json instead of the deprecated ManagedEntry.from_json, consistent with the refactored adapter pattern.


111-111: LGTM: Adapter-based serialization.

The serialization correctly uses self._serialization_adapter.dump_json instead of managed_entry.to_json(), consistent with the refactored adapter pattern.

key-value/key-value-aio/src/key_value/aio/stores/windows_registry/store.py (2)

91-91: LGTM: Adapter-based deserialization.

The deserialization correctly uses self._serialization_adapter.load_json instead of the deprecated ManagedEntry.from_json, consistent with the refactored adapter pattern.


98-98: LGTM: Adapter-based serialization.

The serialization correctly uses self._serialization_adapter.dump_json instead of managed_entry.to_json(), consistent with the refactored adapter pattern.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/store.py (2)

118-118: LGTM: Serialization correctly uses adapter.

The serialization path correctly delegates to the adapter, consistent with the PR's refactoring goals.


141-141: LGTM: Batch serialization correctly uses adapter.

The batch serialization path correctly delegates to the adapter, maintaining consistency with single-entry operations.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/windows_registry/store.py (1)

92-92: LGTM: Consistent adapter usage for both serialization directions.

Both deserialization (line 92) and serialization (line 99) correctly delegate to the adapter, providing a consistent interface as intended by the refactoring.

Also applies to: 99-99

key-value/key-value-sync/tests/code_gen/stores/rocksdb/test_rocksdb.py (1)

70-89: LGTM: Test validates stored JSON format through direct client access.

The test appropriately verifies the serialized format by accessing the underlying RocksDB client. Using store._db with a pyright ignore directive is acceptable in test context for validating storage format, including metadata fields (created_at, expires_at). The snapshot assertions ensure the stored structure remains consistent.

key-value/key-value-aio/tests/stores/memcached/test_memcached.py (1)

71-94: LGTM: Test validates stored JSON format with TTL handling.

The test follows the same pattern as other store tests, appropriately accessing store._client (with pyright ignore) to verify the serialized format. The test validates both non-TTL and TTL scenarios, ensuring metadata fields are correctly included in the stored JSON.

key-value/key-value-aio/src/key_value/aio/stores/keyring/store.py (1)

82-82: LGTM: Consistent adapter usage for keyring serialization.

Both deserialization (line 82) and serialization (line 91) correctly use the adapter, ensuring consistent behavior across the keyring store.

Also applies to: 91-91

key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/store.py (1)

115-115: Note: Partial adapter adoption creates inconsistency.

While these lines correctly use the adapter, line 100 in _get_managed_entry still uses the old ManagedEntry.from_json() method. This inconsistency should be resolved to ensure uniform serialization behavior across all operations.

Also applies to: 125-125

key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py (1)

127-127: LGTM: Serialization paths correctly use adapter.

Both single-entry and batch serialization correctly delegate to the adapter. However, note that the deserialization path (line 112) still needs to be updated for consistency.

Also applies to: 150-150

key-value/key-value-aio/tests/stores/valkey/test_valkey.py (1)

89-112: LGTM: Test validates stored format via direct Valkey client access.

The test follows the established pattern of accessing the underlying client (store._connected_client with pyright ignore) to validate serialized format. The test appropriately verifies both non-TTL and TTL scenarios with snapshot assertions.

key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py (1)

33-50: LGTM! Test validates serialized format correctly.

The test properly verifies that:

  • Stored values include created_at timestamp
  • Values with TTL include expires_at timestamp
  • The structure matches expected JSON format via snapshots

The use of IsDatetime for dynamic timestamp fields is appropriate.

key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py (1)

71-71: Serialization updated correctly.

The migration from ManagedEntry.from_json() to self._serialization_adapter.load_json() is correct and consistent with the adapter pattern.

key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py (1)

38-52: LGTM! Test validates multi-disk serialization correctly.

The test properly verifies the JSON structure stored in the disk cache for MultiDiskStore, consistent with the single DiskStore test pattern.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/multi_store.py (3)

106-108: Correct initialization order.

The adapter is properly initialized before calling super().__init__(), ensuring any parent class setup that might depend on the adapter will work correctly.


118-126: Serialization adapter integration looks good.

The code correctly:

  • Uses adapter.load_json() for deserialization
  • Populates expires_at with timezone-aware datetime (UTC) from expire_epoch
  • Addresses the previous review comment about timezone awareness

132-132: Adapter usage is correct.

The serialization path properly delegates to self._serialization_adapter.dump_json().

key-value/key-value-sync/tests/code_gen/stores/valkey/test_valkey.py (1)

87-110: LGTM! Test validates Valkey serialization format.

The test correctly:

  • Exposes the underlying Valkey client for direct storage inspection
  • Validates the JSON structure includes created_at and value
  • Verifies TTL-based entries include expires_at
  • Uses snapshot testing appropriately
key-value/key-value-aio/src/key_value/aio/stores/memory/store.py (3)

54-66: Verify the default behavior change is intentional.

The max_entries parameter default changed from 10000 to None (unlimited). This is a breaking change in behavior - previously collections were bounded by default, now they're unbounded.

Confirm this change aligns with the PR objectives. If bounded collections are desired by default, consider:

-    def __init__(self, max_entries: int | None = None):
+    def __init__(self, max_entries: int | None = 10000):

66-80: Adapter initialization and usage is correct.

The adapter is properly initialized in __init__() and used consistently in get() and put() methods.


100-122: Correct initialization order; verify default change.

The adapter is properly initialized before super().__init__() (line 122), ensuring correct initialization order. However, the default for max_entries_per_collection changed from 10000 to None (unlimited), which is a behavior change.

Confirm this default change is intentional and document it in the PR description if it's a breaking change.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/keyring/store.py (1)

72-72: Verify that _serialization_adapter is initialized before use.

The code uses self._serialization_adapter.load_json() but the adapter initialization is not visible in this file. Ensure that either BaseStore.__init__() or KeyringStore.__init__() initializes _serialization_adapter before any _get_managed_entry calls.

Run this script to verify the adapter is properly initialized:

key-value/key-value-aio/src/key_value/aio/stores/valkey/store.py (1)

113-113: Verify that _serialization_adapter is initialized.

The adapter is used here but its initialization is not visible in the provided code. Ensure BaseStore or ValkeyStore.__init__() properly initializes the adapter.

key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/store.py (1)

75-82: LGTM on adapter-based (de)serialization path.

Using BasicSerializationAdapter.load_json/dump_json here keeps MemoryStore consistent with other backends. No issues spotted.

Comment on lines +48 to +52
super().__init__(default_collection=default_collection)

self._client = client or Client(host=host, port=port)

super().__init__(default_collection=default_collection)
self._serialization_adapter = BasicSerializationAdapter(value_format="dict")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical initialization order issue.

The code calls super().__init__(default_collection=default_collection) on line 48 before initializing self._serialization_adapter on line 52. If the parent class initialization triggers any methods that depend on the adapter (e.g., seed data processing, setup hooks), this will cause an AttributeError.

Apply this diff to initialize the adapter before calling the parent constructor:

-        super().__init__(default_collection=default_collection)
-
         self._client = client or Client(host=host, port=port)
 
         self._serialization_adapter = BasicSerializationAdapter(value_format="dict")
+
+        super().__init__(default_collection=default_collection)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
super().__init__(default_collection=default_collection)
self._client = client or Client(host=host, port=port)
super().__init__(default_collection=default_collection)
self._serialization_adapter = BasicSerializationAdapter(value_format="dict")
self._client = client or Client(host=host, port=port)
self._serialization_adapter = BasicSerializationAdapter(value_format="dict")
super().__init__(default_collection=default_collection)
🤖 Prompt for AI Agents
In key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py around
lines 48 to 52, the adapter is initialized after calling super().__init__, which
can cause AttributeError if the parent constructor calls methods that use the
adapter; move the line that sets self._serialization_adapter =
BasicSerializationAdapter(value_format="dict") to before the
super().__init__(default_collection=default_collection) call so the adapter is
ready during base-class initialization.

from datetime import datetime

from key_value.shared.utils.compound import compound_key, get_collections_from_compound_keys, get_keys_from_compound_keys
from key_value.shared.utils.managed_entry import ManagedEntry, load_from_json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused import after cleaning up dead code.

Once SimpleStoreEntry.to_managed_entry() is removed (as suggested above), the load_from_json import will no longer be needed.

Apply this diff:

-from key_value.shared.utils.managed_entry import ManagedEntry, load_from_json
+from key_value.shared.utils.managed_entry import ManagedEntry
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from key_value.shared.utils.managed_entry import ManagedEntry, load_from_json
from key_value.shared.utils.managed_entry import ManagedEntry
🤖 Prompt for AI Agents
In key-value/key-value-aio/src/key_value/aio/stores/simple/store.py around line
7, the import load_from_json from key_value.shared.utils.managed_entry is no
longer used after removing SimpleStoreEntry.to_managed_entry(); remove
load_from_json from the import list (leave ManagedEntry if still used) or delete
the entire import line if neither symbol is used to eliminate the unused import.

self._data = defaultdict[str, SimpleStoreEntry]()

super().__init__(default_collection=default_collection)
self._serialization_adapter = ValueOnlySerializationAdapter()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add type annotation for _serialization_adapter.

The instance variable should have a type annotation for consistency and better type checking.

Add the annotation to the class attributes section:

 class SimpleStore(BaseEnumerateCollectionsStore, BaseEnumerateKeysStore, BaseDestroyStore, BaseStore):
     """Simple managed dictionary-based key-value store for testing and development."""
 
     max_entries: int
 
     _data: dict[str, SimpleStoreEntry]
+    _serialization_adapter: ValueOnlySerializationAdapter

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In key-value/key-value-aio/src/key_value/aio/stores/simple/store.py around line
67, add a type annotation for the instance variable _serialization_adapter in
the class attributes section (e.g. declare _serialization_adapter:
ValueOnlySerializationAdapter) so the attribute has an explicit type; if
ValueOnlySerializationAdapter is not already imported at the top of the file,
add the import for it.

Comment on lines 136 to 148
class ValueOnlySerializationAdapter(SerializationAdapter):
"""Serialization adapter that only serializes the value."""

def __init__(self, *, value_format: Literal["string", "dict"] | None = "dict") -> None:
super().__init__(date_format=None, value_format=value_format)

def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
return {
"value": data,
}

def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
return data.get("value")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix return type and behavior of ValueOnlySerializationAdapter; resolve pyright error.

prepare_dump must return a dict, and dump_json should emit only the value. This also fixes the pipeline failure at Line 148.

 class ValueOnlySerializationAdapter(SerializationAdapter):
@@
-    def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
+    def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
         return {
             "value": data,
         }
 
-    def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
-        return data.get("value")
+    def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
+        # Ensure dict return type for static analysis; dump_json is overridden to emit only the value.
+        return {"value": data["value"]}
+
+    def dump_dict(self, entry: ManagedEntry, exclude_none: bool = True) -> dict[str, Any]:
+        # Keep contract but only carry the value forward.
+        return {"value": entry.value_as_dict if self._value_format == "dict" else entry.value_as_json}
+
+    def dump_json(self, entry: ManagedEntry, exclude_none: bool = True) -> str:
+        # Value-only JSON emission.
+        if self._value_format == "dict":
+            return dump_to_json(obj=entry.value_as_dict)
+        return entry.value_as_json

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Run Tests

[error] 148-148: pyright: Type 'Any | None' is not assignable to return type 'dict[str, Any]'. (reportReturnType)

🪛 GitHub Check: static_analysis (key-value/key-value-shared)

[failure] 148-148:
Type "Any | None" is not assignable to return type "dict[str, Any]"
  Type "Any | None" is not assignable to type "dict[str, Any]"
    "None" is not assignable to "dict[str, Any]" (reportReturnType)

🤖 Prompt for AI Agents
key-value/key-value-shared/src/key_value/shared/utils/serialization.py lines
136-148: prepare_dump currently returns the raw value (possibly non-dict) which
violates the expected dict[str, Any] return type and causes pyright/pipeline
failure; change prepare_dump to always return a dict like {"value":
data.get("value")} (ensuring the value key exists and respects typing), and
add/override a dump_json method that serializes and returns only the inner value
(not the wrapper dict) so external JSON output remains the value-only form; keep
prepare_load as-is (it should wrap incoming value into {"value": ...}) and
ensure function and return type annotations reflect dict[str, Any] for
prepare_dump and appropriate str/bytes return for dump_json.

Comment on lines +111 to +118
self.max_entries_per_collection = max_entries_per_collection or sys.maxsize

self._cache = {}

self._stable_api = True

self._serialization_adapter = BasicSerializationAdapter()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Remove or use unused store-level adapter.

MemoryStore sets self._serialization_adapter but doesn't use it in this module. Either pass it into MemoryCollection (as above) or drop it.

🤖 Prompt for AI Agents
In key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/store.py
around lines 111 to 118, the MemoryStore sets self._serialization_adapter but
never uses it; either remove the unused attribute or wire it into
MemoryCollection instances. Fix by passing self._serialization_adapter to
MemoryCollection constructors wherever collections are created in this module
(or to the Collection factory call used), or if collections already accept an
adapter ensure the adapter parameter is forwarded; alternatively delete the
self._serialization_adapter assignment and any related unused imports to
eliminate dead code.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
22.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review ManagedEntry

2 participants