navigator-session 0.4.2__tar.gz → 0.6.2__tar.gz

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.
Files changed (37) hide show
  1. {navigator-session-0.4.2 → navigator_session-0.6.2}/Makefile +2 -4
  2. {navigator-session-0.4.2 → navigator_session-0.6.2}/PKG-INFO +9 -2
  3. navigator_session-0.6.2/docs/requirements-dev.txt +21 -0
  4. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/__init__.py +6 -3
  5. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/conf.py +7 -3
  6. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/data.py +19 -8
  7. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/middleware.py +5 -3
  8. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/session.py +11 -9
  9. navigator_session-0.6.2/navigator_session/storages/__init__.py +1 -0
  10. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/storages/abstract.py +53 -32
  11. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/storages/cookie.py +4 -3
  12. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/storages/redis.py +135 -66
  13. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session/version.py +1 -1
  14. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session.egg-info/PKG-INFO +9 -2
  15. navigator_session-0.6.2/navigator_session.egg-info/requires.txt +6 -0
  16. {navigator-session-0.4.2 → navigator_session-0.6.2}/pyproject.toml +5 -4
  17. {navigator-session-0.4.2 → navigator_session-0.6.2}/setup.py +15 -14
  18. navigator-session-0.4.2/docs/requirements-dev.txt +0 -19
  19. navigator-session-0.4.2/navigator_session/storages/__init__.py +0 -6
  20. navigator-session-0.4.2/navigator_session.egg-info/requires.txt +0 -9
  21. {navigator-session-0.4.2 → navigator_session-0.6.2}/CHANGES.rst +0 -0
  22. {navigator-session-0.4.2 → navigator_session-0.6.2}/LICENSE +0 -0
  23. {navigator-session-0.4.2 → navigator_session-0.6.2}/MANIFEST.in +0 -0
  24. {navigator-session-0.4.2 → navigator_session-0.6.2}/README.md +0 -0
  25. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/Makefile +0 -0
  26. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/api.rst +0 -0
  27. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/authors.rst +0 -0
  28. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/conf.py +0 -0
  29. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/examples.rst +0 -0
  30. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/index.rst +0 -0
  31. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/make.bat +0 -0
  32. {navigator-session-0.4.2 → navigator_session-0.6.2}/docs/requirements.txt +0 -0
  33. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session.egg-info/SOURCES.txt +0 -0
  34. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session.egg-info/dependency_links.txt +0 -0
  35. {navigator-session-0.4.2 → navigator_session-0.6.2}/navigator_session.egg-info/top_level.txt +0 -0
  36. {navigator-session-0.4.2 → navigator_session-0.6.2}/setup.cfg +0 -0
  37. {navigator-session-0.4.2 → navigator_session-0.6.2}/tests/__init__.py +0 -0
@@ -1,13 +1,11 @@
1
1
  venv:
2
- python3.10 -m venv .venv
2
+ python3.11 -m venv .venv
3
3
  echo 'run `source .venv/bin/activate` to start develop Navigator-Session'
4
4
 
5
- setup:
6
- pip install wheel==0.38.4
5
+ install:
7
6
  pip install -e .
8
7
 
9
8
  develop:
10
- pip install wheel==0.38.4
11
9
  pip install -e .
12
10
  pip install -Ur docs/requirements-dev.txt
13
11
  flit install --symlink
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: navigator-session
3
- Version: 0.4.2
3
+ Version: 0.6.2
4
4
  Summary: Navigator Session allows us to store user-specific data into session object.
5
5
  Home-page: https://github.com/phenobarbital/navigator-session
6
6
  Author: Jesus Lara
@@ -21,11 +21,18 @@ Classifier: Topic :: Database :: Front-Ends
21
21
  Classifier: Programming Language :: Python :: 3.9
22
22
  Classifier: Programming Language :: Python :: 3.10
23
23
  Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
24
25
  Classifier: Framework :: AsyncIO
25
26
  Classifier: Framework :: aiohttp
26
- Requires-Python: >=3.9.16
27
+ Requires-Python: >=3.9.13
27
28
  Description-Content-Type: text/markdown
28
29
  License-File: LICENSE
30
+ Requires-Dist: aiohttp>=3.9.5
31
+ Requires-Dist: asyncio==3.4.3
32
+ Requires-Dist: jsonpickle>=3.0.2
33
+ Requires-Dist: redis>=5.0.4
34
+ Requires-Dist: python_datamodel>=0.7.0
35
+ Requires-Dist: navconfig[default]>=1.7.0
29
36
 
30
37
  # Navigator_Session #
31
38
 
