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.
- gpdatacached/__init__.py +79 -0
- gpdatacached/builtins/__init__.py +3 -0
- gpdatacached/builtins/bool_object.py +25 -0
- gpdatacached/builtins/datetime_object.py +72 -0
- gpdatacached/builtins/dict_object.py +195 -0
- gpdatacached/builtins/float_object.py +26 -0
- gpdatacached/builtins/int_object.py +26 -0
- gpdatacached/builtins/list_object.py +232 -0
- gpdatacached/builtins/none_object.py +25 -0
- gpdatacached/builtins/registry.py +110 -0
- gpdatacached/builtins/set_object.py +161 -0
- gpdatacached/builtins/str_object.py +23 -0
- gpdatacached/builtins/tuple_object.py +49 -0
- gpdatacached/codec.py +47 -0
- gpdatacached/config.py +101 -0
- gpdatacached/container.py +205 -0
- gpdatacached/domain.py +362 -0
- gpdatacached/exceptions.py +112 -0
- gpdatacached/extensions/__init__.py +0 -0
- gpdatacached/extensions/pandas_support.py +1635 -0
- gpdatacached/extensions/pydantic_support.py +169 -0
- gpdatacached/gc.py +282 -0
- gpdatacached/keys.py +150 -0
- gpdatacached/lifecycle.py +171 -0
- gpdatacached/locking.py +163 -0
- gpdatacached/meta.py +126 -0
- gpdatacached/namespace.py +602 -0
- gpdatacached/object.py +318 -0
- gpdatacached/refset.py +54 -0
- gpdatacached/scalar.py +56 -0
- gpdatacached/types.py +296 -0
- gpdatacached-0.1.0.dist-info/METADATA +171 -0
- gpdatacached-0.1.0.dist-info/RECORD +35 -0
- gpdatacached-0.1.0.dist-info/WHEEL +4 -0
- gpdatacached-0.1.0.dist-info/licenses/LICENSE +21 -0
gpdatacached/__init__.py
ADDED
|
@@ -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,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"
|