navigator-session 0.7.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 +159 -15
- navigator_session/version.py +2 -1
- {navigator_session-0.7.0.dist-info → navigator_session-0.8.1.dist-info}/METADATA +2 -3
- {navigator_session-0.7.0.dist-info → navigator_session-0.8.1.dist-info}/RECORD +7 -7
- {navigator_session-0.7.0.dist-info → navigator_session-0.8.1.dist-info}/WHEEL +1 -1
- {navigator_session-0.7.0.dist-info → navigator_session-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {navigator_session-0.7.0.dist-info → navigator_session-0.8.1.dist-info}/top_level.txt +0 -0
navigator_session/data.py
CHANGED
|
@@ -12,6 +12,11 @@ from .conf import (
|
|
|
12
12
|
SESSION_ID,
|
|
13
13
|
SESSION_STORAGE
|
|
14
14
|
)
|
|
15
|
+
try:
|
|
16
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
17
|
+
except ImportError:
|
|
18
|
+
PydanticBaseModel = None
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
class ModelHandler(jsonpickle.handlers.BaseHandler):
|
|
17
22
|
"""ModelHandler.
|
|
@@ -30,11 +35,44 @@ class ModelHandler(jsonpickle.handlers.BaseHandler):
|
|
|
30
35
|
|
|
31
36
|
jsonpickle.handlers.registry.register(BaseModel, ModelHandler, base=True)
|
|
32
37
|
|
|
38
|
+
if PydanticBaseModel:
|
|
39
|
+
class PydanticHandler(jsonpickle.handlers.BaseHandler):
|
|
40
|
+
"""PydanticHandler.
|
|
41
|
+
This class can handle with serializable Pydantic Models.
|
|
42
|
+
"""
|
|
43
|
+
def flatten(self, obj, data):
|
|
44
|
+
data['__dict__'] = self.context.flatten(obj.__dict__, reset=False)
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
def restore(self, obj):
|
|
48
|
+
module_and_type = obj['py/object']
|
|
49
|
+
mdl = loadclass(module_and_type)
|
|
50
|
+
cls = mdl.__new__(mdl) if hasattr(mdl, '__new__') else object.__new__(mdl)
|
|
51
|
+
cls.__dict__ = self.context.restore(obj['__dict__'], reset=False)
|
|
52
|
+
return cls
|
|
53
|
+
|
|
54
|
+
jsonpickle.handlers.registry.register(PydanticBaseModel, PydanticHandler, base=True)
|
|
55
|
+
|
|
56
|
+
|
|
33
57
|
class SessionData(MutableMapping[str, Any]):
|
|
34
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.
|
|
35
65
|
"""
|
|
36
66
|
|
|
37
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
|
+
})
|
|
38
76
|
|
|
39
77
|
def __init__(
|
|
40
78
|
self,
|
|
@@ -45,8 +83,10 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
45
83
|
identity: Optional[Any] = None,
|
|
46
84
|
max_age: Optional[int] = None
|
|
47
85
|
) -> None:
|
|
48
|
-
|
|
49
|
-
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)
|
|
50
90
|
# Unique ID:
|
|
51
91
|
self._id_ = (data.get(SESSION_ID, None) if data else id) or uuid.uuid4().hex
|
|
52
92
|
# Session Identity
|
|
@@ -75,7 +115,86 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
75
115
|
self.args = args
|
|
76
116
|
|
|
77
117
|
def __repr__(self) -> str:
|
|
78
|
-
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 ---
|
|
79
198
|
|
|
80
199
|
@property
|
|
81
200
|
def new(self) -> bool:
|
|
@@ -107,7 +226,7 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
107
226
|
|
|
108
227
|
@property
|
|
109
228
|
def empty(self) -> bool:
|
|
110
|
-
return not bool(self._data)
|
|
229
|
+
return not bool(self._data) and not bool(self._objects)
|
|
111
230
|
|
|
112
231
|
@property
|
|
113
232
|
def max_age(self) -> Optional[int]:
|
|
@@ -129,36 +248,61 @@ class SessionData(MutableMapping[str, Any]):
|
|
|
129
248
|
self._changed = True
|
|
130
249
|
|
|
131
250
|
def session_data(self) -> dict:
|
|
251
|
+
"""Return only serializable data (for persistence)."""
|
|
132
252
|
return self._data
|
|
133
253
|
|
|
254
|
+
def session_objects(self) -> dict:
|
|
255
|
+
"""Return in-memory objects (not persisted)."""
|
|
256
|
+
return self._objects
|
|
257
|
+
|
|
134
258
|
def invalidate(self) -> None:
|
|
259
|
+
"""Clear all session data and in-memory objects."""
|
|
135
260
|
self._changed = True
|
|
136
261
|
self._data = {}
|
|
262
|
+
self._objects = {}
|
|
263
|
+
|
|
264
|
+
# --- Magic Methods ---
|
|
137
265
|
|
|
138
|
-
# Magic Methods
|
|
139
266
|
def __len__(self) -> int:
|
|
140
|
-
return len(self._data)
|
|
267
|
+
return len(self._data) + len(self._objects)
|
|
141
268
|
|
|
142
269
|
def __iter__(self) -> Iterator[str]:
|
|
143
|
-
|
|
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
|
|
144
278
|
|
|
145
279
|
def __contains__(self, key: object) -> bool:
|
|
146
|
-
return
|
|
280
|
+
return self._has_value(str(key))
|
|
147
281
|
|
|
148
282
|
def __getitem__(self, key: str) -> Any:
|
|
149
|
-
return self.
|
|
283
|
+
return self._get_value(key)
|
|
150
284
|
|
|
151
285
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
152
|
-
self.
|
|
153
|
-
self._changed = True
|
|
154
|
-
# TODO: also, saved into redis automatically
|
|
286
|
+
self._set_value(key, value)
|
|
155
287
|
|
|
156
288
|
def __delitem__(self, key: str) -> None:
|
|
157
|
-
|
|
158
|
-
self._changed = True
|
|
289
|
+
self._del_value(key)
|
|
159
290
|
|
|
160
291
|
def __getattr__(self, key: str) -> Any:
|
|
161
|
-
|
|
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)
|
|
162
306
|
|
|
163
307
|
def encode(self, obj: Any) -> str:
|
|
164
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.
|
|
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,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: navigator-session
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Navigator Session allows us to store user-specific data into session object.
|
|
5
5
|
Author-email: Jesus Lara Gimenez <jesuslarag@gmail.com>
|
|
6
|
-
License: Apache
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/phenobarbital/navigator-session
|
|
8
8
|
Project-URL: Source, https://github.com/phenobarbital/navigator-session
|
|
9
9
|
Project-URL: Funding, https://paypal.me/phenobarbital
|
|
@@ -22,7 +22,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Framework :: AsyncIO
|
|
24
24
|
Classifier: Framework :: aiohttp
|
|
25
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
26
25
|
Requires-Python: >=3.9.13
|
|
27
26
|
Description-Content-Type: text/markdown
|
|
28
27
|
License-File: LICENSE
|
|
@@ -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.
|
|
12
|
-
navigator_session-0.
|
|
13
|
-
navigator_session-0.
|
|
14
|
-
navigator_session-0.
|
|
15
|
-
navigator_session-0.
|
|
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
|