gpdatacached 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # src/gpdatacached/__init__.py
2
+
3
+ # Software library version (SemVer), tracks feature releases and bugfixes.
4
+ # Independent of GPDC_PROTOCOL_VERSION in domain.py.
5
+ __version__ = "0.1.0"
6
+
7
+ from .codec import GPDCCodec, GPDCContainerCodec, GPDCScalarCodec
8
+ from .config import GPDCDomainConfig, GPDCNamespaceConfig
9
+ from .exceptions import (
10
+ CanonicalObjectReferencedError,
11
+ CyclicReferenceError,
12
+ GPDCLockConfigInconsistentError,
13
+ LockAcquisitionTimeout,
14
+ ObjectGoneError,
15
+ )
16
+ from .domain import GPDCDomain, reset_domain_config
17
+ from .keys import make_uri, uri_to_redis_key, validate_object_name
18
+ from .namespace import GPDCNamespace
19
+ from .types import GPDCTypeRegistry
20
+ from .object import GPDCDataObject
21
+ from .scalar import GPDCScalarObject
22
+ from .container import GPDCContainerObject
23
+ from .builtins.bool_object import BoolObject
24
+ from .builtins.datetime_object import DateObject, DateTimeObject, TimeObject
25
+ from .builtins.dict_object import DictObject
26
+ from .builtins.float_object import FloatObject
27
+ from .builtins.int_object import IntObject
28
+ from .builtins.list_object import ListObject
29
+ from .builtins.none_object import NoneObject
30
+ from .builtins.set_object import SetObject
31
+ from .builtins.str_object import StrObject
32
+ from .builtins.tuple_object import TupleObject
33
+
34
+ from .builtins.bool_object import BoolCodec as BoolCodec # noqa: F401
35
+ from .builtins.datetime_object import DateCodec as DateCodec, DateTimeCodec as DateTimeCodec, TimeCodec as TimeCodec # noqa: F401
36
+ from .builtins.dict_object import DictCodec as DictCodec # noqa: F401
37
+ from .builtins.float_object import FloatCodec as FloatCodec # noqa: F401
38
+ from .builtins.int_object import IntCodec as IntCodec # noqa: F401
39
+ from .builtins.list_object import ListCodec as ListCodec # noqa: F401
40
+ from .builtins.none_object import NoneCodec as NoneCodec # noqa: F401
41
+ from .builtins.set_object import SetCodec as SetCodec # noqa: F401
42
+ from .builtins.str_object import StrCodec as StrCodec # noqa: F401
43
+ from .builtins.tuple_object import TupleCodec as TupleCodec # noqa: F401
44
+
45
+ __all__ = [
46
+ "__version__",
47
+ "CanonicalObjectReferencedError",
48
+ "CyclicReferenceError",
49
+ "ObjectGoneError",
50
+ "GPDCLockConfigInconsistentError",
51
+ "LockAcquisitionTimeout",
52
+ "GPDCDomain",
53
+ "GPDCNamespace",
54
+ "GPDCDomainConfig",
55
+ "GPDCNamespaceConfig",
56
+ "GPDCDataObject",
57
+ "GPDCScalarObject",
58
+ "IntObject",
59
+ "FloatObject",
60
+ "StrObject",
61
+ "BoolObject",
62
+ "DateTimeObject",
63
+ "DateObject",
64
+ "TimeObject",
65
+ "NoneObject",
66
+ "ListObject",
67
+ "DictObject",
68
+ "SetObject",
69
+ "TupleObject",
70
+ "GPDCContainerObject",
71
+ "GPDCCodec",
72
+ "GPDCContainerCodec",
73
+ "GPDCScalarCodec",
74
+ "GPDCTypeRegistry",
75
+ "validate_object_name",
76
+ "make_uri",
77
+ "uri_to_redis_key",
78
+ "reset_domain_config",
79
+ ]
@@ -0,0 +1,3 @@
1
+ from .registry import _register_builtin_types
2
+
3
+ _register_builtin_types()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from ..codec import GPDCScalarCodec
4
+ from ..scalar import GPDCScalarObject
5
+
6
+
7
+ class BoolCodec(GPDCScalarCodec):
8
+ @classmethod
9
+ def encode_value(cls, value: object) -> str:
10
+ if not isinstance(value, bool):
11
+ raise TypeError(f"BoolCodec cannot encode {type(value).__name__!r}")
12
+ return f"bool:{value}"
13
+
14
+ @classmethod
15
+ def decode_value(cls, encoded: str) -> object:
16
+ type_name, raw = cls._parse_encoded(encoded)
17
+ if type_name != "bool":
18
+ raise ValueError(f"BoolCodec cannot decode type {type_name!r}")
19
+ if raw not in ("True", "False"):
20
+ raise ValueError(f"Invalid scalar value for type 'bool': {raw!r}")
21
+ return raw == "True"
22
+
23
+
24
+ class BoolObject(GPDCScalarObject):
25
+ TYPE = "scalar:bool"
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+
5
+ from ..codec import GPDCScalarCodec
6
+ from ..scalar import GPDCScalarObject
7
+
8
+
9
+ class DateTimeCodec(GPDCScalarCodec):
10
+ @classmethod
11
+ def encode_value(cls, value: object) -> str:
12
+ if not isinstance(value, datetime.datetime):
13
+ raise TypeError(f"DateTimeCodec cannot encode {type(value).__name__!r}")
14
+ return f"datetime:{value.isoformat()}"
15
+
16
+ @classmethod
17
+ def decode_value(cls, encoded: str) -> object:
18
+ type_name, raw = cls._parse_encoded(encoded)
19
+ if type_name != "datetime":
20
+ raise ValueError(f"DateTimeCodec cannot decode type {type_name!r}")
21
+ try:
22
+ return datetime.datetime.fromisoformat(raw)
23
+ except (ValueError, TypeError):
24
+ raise ValueError(f"Invalid scalar value for type 'datetime': {raw!r}")
25
+
26
+
27
+ class DateCodec(GPDCScalarCodec):
28
+ @classmethod
29
+ def encode_value(cls, value: object) -> str:
30
+ if isinstance(value, datetime.datetime) or not isinstance(value, datetime.date):
31
+ raise TypeError(f"DateCodec cannot encode {type(value).__name__!r}")
32
+ return f"date:{value.isoformat()}"
33
+
34
+ @classmethod
35
+ def decode_value(cls, encoded: str) -> object:
36
+ type_name, raw = cls._parse_encoded(encoded)
37
+ if type_name != "date":
38
+ raise ValueError(f"DateCodec cannot decode type {type_name!r}")
39
+ try:
40
+ return datetime.date.fromisoformat(raw)
41
+ except (ValueError, TypeError):
42
+ raise ValueError(f"Invalid scalar value for type 'date': {raw!r}")
43
+
44
+
45
+ class TimeCodec(GPDCScalarCodec):
46
+ @classmethod
47
+ def encode_value(cls, value: object) -> str:
48
+ if not isinstance(value, datetime.time):
49
+ raise TypeError(f"TimeCodec cannot encode {type(value).__name__!r}")
50
+ return f"time:{value.isoformat()}"
51
+
52
+ @classmethod
53
+ def decode_value(cls, encoded: str) -> object:
54
+ type_name, raw = cls._parse_encoded(encoded)
55
+ if type_name != "time":
56
+ raise ValueError(f"TimeCodec cannot decode type {type_name!r}")
57
+ try:
58
+ return datetime.time.fromisoformat(raw)
59
+ except (ValueError, TypeError):
60
+ raise ValueError(f"Invalid scalar value for type 'time': {raw!r}")
61
+
62
+
63
+ class DateTimeObject(GPDCScalarObject):
64
+ TYPE = "scalar:datetime"
65
+
66
+
67
+ class DateObject(GPDCScalarObject):
68
+ TYPE = "scalar:date"
69
+
70
+
71
+ class TimeObject(GPDCScalarObject):
72
+ TYPE = "scalar:time"
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+
5
+ from ..codec import GPDCContainerCodec
6
+ from ..container import GPDCContainerObject
7
+ from ..meta import ObjectMeta
8
+ from ..lifecycle import policy_from_meta
9
+ from ..types import decode_element, decode_key, encode_element, encode_key
10
+
11
+
12
+ class DictCodec(GPDCContainerCodec):
13
+ """Codec for Python dict values stored as Redis Hashes."""
14
+
15
+ @classmethod
16
+ def create_raw(cls, namespace, raw_key: str, value: dict, owner_name: str, cache_mode: str, ttl: int) -> None:
17
+ from ..lifecycle import policy_from_defaults
18
+ from ..types import encode_element, encode_key
19
+
20
+ if not value:
21
+ return
22
+ policy = policy_from_defaults(cache_mode, ttl)
23
+ mapping = {}
24
+ for k, v in value.items():
25
+ encoded_key = encode_key(k)
26
+ mapping[encoded_key] = encode_element(v, namespace, owner_name, policy)
27
+ namespace._redis().hset(raw_key, mapping=mapping)
28
+
29
+ @classmethod
30
+ def read_raw(cls, namespace, raw_key: str) -> dict:
31
+ from ..types import decode_element, decode_key
32
+
33
+ data = namespace._redis().hgetall(raw_key)
34
+ if not data:
35
+ return {}
36
+ result = {}
37
+ for encoded_key, encoded_val in data.items():
38
+ key = decode_key(encoded_key)
39
+ result[key] = decode_element(encoded_val, namespace)
40
+ return result
41
+
42
+ @classmethod
43
+ def delete_raw(cls, namespace, raw_key: str) -> None:
44
+ namespace._redis().delete(raw_key)
45
+
46
+
47
+ class DictObject(GPDCContainerObject, collections.abc.MutableMapping):
48
+ """Redis-backed dict object implementing MutableMapping."""
49
+
50
+ TYPE = "collection:dict"
51
+
52
+ @property
53
+ def type(self) -> str:
54
+ return self.TYPE
55
+
56
+ @property
57
+ def value(self) -> dict:
58
+ return DictCodec.read_raw(self._namespace, self.raw_key)
59
+
60
+ @value.setter
61
+ def value(self, new_value: dict) -> None:
62
+ self._reject_value_replacement()
63
+
64
+ def __len__(self) -> int:
65
+ return self._redis().hlen(self.raw_key)
66
+
67
+ def __getitem__(self, key: object) -> object:
68
+ encoded_key = encode_key(key)
69
+ encoded_val = self._redis().hget(self.raw_key, encoded_key)
70
+ if encoded_val is None:
71
+ raise KeyError(key)
72
+ return decode_element(encoded_val, self._namespace)
73
+
74
+ def __setitem__(self, key: object, value: object) -> None:
75
+ encoded_key = encode_key(key)
76
+ r = self._redis()
77
+ old_encoded = r.hget(self.raw_key, encoded_key)
78
+ meta = ObjectMeta.load(r, self._canonical_redis_key())
79
+ policy = policy_from_meta(meta) if meta else None
80
+ encoded_val = encode_element(value, self._namespace, self._data_owner_name(), policy)
81
+ r.hset(self.raw_key, encoded_key, encoded_val)
82
+ self._release_element_ref(old_encoded)
83
+ self._touch()
84
+
85
+ def __delitem__(self, key: object) -> None:
86
+ encoded_key = encode_key(key)
87
+ r = self._redis()
88
+ old_encoded = r.hget(self.raw_key, encoded_key)
89
+ if old_encoded is None:
90
+ raise KeyError(key)
91
+ r.hdel(self.raw_key, encoded_key)
92
+ self._release_element_ref(old_encoded)
93
+ self._touch()
94
+
95
+ def __contains__(self, key: object) -> bool:
96
+ encoded_key = encode_key(key)
97
+ return self._redis().hexists(self.raw_key, encoded_key)
98
+
99
+ def __iter__(self):
100
+ for encoded_key in self._redis().hkeys(self.raw_key):
101
+ yield decode_key(encoded_key)
102
+
103
+ def keys(self) -> collections.abc.KeysView:
104
+ """Return a frozen *KeysView* snapshot.
105
+
106
+ Only keys are fetched from Redis (``HKEYS``); values are not decoded.
107
+ The view is backed by a temporary ``dict`` so mutations to the
108
+ ``DictObject`` after this call are **not** reflected.
109
+ """
110
+ decoded = [decode_key(k) for k in self._redis().hkeys(self.raw_key)]
111
+ return dict.fromkeys(decoded).keys()
112
+
113
+ def values(self) -> collections.abc.ValuesView:
114
+ """Return a frozen *ValuesView* snapshot.
115
+
116
+ All key-value pairs are fetched from Redis (``HGETALL``) and decoded.
117
+ The view is backed by a temporary ``dict`` so mutations to the
118
+ ``DictObject`` after this call are **not** reflected.
119
+ """
120
+ return self.value.values()
121
+
122
+ def items(self) -> collections.abc.ItemsView:
123
+ """Return a frozen *ItemsView* snapshot.
124
+
125
+ All key-value pairs are fetched from Redis (``HGETALL``) and decoded.
126
+ The view is backed by a temporary ``dict`` so mutations to the
127
+ ``DictObject`` after this call are **not** reflected.
128
+ """
129
+ return self.value.items()
130
+
131
+ def get(self, key, default=None):
132
+ try:
133
+ return self[key]
134
+ except KeyError:
135
+ return default
136
+
137
+ _SENTINEL = object()
138
+
139
+ def pop(self, key, default=_SENTINEL):
140
+ encoded_key = encode_key(key)
141
+ r = self._redis()
142
+ encoded_val = r.hget(self.raw_key, encoded_key)
143
+ if encoded_val is None:
144
+ if default is DictObject._SENTINEL:
145
+ raise KeyError(key)
146
+ return default
147
+ value = self._decode_and_copy_value(encoded_val)
148
+ del self[key]
149
+ return value
150
+
151
+ def setdefault(self, key, default=None):
152
+ if key in self:
153
+ return self[key]
154
+ self[key] = default
155
+ return self[key]
156
+
157
+ def update(self, other=None, **kwargs):
158
+ from ..types import encode_session
159
+
160
+ pairs = []
161
+ if other is not None:
162
+ if hasattr(other, "items"):
163
+ pairs = list(other.items())
164
+ else:
165
+ pairs = list(other)
166
+ pairs.extend(kwargs.items())
167
+ if not pairs:
168
+ return
169
+ r = self._redis()
170
+ owner_name = self._data_owner_name()
171
+ meta = ObjectMeta.load(r, self._canonical_redis_key())
172
+ policy = policy_from_meta(meta) if meta else None
173
+ dirty = False
174
+ try:
175
+ with encode_session(self._namespace):
176
+ for k, v in pairs:
177
+ encoded_key = encode_key(k)
178
+ old_encoded = r.hget(self.raw_key, encoded_key)
179
+ encoded_val = encode_element(v, self._namespace, owner_name, policy)
180
+ r.hset(self.raw_key, encoded_key, encoded_val)
181
+ self._release_element_ref(old_encoded)
182
+ dirty = True
183
+ finally:
184
+ if dirty:
185
+ self._touch()
186
+
187
+ def clear(self):
188
+ encoded_vals = self._redis().hvals(self.raw_key)
189
+ self._redis().delete(self.raw_key)
190
+ for encoded_val in encoded_vals:
191
+ self._release_element_ref(encoded_val)
192
+ self._touch()
193
+
194
+ def deepcopy(self):
195
+ return {k: v.deepcopy() if hasattr(v, "deepcopy") else v for k, v in self.value.items()}
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from ..codec import GPDCScalarCodec
4
+ from ..scalar import GPDCScalarObject
5
+
6
+
7
+ class FloatCodec(GPDCScalarCodec):
8
+ @classmethod
9
+ def encode_value(cls, value: object) -> str:
10
+ if not isinstance(value, float):
11
+ raise TypeError(f"FloatCodec cannot encode {type(value).__name__!r}")
12
+ return f"float:{value}"
13
+
14
+ @classmethod
15
+ def decode_value(cls, encoded: str) -> object:
16
+ type_name, raw = cls._parse_encoded(encoded)
17
+ if type_name != "float":
18
+ raise ValueError(f"FloatCodec cannot decode type {type_name!r}")
19
+ try:
20
+ return float(raw)
21
+ except ValueError:
22
+ raise ValueError(f"Invalid scalar value for type 'float': {raw!r}")
23
+
24
+
25
+ class FloatObject(GPDCScalarObject):
26
+ TYPE = "scalar:float"
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from ..codec import GPDCScalarCodec
4
+ from ..scalar import GPDCScalarObject
5
+
6
+
7
+ class IntCodec(GPDCScalarCodec):
8
+ @classmethod
9
+ def encode_value(cls, value: object) -> str:
10
+ if isinstance(value, bool) or not isinstance(value, int):
11
+ raise TypeError(f"IntCodec cannot encode {type(value).__name__!r}")
12
+ return f"int:{value}"
13
+
14
+ @classmethod
15
+ def decode_value(cls, encoded: str) -> object:
16
+ type_name, raw = cls._parse_encoded(encoded)
17
+ if type_name != "int":
18
+ raise ValueError(f"IntCodec cannot decode type {type_name!r}")
19
+ try:
20
+ return int(raw)
21
+ except (ValueError, OverflowError):
22
+ raise ValueError(f"Invalid scalar value for type 'int': {raw!r}")
23
+
24
+
25
+ class IntObject(GPDCScalarObject):
26
+ TYPE = "scalar:int"
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ import uuid
5
+
6
+ from ..codec import GPDCContainerCodec
7
+ from ..container import GPDCContainerObject
8
+ from ..meta import ObjectMeta
9
+ from ..lifecycle import policy_from_meta
10
+ from ..types import decode_element, encode_element
11
+
12
+
13
+ class ListCodec(GPDCContainerCodec):
14
+ """Codec for Python list values stored as Redis lists."""
15
+
16
+ @classmethod
17
+ def create_raw(cls, namespace, raw_key: str, value: list, owner_name: str, cache_mode: str, ttl: int) -> None:
18
+ from ..lifecycle import policy_from_defaults
19
+ from ..types import encode_element
20
+
21
+ if not value:
22
+ return
23
+ policy = policy_from_defaults(cache_mode, ttl)
24
+ encoded = [encode_element(v, namespace, owner_name, policy) for v in value]
25
+ namespace._redis().rpush(raw_key, *encoded)
26
+
27
+ @classmethod
28
+ def read_raw(cls, namespace, raw_key: str) -> list:
29
+ from ..types import decode_element
30
+
31
+ items = namespace._redis().lrange(raw_key, 0, -1)
32
+ return [decode_element(item, namespace) for item in items]
33
+
34
+ @classmethod
35
+ def delete_raw(cls, namespace, raw_key: str) -> None:
36
+ namespace._redis().delete(raw_key)
37
+
38
+
39
+ class ListObject(GPDCContainerObject, collections.abc.MutableSequence):
40
+ """Redis-backed list object implementing MutableSequence."""
41
+
42
+ TYPE = "collection:list"
43
+
44
+ @property
45
+ def type(self) -> str:
46
+ return self.TYPE
47
+
48
+ @property
49
+ def value(self) -> list:
50
+ return ListCodec.read_raw(self._namespace, self.raw_key)
51
+
52
+ @value.setter
53
+ def value(self, new_value: list) -> None:
54
+ self._reject_value_replacement()
55
+
56
+ def __len__(self) -> int:
57
+ return self._redis().llen(self.raw_key)
58
+
59
+ def _normalize_index(self, index: int, message: str = "list index out of range") -> int:
60
+ if isinstance(index, slice):
61
+ raise TypeError("ListObject does not support slice indexing; use list(obj)[start:stop] instead")
62
+ length = self._redis().llen(self.raw_key)
63
+ if index < 0:
64
+ index = length + index
65
+ if index < 0 or index >= length:
66
+ raise IndexError(message)
67
+ return index
68
+
69
+ def __getitem__(self, index: int | slice) -> object:
70
+ if isinstance(index, slice):
71
+ items = self._redis().lrange(self.raw_key, 0, -1)
72
+ sliced = items[index]
73
+ return [decode_element(enc, self._namespace) for enc in sliced]
74
+ index = self._normalize_index(index)
75
+ encoded = self._redis().lindex(self.raw_key, index)
76
+ if encoded is None:
77
+ raise IndexError("list index out of range")
78
+ return decode_element(encoded, self._namespace)
79
+
80
+ def __setitem__(self, index: int, value: object) -> None:
81
+ r = self._redis()
82
+ index = self._normalize_index(index, "list assignment index out of range")
83
+ old_encoded = r.lindex(self.raw_key, index)
84
+ meta = ObjectMeta.load(r, self._canonical_redis_key())
85
+ policy = policy_from_meta(meta) if meta else None
86
+ encoded = encode_element(value, self._namespace, self._data_owner_name(), policy)
87
+ r.lset(self.raw_key, index, encoded)
88
+ self._release_element_ref(old_encoded)
89
+ self._touch()
90
+
91
+ def _delete_index(self, index: int) -> str:
92
+ r = self._redis()
93
+ index = self._normalize_index(index, "list assignment index out of range")
94
+ old_encoded = r.lindex(self.raw_key, index)
95
+ if old_encoded is None:
96
+ raise IndexError("list assignment index out of range")
97
+ sentinel = f"__del_sentinel_{uuid.uuid4().hex}__"
98
+ r.lset(self.raw_key, index, sentinel)
99
+ r.lrem(self.raw_key, 1, sentinel)
100
+ self._release_element_ref(old_encoded)
101
+ self._touch()
102
+ return old_encoded
103
+
104
+ def __delitem__(self, index: int) -> None:
105
+ self._delete_index(index)
106
+
107
+ def insert(self, index: int, value: object) -> None:
108
+ r = self._redis()
109
+ items = r.lrange(self.raw_key, 0, -1)
110
+ meta = ObjectMeta.load(r, self._canonical_redis_key())
111
+ policy = policy_from_meta(meta) if meta else None
112
+ encoded = encode_element(value, self._namespace, self._data_owner_name(), policy)
113
+ items.insert(index, encoded)
114
+ pipe = r.pipeline()
115
+ pipe.delete(self.raw_key)
116
+ if items:
117
+ pipe.rpush(self.raw_key, *items)
118
+ pipe.execute()
119
+ self._touch()
120
+
121
+ def append(self, value: object) -> None:
122
+ meta = self.meta
123
+ policy = policy_from_meta(meta) if meta else None
124
+ encoded = encode_element(value, self._namespace, self._data_owner_name(), policy)
125
+ self._redis().rpush(self.raw_key, encoded)
126
+ self._touch()
127
+
128
+ def extend(self, values) -> None:
129
+ from ..types import encode_session
130
+
131
+ owner_name = self._data_owner_name()
132
+ meta = self.meta
133
+ policy = policy_from_meta(meta) if meta else None
134
+ with encode_session(self._namespace):
135
+ encoded = [encode_element(v, self._namespace, owner_name, policy) for v in values]
136
+ if encoded:
137
+ self._redis().rpush(self.raw_key, *encoded)
138
+ self._touch()
139
+
140
+ def clear(self) -> None:
141
+ items = self._redis().lrange(self.raw_key, 0, -1)
142
+ self._redis().delete(self.raw_key)
143
+ for item in items:
144
+ self._release_element_ref(item)
145
+ self._touch()
146
+
147
+ def pop(self, index: int = -1) -> object:
148
+ if index == -1:
149
+ r = self._redis()
150
+ encoded = r.lindex(self.raw_key, -1)
151
+ if encoded is None:
152
+ raise IndexError("pop from empty list")
153
+ value = self._decode_and_copy_value(encoded)
154
+ self._delete_index(-1)
155
+ return value
156
+ index = self._normalize_index(index, "pop index out of range")
157
+ encoded = self._redis().lindex(self.raw_key, index)
158
+ if encoded is None:
159
+ raise IndexError("pop index out of range")
160
+ value = self._decode_and_copy_value(encoded)
161
+ self._delete_index(index)
162
+ return value
163
+
164
+ def remove(self, value: object) -> None:
165
+ target_ref = None
166
+ if isinstance(value, GPDCContainerObject):
167
+ if value._namespace is not self._namespace:
168
+ raise ValueError("Cross-namespace container reference sharing is not supported")
169
+ target_ref = value._encode_as_ref_value()
170
+
171
+ for index, encoded in enumerate(self._redis().lrange(self.raw_key, 0, -1)):
172
+ if target_ref is not None and encoded == target_ref:
173
+ self._delete_index(index)
174
+ return
175
+ if target_ref is None and decode_element(encoded, self._namespace) == value:
176
+ self._delete_index(index)
177
+ return
178
+
179
+ raise ValueError(f"{value!r} is not in list")
180
+
181
+ def reverse(self) -> None:
182
+ r = self._redis()
183
+ items = r.lrange(self.raw_key, 0, -1)
184
+ items.reverse()
185
+ pipe = r.pipeline()
186
+ pipe.delete(self.raw_key)
187
+ if items:
188
+ pipe.rpush(self.raw_key, *items)
189
+ pipe.execute()
190
+ self._touch()
191
+
192
+ def sort(self, *, key=None, reverse=False) -> None:
193
+ r = self._redis()
194
+ raw_items = r.lrange(self.raw_key, 0, -1)
195
+ pairs = [(decode_element(enc, self._namespace), enc) for enc in raw_items]
196
+ pairs.sort(key=lambda p: key(p[0]) if key else p[0], reverse=reverse)
197
+ encoded = [enc for _, enc in pairs]
198
+ pipe = r.pipeline()
199
+ pipe.delete(self.raw_key)
200
+ if encoded:
201
+ pipe.rpush(self.raw_key, *encoded)
202
+ pipe.execute()
203
+ self._touch()
204
+
205
+ def count(self, value: object) -> int:
206
+ items = self.value
207
+ return items.count(value)
208
+
209
+ def index(self, value: object, start: int = 0, stop: int = None) -> int: # type: ignore[override]
210
+ items = self.value
211
+ if stop is None:
212
+ return items.index(value, start)
213
+ return items.index(value, start, stop)
214
+
215
+ def deepcopy(self):
216
+ return [v.deepcopy() if hasattr(v, "deepcopy") else v for v in self.value]
217
+
218
+ def __add__(self, other):
219
+ """Not supported. Use ``list(obj) + other`` to concatenate."""
220
+ raise NotImplementedError("ListObject does not support +; use list(obj) + other instead.")
221
+
222
+ def __radd__(self, other):
223
+ """Not supported. Use ``other + list(obj)`` to concatenate."""
224
+ raise NotImplementedError("ListObject does not support +; use other + list(obj) instead.")
225
+
226
+ def __mul__(self, other):
227
+ """Not supported. Use ``list(obj) * n`` to repeat."""
228
+ raise NotImplementedError("ListObject does not support *; use list(obj) * n instead.")
229
+
230
+ def __rmul__(self, other):
231
+ """Not supported. Use ``n * list(obj)`` to repeat."""
232
+ raise NotImplementedError("ListObject does not support *; use n * list(obj) instead.")
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from ..codec import GPDCScalarCodec
4
+ from ..scalar import GPDCScalarObject
5
+
6
+
7
+ class NoneCodec(GPDCScalarCodec):
8
+ @classmethod
9
+ def encode_value(cls, value: object) -> str:
10
+ if value is not None:
11
+ raise TypeError(f"NoneCodec cannot encode {type(value).__name__!r}")
12
+ return "none:None"
13
+
14
+ @classmethod
15
+ def decode_value(cls, encoded: str) -> object:
16
+ type_name, raw = cls._parse_encoded(encoded)
17
+ if type_name != "none":
18
+ raise ValueError(f"NoneCodec cannot decode type {type_name!r}")
19
+ if raw != "None":
20
+ raise ValueError(f"Invalid scalar value for type 'none': {raw!r}")
21
+ return None
22
+
23
+
24
+ class NoneObject(GPDCScalarObject):
25
+ TYPE = "scalar:none"