navigator-session 0.8.0__py3-none-any.whl → 0.8.1__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.
- navigator_session/data.py +135 -15
- navigator_session/version.py +2 -1
- {navigator_session-0.8.0.dist-info → navigator_session-0.8.1.dist-info}/METADATA +1 -1
- {navigator_session-0.8.0.dist-info → navigator_session-0.8.1.dist-info}/RECORD +7 -7
- {navigator_session-0.8.0.dist-info → navigator_session-0.8.1.dist-info}/WHEEL +0 -0
- {navigator_session-0.8.0.dist-info → navigator_session-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {navigator_session-0.8.0.dist-info → navigator_session-0.8.1.dist-info}/top_level.txt +0 -0
navigator_session/data.py
CHANGED
|
@@ -56,9 +56,23 @@ if PydanticBaseModel:
|
|
|
56
56
|
|
|
57
57
|
class SessionData(MutableMapping[str, Any]):
|
|
58
58
|
"""Session dict-like object.
|
|
59
|
+
|
|
60
|
+
Supports both serializable data (stored in _data and persisted) and
|
|
61
|
+
in-memory objects (stored in _objects, not persisted).
|
|
62
|
+
|
|
63
|
+
Non-serializable objects (class instances, etc.) are automatically
|
|
64
|
+
stored in _objects when assigned via session.key = value or session['key'] = value.
|
|
59
65
|
"""
|
|
60
66
|
|
|
61
67
|
_data: Union[str, Any] = {}
|
|
68
|
+
_objects: dict[str, Any] = {}
|
|
69
|
+
|
|
70
|
+
# Internal attributes that should not be stored in _data or _objects
|
|
71
|
+
_internal_attrs = frozenset({
|
|
72
|
+
'_data', '_objects', '_changed', '_id_', '_identity', '_new',
|
|
73
|
+
'_max_age', '_now', '__created__', '_created', '_dow', '_doy',
|
|
74
|
+
'_time', 'args'
|
|
75
|
+
})
|
|
62
76
|
|
|
63
77
|
def __init__(
|
|
64
78
|
self,
|
|
@@ -69,8 +83,10 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
69
83
|
identity: Optional[Any] = None,
|
|
70
84
|
max_age: Optional[int] = None
|
|
71
85
|
) -> None:
|
|
72
|
-
|
|
73
|
-
self
|
|
86
|
+
# Initialize internal storage first (before any attribute access)
|
|
87
|
+
object.__setattr__(self, '_data', {})
|
|
88
|
+
object.__setattr__(self, '_objects', {})
|
|
89
|
+
object.__setattr__(self, '_changed', False)
|
|
74
90
|
# Unique ID:
|
|
75
91
|
self._id_ = (data.get(SESSION_ID, None) if data else id) or uuid.uuid4().hex
|
|
76
92
|
# Session Identity
|
|
@@ -99,7 +115,86 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
99
115
|
self.args = args
|
|
100
116
|
|
|
101
117
|
def __repr__(self) -> str:
|
|
102
|
-
return
|
|
118
|
+
return (
|
|
119
|
+
f'<NAV-Session [new:{self.new}, created:{self.created}] '
|
|
120
|
+
f'data={self._data!r}, objects={list(self._objects.keys())}>'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# --- Serialization helpers ---
|
|
124
|
+
|
|
125
|
+
def _is_serializable(self, value: Any) -> bool:
|
|
126
|
+
"""Check if a value can be reliably serialized and restored with jsonpickle.
|
|
127
|
+
|
|
128
|
+
Returns True for primitive types, dicts, lists, and known serializable models.
|
|
129
|
+
Returns False for arbitrary class instances that may not restore properly
|
|
130
|
+
in a different process/context.
|
|
131
|
+
"""
|
|
132
|
+
# Primitive types are always serializable
|
|
133
|
+
if value is None or isinstance(value, (bool, int, float, str, bytes)):
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
# Dicts and lists need recursive check
|
|
137
|
+
if isinstance(value, dict):
|
|
138
|
+
return all(self._is_serializable(v) for v in value.values())
|
|
139
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
140
|
+
return all(self._is_serializable(v) for v in value)
|
|
141
|
+
|
|
142
|
+
# BaseModel (datamodel) instances are handled by ModelHandler
|
|
143
|
+
if isinstance(value, BaseModel):
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
# Pydantic models are handled by PydanticHandler
|
|
147
|
+
if PydanticBaseModel and isinstance(value, PydanticBaseModel):
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# datetime types are serializable
|
|
151
|
+
if isinstance(value, (datetime,)):
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
# Any other type (class instances, functions, etc.) is NOT reliably serializable
|
|
155
|
+
# These should be stored in-memory only
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def _get_value(self, key: str) -> Any:
|
|
159
|
+
"""Unified getter that checks both _objects and _data."""
|
|
160
|
+
if key in self._objects:
|
|
161
|
+
return self._objects[key]
|
|
162
|
+
if key in self._data:
|
|
163
|
+
return self._data[key]
|
|
164
|
+
raise KeyError(key)
|
|
165
|
+
|
|
166
|
+
def _set_value(self, key: str, value: Any) -> None:
|
|
167
|
+
"""Unified setter that routes to _objects or _data based on serializability."""
|
|
168
|
+
if self._is_serializable(value):
|
|
169
|
+
# Remove from _objects if it was there before
|
|
170
|
+
self._objects.pop(key, None)
|
|
171
|
+
self._data[key] = value
|
|
172
|
+
self._changed = True
|
|
173
|
+
else:
|
|
174
|
+
# Store in _objects (in-memory only, not persisted)
|
|
175
|
+
# Remove from _data if it was there before
|
|
176
|
+
self._data.pop(key, None)
|
|
177
|
+
self._objects[key] = value
|
|
178
|
+
# Note: _objects changes don't set _changed since they're not persisted
|
|
179
|
+
|
|
180
|
+
def _del_value(self, key: str) -> None:
|
|
181
|
+
"""Unified delete that removes from both _objects and _data."""
|
|
182
|
+
deleted = False
|
|
183
|
+
if key in self._objects:
|
|
184
|
+
del self._objects[key]
|
|
185
|
+
deleted = True
|
|
186
|
+
if key in self._data:
|
|
187
|
+
del self._data[key]
|
|
188
|
+
self._changed = True
|
|
189
|
+
deleted = True
|
|
190
|
+
if not deleted:
|
|
191
|
+
raise KeyError(key)
|
|
192
|
+
|
|
193
|
+
def _has_value(self, key: str) -> bool:
|
|
194
|
+
"""Check if key exists in either _objects or _data."""
|
|
195
|
+
return key in self._objects or key in self._data
|
|
196
|
+
|
|
197
|
+
# --- Properties ---
|
|
103
198
|
|
|
104
199
|
@property
|
|
105
200
|
def new(self) -> bool:
|
|
@@ -131,7 +226,7 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
131
226
|
|
|
132
227
|
@property
|
|
133
228
|
def empty(self) -> bool:
|
|
134
|
-
return not bool(self._data)
|
|
229
|
+
return not bool(self._data) and not bool(self._objects)
|
|
135
230
|
|
|
136
231
|
@property
|
|
137
232
|
def max_age(self) -> Optional[int]:
|
|
@@ -153,36 +248,61 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
153
248
|
self._changed = True
|
|
154
249
|
|
|
155
250
|
def session_data(self) -> dict:
|
|
251
|
+
"""Return only serializable data (for persistence)."""
|
|
156
252
|
return self._data
|
|
157
253
|
|
|
254
|
+
def session_objects(self) -> dict:
|
|
255
|
+
"""Return in-memory objects (not persisted)."""
|
|
256
|
+
return self._objects
|
|
257
|
+
|
|
158
258
|
def invalidate(self) -> None:
|
|
259
|
+
"""Clear all session data and in-memory objects."""
|
|
159
260
|
self._changed = True
|
|
160
261
|
self._data = {}
|
|
262
|
+
self._objects = {}
|
|
263
|
+
|
|
264
|
+
# --- Magic Methods ---
|
|
161
265
|
|
|
162
|
-
# Magic Methods
|
|
163
266
|
def __len__(self) -> int:
|
|
164
|
-
return len(self._data)
|
|
267
|
+
return len(self._data) + len(self._objects)
|
|
165
268
|
|
|
166
269
|
def __iter__(self) -> Iterator[str]:
|
|
167
|
-
|
|
270
|
+
# Iterate over both _data and _objects keys
|
|
271
|
+
seen = set()
|
|
272
|
+
for key in self._data:
|
|
273
|
+
seen.add(key)
|
|
274
|
+
yield key
|
|
275
|
+
for key in self._objects:
|
|
276
|
+
if key not in seen:
|
|
277
|
+
yield key
|
|
168
278
|
|
|
169
279
|
def __contains__(self, key: object) -> bool:
|
|
170
|
-
return
|
|
280
|
+
return self._has_value(str(key))
|
|
171
281
|
|
|
172
282
|
def __getitem__(self, key: str) -> Any:
|
|
173
|
-
return self.
|
|
283
|
+
return self._get_value(key)
|
|
174
284
|
|
|
175
285
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
176
|
-
self.
|
|
177
|
-
self._changed = True
|
|
178
|
-
# TODO: also, saved into redis automatically
|
|
286
|
+
self._set_value(key, value)
|
|
179
287
|
|
|
180
288
|
def __delitem__(self, key: str) -> None:
|
|
181
|
-
|
|
182
|
-
self._changed = True
|
|
289
|
+
self._del_value(key)
|
|
183
290
|
|
|
184
291
|
def __getattr__(self, key: str) -> Any:
|
|
185
|
-
|
|
292
|
+
# Avoid infinite recursion for internal attributes
|
|
293
|
+
if key.startswith('_'):
|
|
294
|
+
raise AttributeError(key)
|
|
295
|
+
try:
|
|
296
|
+
return self._get_value(key)
|
|
297
|
+
except KeyError:
|
|
298
|
+
raise AttributeError(key) from None
|
|
299
|
+
|
|
300
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
301
|
+
# Handle internal attributes normally
|
|
302
|
+
if key in self._internal_attrs or key.startswith('_'):
|
|
303
|
+
object.__setattr__(self, key, value)
|
|
304
|
+
else:
|
|
305
|
+
self._set_value(key, value)
|
|
186
306
|
|
|
187
307
|
def encode(self, obj: Any) -> str:
|
|
188
308
|
"""encode
|
navigator_session/version.py
CHANGED
|
@@ -6,8 +6,9 @@ __description__ = (
|
|
|
6
6
|
'Navigator Session allows us to store user-specific data '
|
|
7
7
|
'into session object.'
|
|
8
8
|
)
|
|
9
|
-
__version__ = '0.8.
|
|
9
|
+
__version__ = '0.8.1'
|
|
10
10
|
__copyright__ = 'Copyright (c) 2023 Jesus Lara'
|
|
11
11
|
__author__ = 'Jesus Lara'
|
|
12
12
|
__author_email__ = 'jesuslarag@gmail.com'
|
|
13
13
|
__license__ = 'Apache-2.0'
|
|
14
|
+
__url__ = 'https://github.com/phenobarbital/navigator-session'
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
navigator_session/__init__.py,sha256=U1MtM30ccs5N6c_lzUYFRS5NURz6kheDC8jZmRL_2yc,3056
|
|
2
2
|
navigator_session/conf.py,sha256=Pe8qsukmt5cXXPc3hwFphSWF5xCIpZfeFQ8JhICEnow,1695
|
|
3
|
-
navigator_session/data.py,sha256=
|
|
3
|
+
navigator_session/data.py,sha256=z6W4aCWgcEkSvDynYn4ZeRnjp7gGA0LPWjCe7EyLz-w,11430
|
|
4
4
|
navigator_session/middleware.py,sha256=_bQKfD81BxNZTGuyGTc4yh7y1Rb95I-y2hM1ihE4wCk,1944
|
|
5
5
|
navigator_session/session.py,sha256=CG-TjsmQqjuk6N7GCYNgQTCkrXptgq-swHDiemgHmKI,2398
|
|
6
|
-
navigator_session/version.py,sha256=
|
|
6
|
+
navigator_session/version.py,sha256=DIb5BlRf9IBvfNYoZbYmO-bU4pxDnrZIiij_g8hzUZE,492
|
|
7
7
|
navigator_session/storages/__init__.py,sha256=kTcHfqps5LmnDpFAktxevPuSIwRbnrcMmB7Uygq4axQ,34
|
|
8
8
|
navigator_session/storages/abstract.py,sha256=wTv0yWMVXTwrzA_bjexcwI7c6_W3tzosXlMOIZIQrBM,6503
|
|
9
9
|
navigator_session/storages/cookie.py,sha256=VILzVCatKlDhMoQDoE2y0u6s7lp4wbdep5Oc0NfIJcg,2474
|
|
10
10
|
navigator_session/storages/redis.py,sha256=mqSRyb0YOgV8gvNiAuPhYCLA0fCOgZsQlqOUWE85Gss,11528
|
|
11
|
-
navigator_session-0.8.
|
|
12
|
-
navigator_session-0.8.
|
|
13
|
-
navigator_session-0.8.
|
|
14
|
-
navigator_session-0.8.
|
|
15
|
-
navigator_session-0.8.
|
|
11
|
+
navigator_session-0.8.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
+
navigator_session-0.8.1.dist-info/METADATA,sha256=48VS8qLDfWasNb0m8qxSZi4pl6Gu035Sbu7sKtpMU6g,2360
|
|
13
|
+
navigator_session-0.8.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
navigator_session-0.8.1.dist-info/top_level.txt,sha256=ZpOEy3wLKGsxG2rc0nHqcqJCV3HIOG_XCfE6mtsYYYY,18
|
|
15
|
+
navigator_session-0.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|