From d34eba4d12f498e2cb1c9bfdac8d251e47b3d37d Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Sun, 23 Feb 2020 20:12:53 +0100 Subject: [PATCH 1/7] Add class documentation --- falcon_sqla/__init__.py | 4 +- falcon_sqla/manager.py | 83 +++++++++++++++++++++++++++++++++++++-- falcon_sqla/middleware.py | 12 ++++++ falcon_sqla/session.py | 6 +++ falcon_sqla/util.py | 10 +++++ 5 files changed, 110 insertions(+), 5 deletions(-) diff --git a/falcon_sqla/__init__.py b/falcon_sqla/__init__.py index 12b7693..462fb44 100644 --- a/falcon_sqla/__init__.py +++ b/falcon_sqla/__init__.py @@ -14,6 +14,6 @@ from .manager import Manager -__all__ = [ +__all__ = ( 'Manager', -] +) diff --git a/falcon_sqla/manager.py b/falcon_sqla/manager.py index 49b931d..a046b11 100644 --- a/falcon_sqla/manager.py +++ b/falcon_sqla/manager.py @@ -23,7 +23,23 @@ class Manager: - + """A manager for SQLAlchemy sessions. + + This manager allows registering multiple SQLAlchemy engines, specifying + if they are read-write or read-only or write-only capable. + + Args: + engine (Engine): An instance of a SQLAlchemy Engine, usually obtained + with its ``create_engine`` function. This engine is added as + read-write. + session_cls (type, optional): Session class used by this engine to + create the session. Should be a subclass of SQLAlchemy ``Session` + class. Defaults to :class:`.RequestSession`. + binds (dict, optional): A dictionary that allows specifying custom + binds on a per-entity basis in the session. See also + https://docs.sqlalchemy.org/en/13/orm/session_api.html#sqlalchemy.orm.session.Session.params.binds. + Defaults to None. + """ def __init__(self, engine, session_cls=RequestSession, binds=None): self._main_engine = engine self._engines = {engine: 'rw'} @@ -39,11 +55,23 @@ def __init__(self, engine, session_cls=RequestSession, binds=None): self.session_options = SessionOptions() def _filter_by_role(self, engines, role): + """Returns all the ``engines`` whose role is exactly ``role``. + + NOTE: if no engine with a role is found, all the engine are returned. + """ filtered = tuple(engine for engine in engines if self._engines.get(engine) == role) return filtered or engines def add_engine(self, engine, role='r'): + """Adds a new engine with the specified role. + + Args: + engine (Engine): An instance of a SQLAlchemy Engine. + role ({'r', 'rw', 'w'}, optional): The role of the engine + ('r': read-ony, 'rw': read-write, 'w': write-only). Defaults to + 'r'. + """ if role not in {'r', 'rw', 'w'}: raise ValueError("role must be one of ('r', 'rw', 'w')") @@ -67,7 +95,11 @@ def add_engine(self, engine, role='r'): self._session_kwargs = {'_manager_get_bind': self.get_bind} def get_bind(self, req, resp, session, mapper, clause): - """Choose the appropriate bind for the given request session.""" + """Choose the appropriate bind for the given request session. + + This method is not used directly, it's called by the session instance + if multiple engines are defined. + """ write = req.method not in self.session_options.safe_methods or ( self.session_options.write_engine_if_flushing and session._flushing) @@ -82,6 +114,7 @@ def get_bind(self, req, resp, session, mapper, clause): return random.choice(engines) def get_session(self, req=None, resp=None): + """Returns a new session object.""" if req and resp: return self._Session( info={'req': req, 'resp': resp}, **self._session_kwargs) @@ -90,10 +123,12 @@ def get_session(self, req=None, resp=None): @property def read_engines(self): + """A tuple of read capable engines.""" return self._read_engines @property def write_engines(self): + """A tuple of write capable engines.""" return self._write_engines @contextlib.contextmanager @@ -117,10 +152,42 @@ def session_scope(self, req=None, resp=None): @property def middleware(self): + """Returns a new :class:`Middleware` instance connected to this + manager.""" return Middleware(self) class SessionOptions: + """Defines a set of configurable options for the session. + + An instance of this class is exposed via :attr:`Manager.session_options`. + + Attributes: + no_session_methods (frozenset): HTTP methods that by default do not + require a DB session. Defaults to + :attr:`SessionOptions.NO_SESSION_METHODS`. + safe_methods (frozenset): HTTP methods that can use a read-only engine + since they do no alter the state of the db. Defaults to + :attr:`SessionOptions.SAFE_METHODS`. + read_from_rw_engines (bool): When True read operations are allowed from + read-write engines. Only used if more than one engine is defined + in the :class:`Manager`. Defaults to `True`. + write_to_rw_engines (bool): When True write operations are allowed from + read-write engines. Only used if more than one engine is defined + in the :class:`Manager`. Defaults to `True`. + write_engine_if_flushing (bool): When True a write engine is selected + if the session is in flushing state. Only used if more than one + engine is defined in the :class:`Manager`. Defaults to `True`. + sticky_binds (bool): When True the same engine will be used for each + db operation of a particular request. When False the engine will + be chosen randomly from the ones with the required capabilities. + Only used if more than one engine is defined in the + :class:`Manager`. Defaults to `False`. + request_id_func (callabe): A callable object that returns an unique + id for to each session. The returned object must be hashable. + Only used when :attr:`SessionOptions.sticky_binds` is True. + Defaults to ``uuid.uuid4``. + """ NO_SESSION_METHODS = frozenset(['OPTIONS', 'TRACE']) """HTTP methods that by default do not require a DB session.""" @@ -130,6 +197,16 @@ class SessionOptions: These methods are assumed to be fine with read-only replica engines. """ + __slots__ = ( + 'no_session_methods', + 'safe_methods', + 'read_from_rw_engines', + 'write_to_rw_engines', + 'write_engine_if_flushing', + 'sticky_binds', + 'request_id_func', + ) + def __init__(self): self.no_session_methods = self.NO_SESSION_METHODS self.safe_methods = self.SAFE_METHODS @@ -138,5 +215,5 @@ def __init__(self): self.write_to_rw_engines = True self.write_engine_if_flushing = True - self.request_id_func = uuid.uuid4 self.sticky_binds = False + self.request_id_func = uuid.uuid4 diff --git a/falcon_sqla/middleware.py b/falcon_sqla/middleware.py index fd4274c..17f087d 100644 --- a/falcon_sqla/middleware.py +++ b/falcon_sqla/middleware.py @@ -16,7 +16,11 @@ class Middleware: + """Falcon middleware that can be used with the session manage. + Args: + manager (Manager): Manager instance to use in this middleware. + """ def __init__(self, manager): self._manager = manager self._options = manager.session_options @@ -26,6 +30,9 @@ def process_request(self, req, resp): Set up the SQLAlchemy session for this request. The session object is stored as ``req.context.session``. + When the option :attr:`SessionOptions.sticky_binds` is set to True an + identification of the request is stored in ``req.context.request_id`` + (if not already present). """ if req.method not in self._options.no_session_methods: req.context.session = self._manager.get_session(req, resp) @@ -36,7 +43,12 @@ def process_request(self, req, resp): req.context.session = None def process_response(self, req, resp, resource, req_succeeded): + """ + Cleans up the session if one was provided. + Finalizes the session by calling commit if `req_succeeded` is True, + rollback otherwise. Finally it will close the session. + """ def cleanup(): # NOTE(vytas): Break circular references between the request and # the session. diff --git a/falcon_sqla/session.py b/falcon_sqla/session.py index 837338c..9f620b9 100644 --- a/falcon_sqla/session.py +++ b/falcon_sqla/session.py @@ -28,6 +28,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def get_bind(self, mapper=None, clause=None): + """ + Uses the manager to get the appropriate bind when ``_manager_get_bind`` + is defined. Otherwise the default logic is used. + + This method is called by SQLAlchemy. + """ if self._manager_get_bind: return self._manager_get_bind( session=self, mapper=mapper, clause=clause, **self.info) diff --git a/falcon_sqla/util.py b/falcon_sqla/util.py index 0095396..b4cc1f0 100644 --- a/falcon_sqla/util.py +++ b/falcon_sqla/util.py @@ -14,6 +14,16 @@ class ClosingStreamWrapper: + """Iterator that wraps a file-like stream with support for close(). + + This class is used to wrap WSGI response streams to provide a side effect + when the stream is closed + + Args: + stream (object): Readable file-like stream object. + close (callable): A callable object that is called before the stream + is closed. + """ def __init__(self, stream, close): self._stream = stream From e8cbd84233b322addd6f75b3179667f7061b68d2 Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Sun, 23 Feb 2020 20:49:31 +0100 Subject: [PATCH 2/7] Update util.py --- falcon_sqla/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon_sqla/util.py b/falcon_sqla/util.py index b4cc1f0..cfd4e64 100644 --- a/falcon_sqla/util.py +++ b/falcon_sqla/util.py @@ -17,7 +17,7 @@ class ClosingStreamWrapper: """Iterator that wraps a file-like stream with support for close(). This class is used to wrap WSGI response streams to provide a side effect - when the stream is closed + when the stream is closed. Args: stream (object): Readable file-like stream object. From d0526c72d73377222211c520ec9e9559d182ffbe Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Sun, 23 Feb 2020 21:19:54 +0100 Subject: [PATCH 3/7] Fix docs typo --- falcon_sqla/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon_sqla/manager.py b/falcon_sqla/manager.py index a046b11..2831c82 100644 --- a/falcon_sqla/manager.py +++ b/falcon_sqla/manager.py @@ -69,8 +69,8 @@ def add_engine(self, engine, role='r'): Args: engine (Engine): An instance of a SQLAlchemy Engine. role ({'r', 'rw', 'w'}, optional): The role of the engine - ('r': read-ony, 'rw': read-write, 'w': write-only). Defaults to - 'r'. + ('r': read-ony, 'rw': read-write, 'w': write-only). + Defaults to 'r'. """ if role not in {'r', 'rw', 'w'}: raise ValueError("role must be one of ('r', 'rw', 'w')") From d4a8840b83da83cf94130282811ab37db5b2d8ae Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Sat, 21 Mar 2020 10:08:59 +0100 Subject: [PATCH 4/7] Undo the __all__ type change --- falcon_sqla/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon_sqla/__init__.py b/falcon_sqla/__init__.py index 462fb44..12b7693 100644 --- a/falcon_sqla/__init__.py +++ b/falcon_sqla/__init__.py @@ -14,6 +14,6 @@ from .manager import Manager -__all__ = ( +__all__ = [ 'Manager', -) +] From 7796fa0074c832a8bfad62fd04f91b3db0dedd2e Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Sat, 21 Mar 2020 10:13:46 +0100 Subject: [PATCH 5/7] Cosmetic updates --- falcon_sqla/manager.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/falcon_sqla/manager.py b/falcon_sqla/manager.py index 2831c82..d5271d7 100644 --- a/falcon_sqla/manager.py +++ b/falcon_sqla/manager.py @@ -153,7 +153,8 @@ def session_scope(self, req=None, resp=None): @property def middleware(self): """Returns a new :class:`Middleware` instance connected to this - manager.""" + manager. + """ return Middleware(self) @@ -171,18 +172,18 @@ class SessionOptions: :attr:`SessionOptions.SAFE_METHODS`. read_from_rw_engines (bool): When True read operations are allowed from read-write engines. Only used if more than one engine is defined - in the :class:`Manager`. Defaults to `True`. + in the :class:`Manager`. Defaults to ``True``. write_to_rw_engines (bool): When True write operations are allowed from read-write engines. Only used if more than one engine is defined in the :class:`Manager`. Defaults to `True`. write_engine_if_flushing (bool): When True a write engine is selected if the session is in flushing state. Only used if more than one - engine is defined in the :class:`Manager`. Defaults to `True`. - sticky_binds (bool): When True the same engine will be used for each - db operation of a particular request. When False the engine will + engine is defined in the :class:`Manager`. Defaults to ``True``. + sticky_binds (bool): When ``True`` the same engine will be used for each + DB operation of a particular request. When ``False`` the engine will be chosen randomly from the ones with the required capabilities. Only used if more than one engine is defined in the - :class:`Manager`. Defaults to `False`. + :class:`Manager`. Defaults to ``False``. request_id_func (callabe): A callable object that returns an unique id for to each session. The returned object must be hashable. Only used when :attr:`SessionOptions.sticky_binds` is True. From bd18ed326edd4c907302854b54fe7e31ad168899 Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Sat, 21 Mar 2020 10:15:25 +0100 Subject: [PATCH 6/7] Update middleware.py --- falcon_sqla/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon_sqla/middleware.py b/falcon_sqla/middleware.py index 17f087d..f7a76b4 100644 --- a/falcon_sqla/middleware.py +++ b/falcon_sqla/middleware.py @@ -16,7 +16,7 @@ class Middleware: - """Falcon middleware that can be used with the session manage. + """Falcon middleware that can be used with the session manager. Args: manager (Manager): Manager instance to use in this middleware. From 146396a06d95c45e329d5b06c6b795c51623c705 Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Sat, 21 Mar 2020 10:16:11 +0100 Subject: [PATCH 7/7] Update manager.py --- falcon_sqla/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon_sqla/manager.py b/falcon_sqla/manager.py index d5271d7..a99c506 100644 --- a/falcon_sqla/manager.py +++ b/falcon_sqla/manager.py @@ -198,7 +198,7 @@ class SessionOptions: These methods are assumed to be fine with read-only replica engines. """ - __slots__ = ( + __slots__ = [ 'no_session_methods', 'safe_methods', 'read_from_rw_engines', @@ -206,7 +206,7 @@ class SessionOptions: 'write_engine_if_flushing', 'sticky_binds', 'request_id_func', - ) + ] def __init__(self): self.no_session_methods = self.NO_SESSION_METHODS