@@ -0,0 +1,21 @@
1
+ aiounittest==1.4.2
2
+ black==24.3.0
3
+ build==1.0.3
4
+ coverage[toml]==7.2.7
5
+ flit==3.9.0
6
+ hypothesis==6.91.0
7
+ ipython==8.14.0
8
+ lint==1.2.1
9
+ mypy==1.7.1
10
+ pylint==2.16.1
11
+ pyperf==2.6.2
12
+ pytest>=6.0.0
13
+ pytest-asyncio==0.23.2
14
+ pytest-cov==4.0.0
15
+ pytest-cython==0.2.0
16
+ pytest-xdist==3.5.0
17
+ pytest-assume==2.4.3
18
+ sdist==0.0.0
19
+ sphinx==6.1.3
20
+ tox==4.6.4
21
+ twine==5.1.1
@@ -12,6 +12,8 @@ from .conf import (
12
12
  AUTH_SESSION_OBJECT,
13
13
  SESSION_TIMEOUT,
14
14
  SESSION_KEY,
15
+ SESSION_ID,
16
+ SESSION_REQUEST_KEY,
15
17
  SESSION_URL,
16
18
  SESSION_PREFIX,
17
19
  SESSION_USER_PROPERTY
@@ -26,6 +28,7 @@ __all__ = (
26
28
  'SESSION_URL',
27
29
  'SESSION_PREFIX',
28
30
  'SESSION_KEY',
31
+ 'SESSION_ID',
29
32
  'SESSION_USER_PROPERTY',
30
33
  )
31
34
 
@@ -37,7 +40,7 @@ async def new_session(request: web.Request, userdata: dict = None) -> SessionDat
37
40
  storage = request.get(SESSION_STORAGE)
38
41
  if storage is None:
39
42
  raise RuntimeError(
40
- "Missing Configuration for Session Middleware, please install on Aiohttp Middlewares"
43
+ "Missing Configuration for NAV Session Middleware."
41
44
  )
42
45
  session = await storage.new_session(request, userdata)
43
46
  if not isinstance(session, SessionData):
@@ -75,7 +78,7 @@ async def get_session(
75
78
  storage = request.get(SESSION_STORAGE)
76
79
  if storage is None:
77
80
  raise RuntimeError(
78
- "Missing Configuration for Session Storage, please install Session Middleware."
81
+ "Missing Configuration of Session Storage, please install it."
79
82
  )
80
83
  # using the storage session for Load an existing Session
81
84
  try:
@@ -90,7 +93,7 @@ async def get_session(
90
93
  f"Error Loading user Session: {err!s}"
91
94
  ) from err
92
95
  request[SESSION_OBJECT] = session
93
- request['session'] = session
96
+ request[SESSION_REQUEST_KEY] = session
94
97
  if new is True and not isinstance(session, SessionData):
95
98
  raise RuntimeError(
96
99
  f"Installed {session!r} storage should return session instance "
@@ -22,9 +22,12 @@ SESSION_NAME = f"{APP_TITLE}_SESSION"
22
22
  JWT_ALGORITHM = config.get("JWT_ALGORITHM", fallback="HS256")
23
23
  SESSION_PREFIX = f'{SESSION_PREFIX}_session'
24
24
  SESSION_TIMEOUT = config.getint('SESSION_TIMEOUT', fallback=360000)
25
- SESSION_STORAGE = 'NAVIGATOR_SESSION_STORAGE'
26
- SESSION_OBJECT = 'NAV_SESSION'
27
-
25
+ SESSION_STORAGE = config.get(
26
+ 'NAV_SESSION_STORAGE',
27
+ fallback='NAVIGATOR_SESSION_STORAGE'
28
+ )
29
+ SESSION_OBJECT = config.get('SESSION_OBJECT', fallback='NAV_SESSION')
30
+ SESSION_REQUEST_KEY = config.get('SESSION_REQUEST_KEY', fallback='session')
28
31
 
29
32
  # SESSION BACKEND:
30
33
  SESSION_BACKEND = config.get('SESSION_BACKEND', fallback='redis')
@@ -38,4 +41,5 @@ SESSION_URL = f"{SESSION_BACKEND}://{REDIS_HOST}:{REDIS_PORT}/{REDIS_SESSION_DB}
38
41
  # User Attributes:
39
42
  SESSION_USER_PROPERTY = config.get('SESSION_USER_PROPERTY', fallback='user')
40
43
  SESSION_KEY = config.get('SESSION_KEY', fallback='id')
44
+ SESSION_ID = config.get('SESSION_ID', fallback='session_id')
41
45
  SESSION_COOKIE_SECURE = config.get('SESSION_COOKIE_SECURE', fallback='csrf_secure')
@@ -8,8 +8,11 @@ import jsonpickle
8
8
  from jsonpickle.unpickler import loadclass
9
9
  from aiohttp import web
10
10
  from datamodel import BaseModel
11
- from navigator_session.conf import SESSION_KEY, SESSION_STORAGE
12
-
11
+ from .conf import (
12
+ SESSION_KEY,
13
+ SESSION_ID,
14
+ SESSION_STORAGE
15
+ )
13
16
 
14
17
  class ModelHandler(jsonpickle.handlers.BaseHandler):
15
18
  """ModelHandler.
@@ -43,21 +46,27 @@ class SessionData(MutableMapping[str, Any]):
43
46
  *args,
44
47
  data: Optional[Mapping[str, Any]] = None,
45
48
  new: bool = False,
49
+ id: Optional[str] = None,
46
50
  identity: Optional[Any] = None,
47
51
  max_age: Optional[int] = None
48
52
  ) -> None:
49
53
  self._changed = False
50
54
  self._data = {}
55
+ # Unique ID:
56
+ self._id_ = data.get(SESSION_ID, None) if data else id
57
+ if not self._id_:
58
+ self._id_ = uuid.uuid4().hex
59
+ # Session Identity
51
60
  self._identity = data.get(SESSION_KEY, None) if data else identity
52
61
  if not self._identity:
53
- self._identity = uuid.uuid4().hex
62
+ self._identity = self._id_
54
63
  self._new = new if data != {} else True
55
64
  self._max_age = max_age if max_age else None
56
65
  created = data.get('created', None) if data else None
57
- self._now = pendulum.now('UTC') # time for this instance creation
66
+ self._now = pendulum.now('UTC') # time for this instance creation
58
67
  self.__created__ = self._now
59
68
  now = int(self._now.int_timestamp)
60
- self._now = now # time for this instance creation
69
+ self._now = now # time for this instance creation
61
70
  age = now - created if created else now
62
71
  if max_age is not None and age > max_age:
63
72
  data = None
@@ -75,9 +84,7 @@ class SessionData(MutableMapping[str, Any]):
75
84
  self.args = args
76
85
 
77
86
  def __repr__(self) -> str:
78
- return '<{} [new:{}, created:{}] {!r}>'.format( # pylint: disable=C0209
79
- 'NAV-Session ', self.new, self.created, self._data
80
- )
87
+ return f'<NAV-Session [new:{self.new}, created:{self.created}] {self._data!r}>'
81
88
 
82
89
  @property
83
90
  def new(self) -> bool:
@@ -87,6 +94,10 @@ class SessionData(MutableMapping[str, Any]):
87
94
  def logon_time(self) -> datetime:
88
95
  return self.__created__
89
96
 
97
+ @property
98
+ def session_id(self) -> str:
99
+ return self._id_
100
+
90
101
  @property
91
102
  def identity(self) -> Optional[Any]: # type: ignore[misc]
92
103
  return self._identity
@@ -3,25 +3,27 @@ from collections.abc import Callable, Awaitable
3
3
  from aiohttp import web
4
4
  from aiohttp.web_middlewares import Handler
5
5
  from navconfig.logging import logging
6
- from navigator_session.conf import (
6
+ from .conf import (
7
7
  SESSION_OBJECT,
8
8
  SESSION_STORAGE
9
9
  )
10
10
  from .storages.abstract import AbstractStorage
11
11
  from .data import SessionData
12
12
 
13
+
13
14
  Middleware = Callable[[web.Request, Handler], Awaitable[web.StreamResponse]]
14
15
 
16
+
15
17
  ### Basic Middleware for Session System
16
18
  def session_middleware(
17
- app: web.Application, # pylint: disable=W0613
19
+ app: web.Application, # pylint: disable=W0613
18
20
  storage: AbstractStorage
19
21
  ) -> Middleware:
20
22
  """Middleware to attach Session Storage to every Request."""
21
23
  if not isinstance(storage, AbstractStorage):
22
24
  raise RuntimeError(
23
25
  f"Expected an AbstractStorage got {storage!s}"
24
- )
26
+ )
25
27
 
26
28
  @web.middleware
27
29
  async def middleware(
@@ -8,10 +8,13 @@ class SessionHandler:
8
8
  """Authentication Backend for Navigator."""
9
9
  storage: Callable = None
10
10
 
11
- def __init__(self,storage: str = 'redis', **kwargs) -> None:
11
+ def __init__(self, storage: str = 'redis', **kwargs) -> None:
12
12
  # TODO: Session Support with parametrization (other storages):
13
+ self._session_object: str = kwargs.get('session_object', 'nav_session')
14
+ # Logging Object:
15
+ self.logger = logging.getLogger(self.__class__.__name__)
13
16
  if storage == 'redis':
14
- self.storage = RedisStorage(**kwargs)
17
+ self.storage = RedisStorage(logger=self.logger, **kwargs)
15
18
  else:
16
19
  raise NotImplementedError(
17
20
  f"Cannot load a Session Storage {storage}"
@@ -19,9 +22,9 @@ class SessionHandler:
19
22
 
20
23
  def setup(self, app: web.Application) -> None:
21
24
  if isinstance(app, web.Application):
22
- self.app = app # register the app into the Extension
25
+ self.app = app # register the app into the Extension
23
26
  else:
24
- self.app = app.get_app() # Nav Application
27
+ self.app = app.get_app() # Nav Application
25
28
  ## Configure the Middleware for NAV Session.
26
29
  self.app.middlewares.append(
27
30
  session_middleware(app, self.storage)
@@ -34,10 +37,9 @@ class SessionHandler:
34
37
  self.app.on_cleanup.append(
35
38
  self.session_cleanup
36
39
  )
37
- logging.debug(':::: Session Handler Loaded ::::')
40
+ self.logger.debug(':::: Session Handler Loaded ::::')
38
41
  # register the Auth extension into the app
39
- self.app['nav_session'] = self
40
-
42
+ self.app[self._session_object] = self
41
43
 
42
44
  async def session_startup(self, app: web.Application):
43
45
  """
@@ -46,7 +48,7 @@ class SessionHandler:
46
48
  try:
47
49
  await self.storage.on_startup(app)
48
50
  except Exception as ex:
49
- logging.exception(f'{ex}')
51
+ self.logger.exception(f'{ex}')
50
52
  raise RuntimeError(
51
53
  f"Session Storage Error: cannot start Storage Backend {ex}"
52
54
  ) from ex
@@ -58,7 +60,7 @@ class SessionHandler:
58
60
  try:
59
61
  await self.storage.on_cleanup(app)
60
62
  except Exception as ex:
61
- logging.exception(f'{ex}')
63
+ self.logger.exception(f'{ex}')
62
64
  raise RuntimeError(
63
65
  f"Session Storage Error: cannot start Storage Backend {ex}"
64
66
  ) from ex
@@ -0,0 +1 @@
1
+ """NAV Session Storage Module."""
@@ -1,19 +1,26 @@
1
1
  """Base Class for all Session Storages."""
2
2
  import sys
3
- import abc
3
+ from abc import ABCMeta, abstractmethod
4
4
  import uuid
5
5
  import time
6
6
  import logging
7
7
  from typing import Optional
8
8
  from aiohttp import web
9
9
  from datamodel.parsers.encoders import DefaultEncoder
10
- from navigator_session.conf import (
10
+ from datamodel.parsers.json import ( # pylint: disable=C0411
11
+ json_encoder,
12
+ json_decoder
13
+ )
14
+ from ..conf import (
11
15
  SESSION_TIMEOUT,
12
16
  SESSION_KEY,
17
+ SESSION_ID,
13
18
  SESSION_OBJECT,
19
+ SESSION_REQUEST_KEY,
14
20
  SESSION_COOKIE_SECURE
15
21
  )
16
- from navigator_session.data import SessionData
22
+ from ..data import SessionData
23
+
17
24
 
18
25
  if sys.version_info >= (3, 8):
19
26
  from typing import TypedDict
@@ -29,13 +36,14 @@ class _CookieParams(TypedDict, total=False):
29
36
  samesite: Optional[str]
30
37
  expires: str
31
38
 
32
- class AbstractStorage(metaclass=abc.ABCMeta):
39
+ class AbstractStorage(metaclass=ABCMeta):
33
40
 
34
- use_cookie: bool = False
41
+ _use_cookies: bool = False
35
42
 
36
43
  def __init__(
37
44
  self,
38
45
  *,
46
+ logger: Optional[logging.Logger] = None,
39
47
  max_age: int = None,
40
48
  secure: bool = True,
41
49
  domain: Optional[str] = None,
@@ -43,14 +51,17 @@ class AbstractStorage(metaclass=abc.ABCMeta):
43
51
  httponly: bool = True,
44
52
  samesite: Optional[str] = 'Lax',
45
53
  **kwargs
46
- ) -> None:
54
+ ) -> None:
55
+ if logger is not None:
56
+ self._logger = logger
57
+ else:
58
+ self._logger = logging.getLogger('Nav_Session.Storage')
47
59
  if not max_age:
48
60
  self.max_age = SESSION_TIMEOUT
49
61
  else:
50
62
  self.max_age = max_age
51
- if 'use_cookie' in kwargs:
52
- self.use_cookie = kwargs['use_cookie']
53
- del kwargs['use_cookie']
63
+ # Using session cookies:
64
+ self._use_cookies = kwargs.get('use_cookies', False)
54
65
  # Storage Name
55
66
  self.__name__: str = SESSION_COOKIE_SECURE
56
67
  self._domain: Optional[str] = domain
@@ -60,8 +71,8 @@ class AbstractStorage(metaclass=abc.ABCMeta):
60
71
  self._httponly = httponly
61
72
  self._samesite = samesite
62
73
  self._objencoder = DefaultEncoder()
63
- self._encoder = self._objencoder.dumps
64
- self._decoder = self._objencoder.loads
74
+ self._encoder = json_encoder
75
+ self._decoder = json_decoder
65
76
 
66
77
  def id_factory(self) -> str:
67
78
  return uuid.uuid4().hex
@@ -70,15 +81,15 @@ class AbstractStorage(metaclass=abc.ABCMeta):
70
81
  def cookie_name(self) -> str:
71
82
  return self.__name__
72
83
 
73
- @abc.abstractmethod
84
+ @abstractmethod
74
85
  async def on_startup(self, app: web.Application):
75
86
  pass
76
87
 
77
- @abc.abstractmethod
88
+ @abstractmethod
78
89
  async def on_cleanup(self, app: web.Application):
79
90
  pass
80
91
 
81
- @abc.abstractmethod
92
+ @abstractmethod
82
93
  async def new_session(
83
94
  self,
84
95
  request: web.Request,
@@ -86,7 +97,7 @@ class AbstractStorage(metaclass=abc.ABCMeta):
86
97
  ) -> SessionData:
87
98
  pass
88
99
 
89
- @abc.abstractmethod
100
+ @abstractmethod
90
101
  async def load_session(
91
102
  self,
92
103
  request: web.Request,
@@ -97,55 +108,66 @@ class AbstractStorage(metaclass=abc.ABCMeta):
97
108
  ) -> SessionData:
98
109
  pass
99
110
 
100
- @abc.abstractmethod
111
+ @abstractmethod
101
112
  async def get_session(self, request: web.Request) -> SessionData:
102
113
  pass
103
114
 
104
115
  def empty_session(self) -> SessionData:
105
- return SessionData(None, data=None, new=True, max_age=self.max_age)
106
-
107
- @abc.abstractmethod
108
- async def save_session(self,
116
+ return SessionData(
117
+ None,
118
+ data=None,
119
+ new=True,
120
+ max_age=self.max_age
121
+ )
122
+
123
+ @abstractmethod
124
+ async def save_session(
125
+ self,
109
126
  request: web.Request,
110
127
  response: web.StreamResponse,
111
128
  session: SessionData
112
129
  ) -> None:
113
130
  pass
114
131
 
115
- @abc.abstractmethod
132
+ @abstractmethod
116
133
  async def invalidate(
117
134
  self,
118
135
  request: web.Request,
119
136
  session: SessionData
120
137
  ) -> None:
121
138
  """Try to Invalidate the Session in the Storage."""
139
+ pass
122
140
 
123
141
  async def forgot(self, request: web.Request, response: web.StreamResponse = None):
124
- """Delete a User Session."""
142
+ """forgot.
143
+
144
+ Forgot (delete) a user session.
145
+ """
125
146
  session = await self.get_session(request)
126
147
  await self.invalidate(request, session)
127
- request["session"] = None
148
+ request[SESSION_REQUEST_KEY] = None
128
149
  try:
129
150
  del request[SESSION_KEY]
151
+ del request[SESSION_ID]
130
152
  del request[SESSION_OBJECT]
131
- except Exception as err: # pylint: disable=W0703
132
- logging.warning(
153
+ except Exception as err: # pylint: disable=W0703
154
+ self._logger.warning(
133
155
  f'Session: Error on Forgot Method: {err}'
134
156
  )
135
157
  if response is not None:
136
158
  # also, forgot the secure Cookie:
137
- self.forgot_cooke(response)
159
+ self.forgot_cookie(response)
138
160
 
139
161
  def load_cookie(self, request: web.Request) -> str:
140
162
  """Getting Cookie from User (if needed)"""
141
- if self.use_cookie is True:
163
+ if self._use_cookies is True:
142
164
  cookie = request.cookies.get(self.__name__, None)
143
165
  if cookie:
144
166
  return self._decoder(cookie)
145
167
  return None
146
168
 
147
- def forgot_cooke(self, response: web.StreamResponse) -> None:
148
- if self.use_cookie is True:
169
+ def forgot_cookie(self, response: web.StreamResponse) -> None:
170
+ if self._use_cookies is True:
149
171
  response.del_cookie(
150
172
  self.__name__, domain=self._domain, path=self._path
151
173
  )
@@ -157,7 +179,7 @@ class AbstractStorage(metaclass=abc.ABCMeta):
157
179
  *,
158
180
  max_age: Optional[int] = None,
159
181
  ) -> None:
160
- if self.use_cookie is True:
182
+ if self._use_cookies is True:
161
183
  expires = None
162
184
  if max_age is not None:
163
185
  t = time.gmtime(time.time() + max_age)
@@ -180,7 +202,6 @@ class AbstractStorage(metaclass=abc.ABCMeta):
180
202
  else:
181
203
  response.set_cookie(self.__name__, cookie_data, **params)
182
204
 
183
-
184
205
  def session_info(self, session: SessionData, request: web.Request) -> SessionData:
185
206
  """session_info.
186
207
  Session Helper for adding more information extracted from Request.
@@ -198,6 +219,6 @@ class AbstractStorage(metaclass=abc.ABCMeta):
198
219
  session.headers = request.headers
199
220
  session.rel_url = request.rel_url
200
221
  except (TypeError, AttributeError, ValueError) as ex:
201
- logging.warning(f'Unable to read Request info: {ex}')
222
+ self._logger.warning(f'Unable to read Request info: {ex}')
202
223
  ### modified Session Object:
203
224
  return session
@@ -1,4 +1,4 @@
1
- """Encrypted JSON Cookie Storage."""
1
+ """TODO: Encrypted JSON Cookie Storage."""
2
2
  import base64
3
3
  from typing import (
4
4
  Optional,
@@ -24,7 +24,7 @@ class CookieStorage(AbstractStorage):
24
24
  path: str = "/",
25
25
  secret_key: Union[str, bytes, bytearray, fernet.Fernet] = None,
26
26
  **kwargs
27
- ) -> None:
27
+ ) -> None:
28
28
  super(
29
29
  CookieStorage, self
30
30
  ).__init__(
@@ -75,7 +75,8 @@ class CookieStorage(AbstractStorage):
75
75
  async def get_session(self, request: web.Request) -> SessionData:
76
76
  pass
77
77
 
78
- async def save_session(self,
78
+ async def save_session(
79
+ self,
79
80
  request: web.Request,
80
81
  response: web.StreamResponse,
81
82
  session: SessionData
@@ -1,14 +1,15 @@
1
1
  """Using Redis for Saving Session Storage."""
2
2
  import time
3
- import logging
4
3
  from typing import Optional
5
4
  from collections.abc import Callable
6
5
  from aiohttp import web
7
- import aioredis
8
- from navigator_session.conf import (
6
+ from redis import asyncio as aioredis
7
+ from ..conf import (
9
8
  SESSION_URL,
10
9
  SESSION_KEY,
10
+ SESSION_ID,
11
11
  SESSION_OBJECT,
12
+ SESSION_REQUEST_KEY,
12
13
  SESSION_STORAGE
13
14
  )
14
15
  from .abstract import AbstractStorage, SessionData
@@ -24,7 +25,7 @@ class RedisStorage(AbstractStorage):
24
25
  domain: Optional[str] = None,
25
26
  path: str = "/",
26
27
  **kwargs
27
- ) -> None:
28
+ ) -> None:
28
29
  self._redis: Callable = None
29
30
  super(
30
31
  RedisStorage, self
@@ -43,21 +44,27 @@ class RedisStorage(AbstractStorage):
43
44
  decode_responses=True,
44
45
  encoding='utf-8'
45
46
  )
46
- except Exception as err: # pylint: disable=W0703
47
- logging.exception(err, stack_info=True)
47
+ except Exception as err: # pylint: disable=W0703
48
+ self._logger.exception(err, stack_info=True)
48
49
  return False
49
50
 
50
51
  async def on_cleanup(self, app: web.Application):
51
52
  try:
52
- await self._redis.disconnect(inuse_connections = True)
53
- except Exception as ex: # pylint: disable=W0703
54
- logging.warning(ex)
53
+ await self._redis.disconnect(inuse_connections=True)
54
+ except Exception as ex: # pylint: disable=W0703
55
+ self._logger.warning(ex)
55
56
 
56
- async def get_session(self, request: web.Request, userdata: dict = None) -> SessionData:
57
+ async def get_session(
58
+ self,
59
+ request: web.Request,
60
+ userdata: dict = None
61
+ ) -> SessionData:
57
62
  try:
58
63
  session = request.get(SESSION_OBJECT)
59
- except Exception as err: # pylint: disable=W0703
60
- logging.debug(f'Redis Storage: Error on get Session: {err!s}')
64
+ except Exception as err: # pylint: disable=W0703
65
+ self._logger.debug(
66
+ f'Redis Storage: Error on get Session: {err!s}'
67
+ )
61
68
  session = None
62
69
  if session is None:
63
70
  storage = request.get(SESSION_STORAGE)
@@ -67,28 +74,40 @@ class RedisStorage(AbstractStorage):
67
74
  )
68
75
  session = await self.load_session(request, userdata)
69
76
  request[SESSION_OBJECT] = session
70
- request["session"] = session
77
+ request[SESSION_REQUEST_KEY] = session
71
78
  return session
72
79
 
73
80
  async def invalidate(self, request: web.Request, session: SessionData) -> None:
74
81
  conn = aioredis.Redis(connection_pool=self._redis)
75
82
  if not session:
76
83
  data = None
77
- session_id = request.get(SESSION_KEY, None)
84
+ session_id = request.get(SESSION_ID, None)
78
85
  if session_id:
79
- data = await conn.get(session_id)
86
+ _id_ = f"session:{session_id}"
87
+ data = await conn.get(_id_)
80
88
  if data is None:
81
89
  # nothing to forgot
82
90
  return True
83
91
  try:
84
92
  # delete the ID of the session
85
- await conn.delete(session.identity)
86
- session.invalidate() # invalidate this session object
87
- except Exception as err: # pylint: disable=W0703
88
- logging.error(err)
93
+ await conn.delete(f"session:{session.session_id}")
94
+ session.invalidate() # invalidate this session object
95
+ except Exception as err: # pylint: disable=W0703
96
+ self._logger.error(err)
89
97
  return False
90
98
  return True
91
99
 
100
+ async def get_session_id(self, conn: aioredis.Redis, identity: str) -> str:
101
+ """Get Session ID from Redis."""
102
+ try:
103
+ session_id = await conn.get(f"user:{identity}")
104
+ except Exception as err:
105
+ self._logger.error(
106
+ f'Redis Storage: Error Getting Session ID: {err!s}'
107
+ )
108
+ session_id = None
109
+ return session_id
110
+
92
111
  async def load_session(
93
112
  self,
94
113
  request: web.Request,
@@ -106,9 +125,9 @@ class RedisStorage(AbstractStorage):
106
125
  ---
107
126
  new: if False, new session is not created.
108
127
  """
109
- # TODO: first: for security, check if cookie csrf_secure exists
128
+ # first: for security, check if cookie csrf_secure exists
110
129
  session_id = None
111
- if ignore_cookie is False and self.use_cookie is True:
130
+ if ignore_cookie is False and self._use_cookies is True:
112
131
  cookie = self.load_cookie(request)
113
132
  try:
114
133
  session_id = cookie['session_id']
@@ -118,26 +137,35 @@ class RedisStorage(AbstractStorage):
118
137
  try:
119
138
  conn = aioredis.Redis(connection_pool=self._redis)
120
139
  except Exception as err:
121
- logging.exception(
140
+ self._logger.exception(
122
141
  f'Redis Storage: Error loading Redis Session: {err!s}'
123
142
  )
124
143
  raise RuntimeError(
125
144
  f'Redis Storage: Error loading Redis Session: {err!s}'
126
145
  ) from err
127
146
  if session_id is None:
128
- session_id = request.get(SESSION_KEY, None)
147
+ session_id = request.get(SESSION_ID, None)
129
148
  if not session_id:
130
- session_id = userdata.get(SESSION_KEY, None) if userdata else None
131
- # TODO: getting from cookie
149
+ session_id = userdata.get(SESSION_ID, None) if userdata else None
150
+ # get session id from redis using identity:
151
+ session_identity = userdata.get(
152
+ SESSION_KEY, None) if userdata else request.get(SESSION_KEY, None)
153
+ if not session_id:
154
+ session_id = await self.get_session_id(conn, session_identity)
155
+ print('SESSION IDENTITY IS:', session_identity, session_id)
132
156
  if session_id is None and new is False:
157
+ # No Session was found, returning false:
133
158
  return False
134
159
  # we need to load session data from redis
135
- logging.debug(f':::::: LOAD SESSION FOR {session_id} ::::: ')
160
+ self._logger.debug(
161
+ f':::::: LOAD SESSION FOR {session_id} ::::: '
162
+ )
163
+ _id_ = f"session:{session_id}"
136
164
  try:
137
- data = await conn.get(session_id)
138
- except Exception as err: # pylint: disable=W0703
139
- logging.error(
140
- f'Redis Storage: Error Getting Session data: {err!s}'
165
+ data = await conn.get(_id_)
166
+ except Exception as err: # pylint: disable=W0703
167
+ self._logger.error(
168
+ f'Redis Storage: Error Getting Session: {err!s}'
141
169
  )
142
170
  data = None
143
171
  if data is None:
@@ -145,46 +173,57 @@ class RedisStorage(AbstractStorage):
145
173
  # create a new session if not exists:
146
174
  return await self.new_session(request, userdata)
147
175
  else:
176
+ # No Session Was Found
148
177
  return False
149
178
  try:
150
179
  data = self._decoder(data)
151
180
  session = SessionData(
152
- identity=session_id,
181
+ id=session_id,
182
+ identity=session_identity,
153
183
  data=data,
154
184
  new=False,
155
185
  max_age=self.max_age
156
186
  )
157
- except Exception as err: # pylint: disable=W0703
158
- logging.warning(err)
187
+ except Exception as err: # pylint: disable=W0703
188
+ self._logger.warning(
189
+ f"Error creating Session Data: {err}"
190
+ )
159
191
  session = SessionData(
160
- identity=None,
192
+ id=session_id,
193
+ identity=session_identity,
161
194
  data=None,
162
195
  new=True,
163
196
  max_age=self.max_age
164
197
  )
165
198
  ## add other options to session:
166
199
  self.session_info(session, request)
167
- request[SESSION_KEY] = session_id
168
200
  session[SESSION_KEY] = session_id
201
+ request[SESSION_KEY] = session_identity
202
+ request[SESSION_ID] = session_id
169
203
  request[SESSION_OBJECT] = session
170
- request["session"] = session
171
- if self.use_cookie is True and response is not None:
204
+ request[SESSION_REQUEST_KEY] = session
205
+ if self._use_cookies is True and response is not None:
172
206
  cookie_data = {
173
207
  "session_id": session_id
174
208
  }
175
209
  cookie_data = self._encoder(cookie_data)
176
- self.save_cookie(response, cookie_data=cookie_data, max_age=self.max_age)
210
+ self.save_cookie(
211
+ response,
212
+ cookie_data=cookie_data,
213
+ max_age=self.max_age
214
+ )
177
215
  return session
178
216
 
179
- async def save_session(self,
217
+ async def save_session(
218
+ self,
180
219
  request: web.Request,
181
220
  response: web.StreamResponse,
182
221
  session: SessionData
183
222
  ) -> None:
184
223
  """Save the whole session in the backend Storage."""
185
- session_id = session.identity
224
+ session_id = session.session_id if session else request.get(SESSION_ID, None)
186
225
  if not session_id:
187
- session_id = session.get(SESSION_KEY, None)
226
+ session_id = session.get(SESSION_ID, None)
188
227
  if not session_id:
189
228
  session_id = self.id_factory()
190
229
  if session.empty:
@@ -192,15 +231,14 @@ class RedisStorage(AbstractStorage):
192
231
  data = self._encoder(session.session_data())
193
232
  max_age = session.max_age
194
233
  expire = max_age if max_age is not None else 0
195
- # TODO: add expiration on redis to value
196
234
  try:
197
235
  conn = aioredis.Redis(connection_pool=self._redis)
198
- result = await conn.set(
199
- session_id, data, expire
236
+ _id_ = f"session:{session_id}"
237
+ await conn.set(
238
+ _id_, data, expire
200
239
  )
201
- except Exception as err: # pylint: disable=W0703
202
- print('Error Saving Session: ', err)
203
- logging.exception(err, stack_info=True)
240
+ except Exception as err: # pylint: disable=W0703
241
+ self._logger.exception(err, stack_info=True)
204
242
  return False
205
243
 
206
244
  async def new_session(
@@ -210,53 +248,84 @@ class RedisStorage(AbstractStorage):
210
248
  response: web.StreamResponse = None
211
249
  ) -> SessionData:
212
250
  """Create a New Session Object for this User."""
213
- session_id = request.get(SESSION_KEY, None)
251
+ session_identity = request.get(SESSION_KEY, None)
252
+ session_id = request.get(SESSION_ID, None)
253
+ try:
254
+ conn = aioredis.Redis(connection_pool=self._redis)
255
+ except Exception as err:
256
+ self._logger.error(
257
+ f'Redis Storage: Error loading Redis Session: {err!s}'
258
+ )
259
+ raise RuntimeError(
260
+ f'Redis Storage: Error loading Redis Session: {err!s}'
261
+ ) from err
262
+ if not session_id:
263
+ session_id = await self.get_session_id(conn, session_identity)
214
264
  if not session_id:
215
265
  try:
216
- session_id = data[SESSION_KEY]
266
+ session_id = data[SESSION_ID]
217
267
  except KeyError:
218
268
  session_id = self.id_factory()
219
- logging.debug(f':::::: START CREATING A NEW SESSION FOR {session_id} ::::: ')
269
+ self._logger.debug(
270
+ f':::::: CREATING A NEW SESSION FOR {session_id} ::::: '
271
+ )
220
272
  if not data:
221
273
  data = {}
222
274
  # saving this new session on DB
223
275
  try:
224
- conn = aioredis.Redis(connection_pool=self._redis)
225
276
  t = time.time()
226
277
  data['created'] = t
227
278
  data['last_visit'] = t
228
- data["last_visited"] = f"Last visited: {t!s}"
229
- data[SESSION_KEY] = session_id
279
+ data[SESSION_KEY] = session_identity
280
+ data[SESSION_ID] = session_id
230
281
  dt = self._encoder(data)
282
+ _id_ = f'session:{session_id}'
231
283
  result = await conn.set(
232
- session_id, dt, self.max_age
284
+ _id_, dt, self.max_age
285
+ )
286
+ self._logger.debug(
287
+ f'Session Creation: {result}'
288
+ )
289
+ # Saving the Session ID on redis:
290
+ await conn.set(
291
+ f"user:{session_identity}",
292
+ session_id,
293
+ self.max_age
233
294
  )
234
- logging.info(f'Creation of New Session: {result}')
235
- except Exception as err: # pylint: disable=W0703
236
- logging.exception(err)
295
+ except Exception as err: # pylint: disable=W0703
296
+ self._logger.exception(err)
237
297
  return False
238
298
  try:
239
299
  session = SessionData(
240
- identity=session_id,
300
+ id=session_id,
301
+ identity=session_identity,
241
302
  data=data,
242
303
  new=True,
243
304
  max_age=self.max_age
244
305
  )
245
- if self.use_cookie is True and response is not None:
306
+ if self._use_cookies is True and response is not None:
246
307
  cookie_data = {
247
308
  "last_visit": t,
248
309
  "session_id": session_id
249
310
  }
311
+ # TODO: adding crypt
250
312
  cookie_data = self._encoder(cookie_data)
251
- self.save_cookie(response, cookie_data=cookie_data, max_age=self.max_age)
252
- except Exception as err: # pylint: disable=W0703
253
- print(err)
254
- logging.exception(f'Error creating Session Data: {err!s}')
313
+ self.save_cookie(
314
+ response,
315
+ cookie_data=cookie_data,
316
+ max_age=self.max_age
317
+ )
318
+ except Exception as err: # pylint: disable=W0703
319
+ self._logger.exception(
320
+ f'Error creating Session Data: {err!s}'
321
+ )
322
+ return False
255
323
  # Saving Session Object:
256
324
  ## add other options to session:
257
325
  self.session_info(session, request)
258
- session[SESSION_KEY] = session_id
326
+ session[SESSION_KEY] = session_identity
327
+ request[SESSION_ID] = session_id
259
328
  request[SESSION_OBJECT] = session
260
- request[SESSION_KEY] = session_id
261
- request["session"] = session
329
+ request[SESSION_KEY] = session_identity
330
+ request[SESSION_REQUEST_KEY] = session
262
331
  return session
@@ -5,7 +5,7 @@
5
5
  __title__ = 'navigator_session'
6
6
  __description__ = ('Navigator Session allows us to store user-specific data '
7
7
  'into session object.')
8
- __version__ = '0.4.2'
8
+ __version__ = '0.6.2'
9
9
  __author__ = 'Jesus Lara'
10
10
  __author_email__ = 'jesuslarag@gmail.com'
11
11
  __license__ = 'Apache-2.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: navigator-session
3
- Version: 0.4.2
3
+ Version: 0.6.2
4
4
  Summary: Navigator Session allows us to store user-specific data into session object.
5
5
  Home-page: https://github.com/phenobarbital/navigator-session
6
6
  Author: Jesus Lara
@@ -21,11 +21,18 @@ Classifier: Topic :: Database :: Front-Ends
21
21
  Classifier: Programming Language :: Python :: 3.9
22
22
  Classifier: Programming Language :: Python :: 3.10
23
23
  Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
24
25
  Classifier: Framework :: AsyncIO
25
26
  Classifier: Framework :: aiohttp
26
- Requires-Python: >=3.9.16
27
+ Requires-Python: >=3.9.13
27
28
  Description-Content-Type: text/markdown
28
29
  License-File: LICENSE
30
+ Requires-Dist: aiohttp>=3.9.5
31
+ Requires-Dist: asyncio==3.4.3
32
+ Requires-Dist: jsonpickle>=3.0.2
33
+ Requires-Dist: redis>=5.0.4
34
+ Requires-Dist: python_datamodel>=0.7.0
35
+ Requires-Dist: navconfig[default]>=1.7.0
29
36
 
30
37
  # Navigator_Session #
31
38
 
@@ -0,0 +1,6 @@
1
+ aiohttp>=3.9.5
2
+ asyncio==3.4.3
3
+ jsonpickle>=3.0.2
4
+ redis>=5.0.4
5
+ python_datamodel>=0.7.0
6
+ navconfig[default]>=1.7.0
@@ -1,8 +1,8 @@
1
1
  [build-system]
2
2
  requires = [
3
- 'setuptools==67.5.1',
4
- 'Cython==0.29.33',
5
- 'wheel==0.38.4'
3
+ 'setuptools==74.0.0',
4
+ 'Cython==3.0.11',
5
+ 'wheel==0.44.0'
6
6
  ]
7
7
 
8
8
  build-backend = "setuptools.build_meta"
@@ -20,6 +20,7 @@ classifiers=[
20
20
  "Programming Language :: Python :: 3.9",
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
23
24
  "Programming Language :: Python",
24
25
  "Typing :: Typed",
25
26
  "Environment :: Web Environment",
@@ -32,7 +33,7 @@ classifiers=[
32
33
  "License :: OSI Approved :: BSD License",
33
34
  ]
34
35
  description-file = "README.md"
35
- requires-python = ">=3.9.16"
36
+ requires-python = ">=3.9.13"
36
37
 
37
38
  [tool.pytest.ini_options]
38
39
  addopts = [
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python
2
2
  """Navigator-Session.
3
3
 
4
- Asynchronous library for managing user-specific data into a session object, used by Navigator.
4
+ Asynchronous library for managing user-specific data into a session object,
5
+ used by Navigator.
5
6
  See:
6
7
  https://github.com/phenobarbital/navigator-session
7
8
  """
@@ -32,7 +33,9 @@ with open(version, 'r', encoding='utf-8') as meta:
32
33
  '__title__',
33
34
  '__description__',
34
35
  '__author__',
35
- '__license__', '__author_email__'):
36
+ '__license__',
37
+ '__author_email__'
38
+ ):
36
39
  v = node.value
37
40
  if name.id == '__version__':
38
41
  __version__ = v.s
@@ -51,7 +54,7 @@ with open(version, 'r', encoding='utf-8') as meta:
51
54
  setup(
52
55
  name="navigator-session",
53
56
  version=__version__,
54
- python_requires=">=3.9.16",
57
+ python_requires=">=3.9.13",
55
58
  url="https://github.com/phenobarbital/navigator-session",
56
59
  description=__description__,
57
60
  keywords=['asyncio', 'session', 'aioredis', 'aiohttp'],
@@ -69,6 +72,7 @@ setup(
69
72
  "Programming Language :: Python :: 3.9",
70
73
  "Programming Language :: Python :: 3.10",
71
74
  "Programming Language :: Python :: 3.11",
75
+ "Programming Language :: Python :: 3.12",
72
76
  "Framework :: AsyncIO",
73
77
  "Framework :: aiohttp",
74
78
  ],
@@ -77,20 +81,17 @@ setup(
77
81
  packages=find_packages(exclude=["contrib", "docs", "tests"]),
78
82
  license=__license__,
79
83
  setup_requires=[
80
- "wheel==0.38.4",
81
- "Cython==0.29.33",
82
- "asyncio==3.4.3"
84
+ 'setuptools==74.0.0',
85
+ 'Cython==3.0.11',
86
+ 'wheel==0.44.0'
83
87
  ],
84
88
  install_requires=[
85
- "PyNaCl==1.5.0",
86
- "aiohttp==3.8.4",
87
- "uvloop==0.17.0",
89
+ "aiohttp>=3.9.5",
88
90
  "asyncio==3.4.3",
89
- "navconfig[default]>=1.0.15",
90
- "jsonpickle==3.0.1",
91
- "aioredis==2.0.1",
92
- "pendulum==2.1.2",
93
- "python_datamodel>=0.2.1"
91
+ "jsonpickle>=3.0.2",
92
+ "redis>=5.0.4",
93
+ "python_datamodel>=0.7.0",
94
+ "navconfig[default]>=1.7.0",
94
95
  ],
95
96
  project_urls={ # Optional
96
97
  "Source": "https://github.com/phenobarbital/navigator-session",
@@ -1,19 +0,0 @@
1
- aiounittest==1.4.2
2
- black==23.1.0
3
- codecov==2.1.12
4
- coverage[toml]==7.2.1
5
- flit==3.8.0
6
- hypothesis==6.68.2
7
- ipython==8.11.0
8
- lint==1.2.1
9
- mypy==1.1.1
10
- pylint==2.17.0
11
- sphinx==6.1.3
12
- pytest>=6.0.0
13
- pytest-asyncio==0.20.3
14
- pytest-cov==4.0.0
15
- pytest-cython==0.2.0
16
- pytest-xdist==3.2.0
17
- pytest-assume==2.4.3
18
- sphinx==6.1.3
19
- tox==4.4.6
@@ -1,6 +0,0 @@
1
- """NAV Session Storage Module."""
2
-
3
- # from .redis import RedisStorage
4
- from .abstract import SessionData
5
-
6
- __all__ = ('SessionData', )
@@ -1,9 +0,0 @@
1
- PyNaCl==1.5.0
2
- aiohttp==3.8.4
3
- uvloop==0.17.0
4
- asyncio==3.4.3
5
- navconfig[default]>=1.0.15
6
- jsonpickle==3.0.1
7
- aioredis==2.0.1
8
- pendulum==2.1.2
9
- python_datamodel>=0.2.1