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 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
- self._changed = False
49
- self._data = {}
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 f'<NAV-Session [new:{self.new}, created:{self.created}] {self._data!r}>'
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
- return iter(self._data)
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 key in self._data
280
+ return self._has_value(str(key))
147
281
 
148
282
  def __getitem__(self, key: str) -> Any:
149
- return self._data[key]
283
+ return self._get_value(key)
150
284
 
151
285
  def __setitem__(self, key: str, value: Any) -> None:
152
- self._data[key] = value
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
- del self._data[key]
158
- self._changed = True
289
+ self._del_value(key)
159
290
 
160
291
  def __getattr__(self, key: str) -> Any:
161
- return self._data[key]
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
@@ -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.7.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.7.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 2.0
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=z4OULrvzA7Y8x25O7AQBXJ5dywJKxjgkiWGFj6MhKBA,5867
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=qR04DpwcmCw55yZJy4MZ_FiZczGhsbOdrn-kRUm91pU,430
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.7.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- navigator_session-0.7.0.dist-info/METADATA,sha256=khRLMQxXBDBxF5EgC0bfXMJpz4ZPfxlnMh6ocvoxEpA,2412
13
- navigator_session-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- navigator_session-0.7.0.dist-info/top_level.txt,sha256=ZpOEy3wLKGsxG2rc0nHqcqJCV3HIOG_XCfE6mtsYYYY,18
15
- navigator_session-0.7.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5