Repository: candango/firenado (language: python)

Issue:
Bump tornado to 6.2.0 and changed some deps versions to relative.

Get the isolation level from data source config
Handle the isolation level in the configuration while creating a sqlalchemy engine.

See:
 - https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#transaction-isolation-level
 - https://github.com/candango/firenado/commit/7a04810adfa16fb7ea10ec10d510c06e15afe67c
Set key dependencies relative to the minor version instead of a fixed version
To facilitate requirements integration between projects let's set dependencies relative to a minor instead setting fixed versions.

From some experience with automatoes, firenado, and peasant in the same project sometimes cartola will break pip if one project bumps before firenado or vice-versa.

Adopting that approach on the following packages:

 - cartola
 - hiredis
 - redis
 - sqlalchemy
 - pexpect

Bumping versions will occur when a minor or major emerges, but the minor will be the reference.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index b6a1cfd..24ad5d5 100644
--- a/README.md
+++ b/README.md
@@ -28,18 +28,55 @@ pip install firenado
 
 It is possible to install extra packages as redis-py, sqlalchemy and pexpect.
 
-To install only redis-py:
+Installing only redis-py:
 
 ```
 pip install firenado[redis]
 ```
 
-Complete installation:
+Installing only redis-py:
 
 ```
-pip install firenado[pexpect, redis, sqlalchemy]
+pip install firenado[sqlalchemy pexpect]
 ```
 
+Installing only redis (redis-py, hiredis):
+
+```
+pip install firenado[sqlalchemy pexpect]
+```
+
+Installing redis and schedule(croniter):
+
+```
+pip install firenado[redis schedule]
+```
+
+Complete installation(what it is being the case, everytime):
+
+```
+pip install firenado[all]
+```
+
+> In the future, the installation logic will be inverted. Redis and pexpect
+> will be added by default, and disabling them using optional parameters.
+>
+> The sqlalchemy and schedule(croniter) optionals will remain as is.
+>
+> With that change if you want just add schedule to the redis and pexpect:
+>
+> ``` pip install firenado[schedule] ```
+>
+> Maybe you want an agent with scheduled features and no redis:
+>
+> ``` pip install firenado[schedule noredis] ```
+>
+> Or don't need ProcessLaucher but sqlalchemy support:
+>
+> ``` pip install firenado[sqlalchemy nopexpect] ```
+>
+> See: #401
+
 ## Usage
 
 Creating and running a new application:
diff --git a/docs/conf.py b/docs/conf.py
index a3c1de0..f44e35b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -57,7 +57,7 @@
 # The short X.Y version.
 version = '0.2'
 # The full version, including alpha/beta/rc tags.
-release = '0.2.16'
+release = '0.2.17'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/releases.rst b/docs/releases.rst
index caa7517..23e6216 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -4,6 +4,7 @@ Release notes
 .. toctree::
    :maxdepth: 2
 
+   releases/v0.2.17
    releases/v0.2.16
    releases/v0.2.15
    releases/v0.2.14
diff --git a/docs/releases/v0.2.17.rst b/docs/releases/v0.2.17.rst
new file mode 100644
index 0000000..4f03e1a
--- /dev/null
+++ b/docs/releases/v0.2.17.rst
@@ -0,0 +1,13 @@
+What's new in Firenado 0.2.17
+=============================
+
+Apr 09, 2022
+------------
+
+We are pleased to announce the release of Firenado 0.2.17.
+
+This release is just upgrading cartola, redis and sqlalchemy versions.
+
+The new feature from cartola will open an space so we can start do create a
+more dynamic framework.
+
diff --git a/examples/launcher/app.py b/examples/launcher/app.py
index 5e598c7..386784e 100644
--- a/examples/launcher/app.py
+++ b/examples/launcher/app.py
@@ -1,7 +1,6 @@
 from . import handlers
 from firenado import tornadoweb
 from firenado.launcher import ProcessLauncher
-from tornado import gen
 import os
 
 
@@ -12,20 +11,27 @@ def __init__(self, name, application):
         self.launcher_path = os.path.abspath(os.path.dirname(__file__))
         self.charge_path = os.path.join(self.launcher_path, "charge")
         self.launcher = None
+        self.launcher_callback = None
 
     def get_handlers(self):
         return [
             (r'/', handlers.IndexHandler),
         ]
 
-    @gen.coroutine
     def initialize(self):
+        import tornado.ioloop
+        self.launcher_callback = tornado.ioloop.IOLoop.current().add_callback(
+            callback=self.launch_charge
+        )
+
+    async def launch_charge(self):
         import sys
+        os.chdir(self.charge_path)
         self.launcher = ProcessLauncher(dir=self.charge_path,
                                         logfile=sys.stderr)
         self.launcher.load()
-        yield self.launcher.launch()
+        await self.launcher.launch()
+        os.chdir(self.launcher_path)
 
-    @gen.coroutine
     def shutdown(self):
         self.launcher.shutdown()
diff --git a/firenado/__init__.py b/firenado/__init__.py
index d539bd1..172c2f3 100644
--- a/firenado/__init__.py
+++ b/firenado/__init__.py
@@ -18,7 +18,7 @@
 """The Firenado Framework"""
 
 __author__ = "Flávio Gonçalves Garcia <piraz@candango.org>"
-__version__ = (0, 2, 16)
+__version__ = (0, 2, 17)
 __licence__ = "Apache License V2.0"
 
 
diff --git a/firenado/bin/__init__.py b/firenado/bin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/firenado/config.py b/firenado/config.py
index 36fdc4e..d50c984 100644
--- a/firenado/config.py
+++ b/firenado/config.py
@@ -1,6 +1,6 @@
 # -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -204,7 +204,7 @@ def __init__(self):
                 if not os.path.exists(file):
                     logger.critical("The file %s doesn't exists. Please check"
                                     "your apps definition and inform a valid "
-                                    "file." % file)
+                                    "file.", file)
                     sysexits.exit_fatal(sysexits.EX_CONFIG)
                 file_app_config = load_yaml_file(file)
                 if "app" in file_app_config:
@@ -223,7 +223,7 @@ def __init__(self):
     if config.current_app_name in config.apps:
         config.app = config.apps[config.current_app_name].app
     else:
-        logger.critical("The application %s is not defined." %
+        logger.critical("The application %s is not defined.",
                         config.current_app_name)
         sysexits.exit_fatal(sysexits.EX_CONFIG)
 
@@ -246,8 +246,9 @@ def process_app_config_section(config, app_config):
         config.app['id'] = app_config['id']
     if 'login' in app_config:
         if 'urls' in app_config['login']:
-            for url in app_config['login']['urls']:
-                config.app['login']['urls'][url['name']] = url['value']
+            if app_config['login']['urls']:
+                for url in app_config['login']['urls']:
+                    config.app['login']['urls'][url['name']] = url['value']
     if 'pythonpath' in app_config:
         config.app['pythonpath'] = app_config['pythonpath']
     if 'port' in app_config:
@@ -430,6 +431,6 @@ def process_session_config_section(config, session_config):
             config.session['encoder'] = session_config['encoder']
         else:
             logger = logging.getLogger(__name__)
-            logger.critical("The session encoder \"{}\" is not defined."
-                            "".format(session_config['encoder']))
+            logger.critical("The session encoder \"%s\" is not defined.",
+                            session_config['encoder'])
             sysexits.exit_fatal(sysexits.EX_CONFIG)
diff --git a/firenado/data.py b/firenado/data.py
index 08d005d..ff102b1 100644
--- a/firenado/data.py
+++ b/firenado/data.py
@@ -172,7 +172,6 @@ def configure(self, name, conf):
             if conf['backend'] == 'mysql':
                 # Setting connection default connection timeout for mysql
                 # backends as suggested on http://bit.ly/2bvOLxs
-                # TODO: ignore this if pool_recycle is defined on conf
                 engine_params['pool_recycle'] = 3600
 
         if "future" in conf:
@@ -180,15 +179,20 @@ def configure(self, name, conf):
                 engine_params['future'] = True
 
         if "pool" in conf:
-            if "size" in conf['pool']:
-                engine_params['pool_size'] = conf['pool']['size']
-            if "max_overflow" in conf['pool']:
-                engine_params['max_overflow'] = conf['pool']['max_overflow']
             if "class" in conf['pool']:
                 engine_params['pool_class'] = conf['pool']['class']
                 if isinstance(engine_params['pool_class'], str):
                     engine_params['pool_class'] = config.get_from_string(
                         engine_params['pool_class'])
+            if "isolation_level" in conf['pool']:
+                engine_params['isolation_level'] = conf['pool'][
+                    'isolation_level']
+            if "max_overflow" in conf['pool']:
+                engine_params['max_overflow'] = conf['pool']['max_overflow']
+            if "pool_recycle" in conf['pool']:
+                engine_params['pool_recycle'] = conf['pool']['pool_recycle']
+            if "size" in conf['pool']:
+                engine_params['pool_size'] = conf['pool']['size']
 
         if "session" in conf:
             if "autoflush" in conf['session']:
@@ -204,11 +208,9 @@ def configure(self, name, conf):
                 self.__connection['session']['info'] = conf['session']['info']
 
         if "url" not in conf:
-            print(self.__connection)
             logger.error("It is not possible to create sqlalchemy engine for "
-                         "%s datasource. Configuration: %s." %
-                         (self.__name, conf))
-
+                         "%s datasource. Configuration: %s.", self.__name,
+                         conf)
         self.__engine = create_engine(conf['url'], **engine_params)
 
         @event.listens_for(self.__engine, "engine_connect")
@@ -271,8 +273,22 @@ def connect_engine(self):
             logger.fatal("Error trying to connect to database: %s", op_error)
             sys.exit(errno.ECONNREFUSED)
 
-    def get_a_session(self, autoflush=True, autocommit=False,
-                      expire_on_commit=True, info=None):
+    def get_a_session(self, **kwargs) -> "sqlalchemy.orm.Session":
+        """ Return a session bind to the datasource engine.
+
+        Default parameters based on: https://bit.ly/3MjWDzF
+        :param dict kwargs:
+        :key bool autoflush: Default to True
+        :key bool autocommit: Default to False
+        :key bool expire_on_commit: Default to False
+        :key dict info: Default to None
+        :return "sqlalchemy.orm.session.Session":
+        """
+        autoflush = kwargs.get("autoflush", True)
+        autocommit = kwargs.get("autocommit", False)
+        expire_on_commit = kwargs.get("expire_on_commit", True)
+        info = kwargs.get("info")
+
         from firenado.util.sqlalchemy_util import Session
         Session.configure(bind=self.__engine, autoflush=autoflush,
                           autocommit=autocommit,
@@ -361,7 +377,7 @@ def configure_data_sources(data_sources, data_connected):
     if isinstance(data_sources, str):
         if data_sources in firenado.conf.data['sources']:
             logger.debug("Found data source [%s] in the list. Preceding with "
-                         "the configuration process." % data_sources)
+                         "the configuration process.", data_sources)
             conf = firenado.conf.data['sources'][data_sources]
             data_source_instance = config_to_data_source(
                 data_sources, conf, data_connected)
@@ -371,7 +387,7 @@ def configure_data_sources(data_sources, data_connected):
                          "available data sources. Please fix the firenado "
                          "configuration file. Sometimes that could be only a "
                          "typo in one of app data sources to be created. Look "
-                         "at app.data.sources list." % data_sources)
+                         "at app.data.sources list.", data_sources)
             sys.exit(errno.ENOKEY)
         # TODO: Testing the connection
         # Without that the error will just happen during the handler execution
diff --git a/firenado/launcher.py b/firenado/launcher.py
index 7fa12fa..2658912 100644
--- a/firenado/launcher.py
+++ b/firenado/launcher.py
@@ -1,6 +1,6 @@
 # -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
 import logging
 import os
 import sys
-from tornado import gen
+import warnings
 
 logger = logging.getLogger(__name__)
 
@@ -70,10 +70,13 @@ def __init__(self, **settings):
                             format=firenado.conf.log['format'])
 
     def load(self):
-        return None
+        raise NotImplementedError()
 
     def launch(self):
-        return None
+        raise NotImplementedError()
+
+    def shutdown(self):
+        raise NotImplementedError()
 
 
 class ProcessLauncher(FirenadoLauncher):
@@ -86,9 +89,10 @@ class ProcessLauncher(FirenadoLauncher):
                      "ProcessLauncher.")
 
     def __init__(self, **settings):
-        super(ProcessLauncher, self).__init__(**settings)
+        super().__init__(**settings)
         self.process = None
         self.process_callback = None
+        self.process_callback_time = 100
         self.logfile = settings.get("logfile", None)
         self.command = None
         self.response = None
@@ -112,41 +116,42 @@ def load(self):
             socket_parameter = "--port=%s" % self.socket
             self.command = "%s %s" % (self.command, socket_parameter)
 
-    @gen.coroutine
-    def read_process(self):
+    async def read_process(self):
         import pexpect
         self.process_callback.stop()
         try:
             # Simple way to catch everything is wait for a new line:
             #
-            yield self.process.expect("\n", async_=True)
+            await self.process.expect("\n", async_=True)
         except pexpect.TIMEOUT:
-            logger.warning("Reached timeout")
-            pass
+            logger.debug("Reached timeout, getting back in the loop.")
         self.process_callback.start()
 
-    @gen.coroutine
-    def launch(self):
-        import pexpect
-        import tornado.ioloop
-        logger.info("Launching %s" % self.command)
-        parameters = {
-            'command': self.command,
-            'encoding': "utf-8"
-        }
-        if self.logfile is not None:
-            parameters['logfile'] = self.logfile
-        self.process = pexpect.spawn(**parameters)
-        yield self.process.expect(
-            [r"[Firenado server started successfully].*"], async_=True)
-        self.process_callback = tornado.ioloop.PeriodicCallback(
-            self.read_process,
-            400
-        )
-        self.process_callback.start()
+    async def launch(self):
+        with warnings.catch_warnings():
+            import pexpect
+            import tornado.ioloop
+            logger.info("Launching %s", self.command)
+            parameters = {
+                'command': self.command,
+                'encoding': "utf-8"
+            }
+            if self.dir:
+                parameters['cwd'] = self.dir
+            if self.logfile is not None:
+                parameters['logfile'] = self.logfile
+            self.process = pexpect.spawn(**parameters)
+            warnings.simplefilter("ignore")
+            await self.process.expect(
+                [r"[Firenado server started successfully].*"], async_=True)
+            self.process_callback = tornado.ioloop.PeriodicCallback(
+                self.read_process,
+                self.process_callback_time
+            )
+            self.process_callback.start()
 
     def send(self, line):
-        logger.info("Sending line {}".format(line))
+        logger.info("Sending line %s", line)
         self.process.sendline(line)
 
     def shutdown(self):
@@ -190,14 +195,14 @@ def launch(self):
         self.http_server = tornado.httpserver.HTTPServer(self.application)
         if firenado.conf.app['xheaders'] is not None and type(
                 firenado.conf.app['xheaders']) == bool:
-            logger.debug("Setting http server xheaders as %s." %
+            logger.debug("Setting http server xheaders as %s.",
                          firenado.conf.app['xheaders'])
             self.http_server.xheaders = firenado.conf.app['xheaders']
         if firenado.conf.app['xheaders'] is not None and type(
                 firenado.conf.app['xheaders']) != bool:
             logger.warning("The xheaders defined in the application section"
                            "must be bool instead of %s. Ignoring the "
-                           "configuration item." %
+                           "configuration item.",
                            type(firenado.conf.app['xheaders']).__name__)
         listening_count = 0
         listening_what = "socket"
@@ -208,7 +213,7 @@ def launch(self):
                 socket_path = self.socket
             socket = bind_unix_socket(socket_path)
             self.http_server.add_socket(socket)
-            logger.info("Firenado listening at socket ""%s" %
+            logger.info("Firenado listening at socket %s",
                         socket.getsockname())
             listening_count += 1
         else:
@@ -224,7 +229,7 @@ def launch(self):
             if listening_count > 1:
                 listening_what = "%ss" % listening_what
             logger.info("Firenado server started successfully. Listening at %s"
-                        " %s." % (listening_count, listening_what))
+                        " %s.", listening_count, listening_what)
             if firenado.conf.app['process']['num_processes'] is not None:
                 import tornado.process
                 num_processes = firenado.conf.app['process']['num_processes']
@@ -234,7 +239,7 @@ def launch(self):
                     num_processes_alert = ("0 (assuming %s as cpu count)" %
                                            tornado.process.cpu_count())
                 logger.info("Tornado set to start %s processes with %s max "
-                            "restarts." % (num_processes_alert, max_restarts))
+                            "restarts.", num_processes_alert, max_restarts)
                 tornado.process.fork_processes(num_processes, max_restarts)
             tornado.ioloop.IOLoop.current().start()
         else:
@@ -251,11 +256,10 @@ def sig_handler(self, sig, _):
         tid = task_id()
         pid = os.getpid()
         if tid is None:
-            logger.warning("main process (pid %s) caught signal: %s" %
-                           (pid, sig))
+            logger.warning("main process (pid %s) caught signal: %s", pid, sig)
         else:
-            logger.warning("child %s (pid %s) caught signal: %s" %
-                           (tid, pid, sig))
+            logger.warning("child %s (pid %s) caught signal: %s", tid, pid,
+                           sig)
         tornado.ioloop.IOLoop.current().add_callback_from_signal(self.shutdown)
 
     def add_sockets(self, port, address=None):
@@ -268,20 +272,18 @@ def add_sockets(self, port, address=None):
                 sockets = bind_sockets(port)
                 address = "127.0.0.1"
             self.http_server.add_sockets(sockets)
-            logger.info("Firenado listening at http://%s:%s." %
-                        (address.strip(), port))
+            logger.info("Firenado listening at http://%s:%s.", address.strip(),
+                        port)
             return True
         except gaierror as error:
-            logger.error("Firenado failed to listen at http://%s:%s."
-                         % (address.strip(), port))
+            logger.error("Firenado failed to listen at http://%s:%s.",
+                         address.strip(), port)
         except OSError as error:
-            logger.error("Firenado failed to listen at http://%s:%s. "
-                         "Check if another process is listening at "
-                         "this port or if you provided a dns name and"
-                         "the correspondent ip in the addresses"
-                         "configuration or if the machine owns the ip "
-                         "Fireando will start to listen." %
-                         (address, port))
+            logger.error("Firenado failed to listen at http://%s:%s. Check if "
+                         "another process is listening at this port or if you "
+                         "provided a dns name and the correspondent ip in the "
+                         "addresses configuration or if the machine owns the "
+                         "ip Fireando will start to listen.", address, port)
         return False
 
     def shutdown(self):
@@ -291,9 +293,9 @@ def shutdown(self):
         tid = task_id()
         pid = os.getpid()
         if tid is None:
-            logger.info("main process (pid %s): stopping http server" % pid)
+            logger.info("main process (pid %s): stopping http server.", pid)
         else:
-            logger.info("child %s (pid %s): stopping http server" % (tid, pid))
+            logger.info("child %s (pid %s): stopping http server.", tid, pid)
         for key, component in self.application.components.items():
             component.shutdown()
         self.http_server.stop()
@@ -303,18 +305,16 @@ def shutdown(self):
         if self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN == 0:
             io_loop.stop()
             if tid is None:
-                logger.info("main process (pid %s): application is down" % pid)
+                logger.info("main process (pid %s): application is down", pid)
             else:
-                logger.info("child %s (pid %s): application is down" %
-                            (tid, pid))
+                logger.info("child %s (pid %s): application is down", tid, pid)
         else:
             if tid is None:
                 logger.info("main process (pid %s): shutdown in %s seconds "
-                            "..." %
-                            (pid, self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN))
+                            "...", pid, self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN)
             else:
-                logger.info("child %s (pid %s): shutdown in %s seconds ..." %
-                            (tid, pid, self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN))
+                logger.info("child %s (pid %s): shutdown in %s seconds ...",
+                            tid, pid, self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN)
             deadline = time.time() + self.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN
 
             def stop_loop():
@@ -325,8 +325,8 @@ def stop_loop():
                     io_loop.stop()
                     if tid is None:
                         logger.info("main process (pid %s): application is "
-                                    "down" % pid)
+                                    "down", pid)
                     else:
-                        logger.info("child %s (pid %s): application is down" %
-                                    (tid, pid))
+                        logger.info("child %s (pid %s): application is down",
+                                    tid, pid)
             stop_loop()
diff --git a/firenado/management/management.py b/firenado/management/management.py
index 6a273bb..ac456ee 100644
--- a/firenado/management/management.py
+++ b/firenado/management/management.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -34,11 +34,10 @@ def run_from_command_line():
     """ Run Firenado's management commands from a command line
     """
     for commands_conf in firenado.conf.management['commands']:
-        logger.debug("Loading %s commands from %s." % (
-            commands_conf['name'],
-            commands_conf['module']
-        ))
-        exec('import %s' % commands_conf['module'])
+        logger.debug("Loading %s commands from %s.", commands_conf['name'],
+                     commands_conf['module'])
+        # from https://stackoverflow.com/a/19221024/2887989
+        __import__(commands_conf['module'])
     command_index = 1
     for arg in sys.argv[1:]:
         command_index += 1
@@ -54,6 +53,7 @@ def run_from_command_line():
         if not command_exists(namespace.command):
             show_command_line_usage(parser)
         else:
+
             run_command(namespace.command, sys.argv[command_index-1:])
     except FirenadoArgumentError:
         show_command_line_usage(parser, True)
diff --git a/firenado/schedule.py b/firenado/schedule.py
index 273facc..dd3ab33 100644
--- a/firenado/schedule.py
+++ b/firenado/schedule.py
@@ -99,8 +99,8 @@ def can_run(self) -> bool:
         return self._can_run
 
     def add_job(self, job):
-        logger.debug("Adding job %s into the scheduler [id: %s, name: %s]." %
-                     (job.id, self.id, self.name))
+        logger.debug("Adding job %s into the scheduler [id: %s, name: %s].",
+                     job.id, self.id, self.name)
         self._jobs[job.id] = job
 
     def get_job(self, job_id):
@@ -109,15 +109,15 @@ def get_job(self, job_id):
     def initialize(self, **kwargs):
         self._id = kwargs.get("id")
         self._name = kwargs.get("name")
-        logger.debug("Initializing scheduler [id: %s, name: %s]." % (
-            self.id, self.name))
+        logger.debug("Initializing scheduler [id: %s, name: %s].", self.id,
+                     self.name)
 
     def load_jobs(self):
         raise NotImplementedError
 
     def remove_job(self, job_id):
-        logger.debug("Removing job %s from the scheduler [id: %s, name: %s]."
-                     % (job_id, self.id, self.name))
+        logger.debug("Removing job %s from the scheduler [id: %s, name: %s].",
+                     job_id, self.id, self.name)
         job = self.get_job(job_id)
         if job is None:
             return None
@@ -126,38 +126,36 @@ def remove_job(self, job_id):
 
     def run(self):
         self.load_jobs()
-        logger.debug("Scheduler [id: %s, name: %s] interval set to %sms." %
-                     (self.id, self.name, self._interval))
+        logger.debug("Scheduler [id: %s, name: %s] interval set to %sms.",
+                     self.id, self.name, self._interval)
         self._periodic_callback = tornado.ioloop.PeriodicCallback(
             self._manage_jobs, self._interval)
         self._periodic_callback.start()
 
     def _manage_jobs(self):
-        logger.debug("Scheduler [id: %s, name: %s] managing jobs." %
-                     (self.id, self.name))
+        logger.debug("Scheduler [id: %s, name: %s] managing jobs.", self.id,
+                     self.name)
         logger.debug("Scheduler [id: %s, name: %s] stopping periodic callback."
-                     % (self.id, self.name))
+                     , self.id, self.name)
         self._periodic_callback.stop()
         for job in self.jobs:
             if not job.already_scheduled:
                 logger.debug("Job %s from Scheduler [id: %s, name: %s] isn't "
-                             "scheduled yet." % (job.hard_id, self.id,
-                                                 self.name))
+                             "scheduled yet.", job.hard_id, self.id, self.name)
                 if job.must_schedule:
-                    logger.debug(
-                        "Job %s from Scheduler [id: %s, name: %s] must be "
-                        "scheduled to run at %s." % (
-                            job.hard_id, self.id, self.name, job.next_run))
+                    logger.debug("Job %s from Scheduler [id: %s, name: %s] "
+                                 "must be scheduled to run at %s.",
+                                 job.hard_id, self.id, self.name, job.next_run)
                     job.schedule()
             else:
-                logger.debug("Job %s from Scheduler [id: %s, name: %s] already"
-                             "scheduled." % (job.hard_id, self.id,
-                                             self.name))
+                logger.debug("Job %s from Scheduler [id: %s, name: %s] "
+                             "already scheduled.", job.hard_id, self.id,
+                             self.name)
 
-        logger.debug("Scheduler [id: %s, name: %s] ending of managing jobs." %
-                     (self.id, self.name))
+        logger.debug("Scheduler [id: %s, name: %s] ending of managing jobs.",
+                     self.id, self.name)
         logger.debug("Scheduler [id: %s, name: %s] starting periodic callback."
-                     % (self.id, self.name))
+                     , self.id, self.name)
         self._periodic_callback.start()
 
 
@@ -184,10 +182,10 @@ def load_jobs(self):
                                    "in the file:\n        %s\n    Job id: %s"
                                    "\n    Scheduler: [id: %s, name: %s]"
                                    "\n        Config: %s\n"
-                                   "\n    Please fix this issue.\n" %
-                                   (job_conf['class'],
-                                    self.component.get_complete_config_file(),
-                                    job_id, self.id, self.name, job_conf))
+                                   "\n    Please fix this issue.\n",
+                                   job_conf['class'],
+                                   self.component.get_complete_config_file(),
+                                   job_id, self.id, self.name, job_conf)
                 else:
                     if job_id is None:
                         logger.warning(
@@ -209,9 +207,9 @@ def load_jobs(self):
                                 "  Job id: %s\n    Scheduler: [id: %s, "
                                 "name: %s]\n        Config: %s\n"
                                 "\n    Please define either a cron string or "
-                                "date.\n" %
-                                (self.component.get_complete_config_file(),
-                                 job_id, self.id, self.name, job_conf))
+                                "date.\n",
+                                self.component.get_complete_config_file(),
+                                job_id, self.id, self.name, job_conf)
                         else:
                             self.add_job(job_resolved_class(self, **job_conf))
 
@@ -224,12 +222,12 @@ def initialize(self):
         if "id" not in self._conf:
             logger.warning("It is not possible to initialize the scheduler "
                            "because id is missing in the configuration "
-                           "provided.\n Config: %s" % self._conf)
+                           "provided.\n Config: %s", self._conf)
             has_error = True
         if "name" not in self._conf:
             logger.warning("It is not possible to initialize the scheduler "
                            "because name is missing in the configuration "
-                           "provided.\n Config: %s" % self._conf)
+                           "provided.\n Config: %s", self._conf)
             has_error = True
 
         if not has_error:
@@ -304,10 +302,9 @@ def must_schedule(self):
     def schedule(self):
         next_interval = self.next_interval
         logger.debug("Job %s from Scheduler [id: %s, name: %s] scheduled to "
-                     "run at next interval of %sms." % (self.hard_id,
-                                                        self._scheduler.id,
-                                                        self._scheduler.name,
-                                                        self.next_interval))
+                     "run at next interval of %sms.", self.hard_id,
+                     self._scheduler.id, self._scheduler.name,
+                     self.next_interval)
         self._periodic_callback = tornado.ioloop.PeriodicCallback(
             self._run_job, next_interval)
         self._periodic_callback.start()
@@ -317,26 +314,23 @@ async def _run_job(self):
         :return None:
         """
         self._periodic_callback.stop()
-        logger.debug(
-            "Running job %s from Scheduler [id: %s, name: %s]." % (
-                self.hard_id, self._scheduler.id, self._scheduler.name))
+        logger.debug("Running job %s from Scheduler [id: %s, name: %s].",
+                     self.hard_id, self._scheduler.id, self._scheduler.name)
         try:
             future = self.run()
             if future:
-                logger.debug(
-                    "Running job %s from Scheduler [id: %s, name: %s] "
-                    "asynchronously. " % (self.hard_id, self._scheduler.id,
-                                          self._scheduler.name))
+                logger.debug("Running job %s from Scheduler [id: %s, name: "
+                             "%s] asynchronously.", self.hard_id,
+                             self._scheduler.id, self._scheduler.name)
                 await future
         except:
             logger.error("A non handled exception was cough while running the "
-                         "job %s:" % self.hard_id,
+                         "job %s:", self.hard_id,
                          exc_info=exception.full_exc_info())
             logger.error("Please handle the exception to fix the job "
                          "execution and avoid breaking the scheduler.")
-        logger.debug(
-            "Job %s removed from Scheduler [id: %s, name: %s]" % (
-                self.hard_id, self._scheduler.id, self._scheduler.name))
+        logger.debug("Job %s removed from Scheduler [id: %s, name: %s]",
+                     self.hard_id, self._scheduler.id, self._scheduler.name)
         self._periodic_callback = None
         return
 
@@ -371,19 +365,18 @@ def get_scheduler(self, name):
     def _config_schedule_component(self):
         if "interval" in self.schedule_conf():
             self._interval = self.conf['interval']
-        logger.debug("Scheduled component %s interval set to %sms." %
-                     (self.name, self._interval))
+        logger.debug("Scheduled component %s interval set to %sms.", self.name,
+                     self._interval)
         if "schedulers" in self.schedule_conf():
-            logger.debug("Initializing %s schedulers." % self.name)
+            logger.debug("Initializing %s schedulers.", self.name)
             self._initialize_schedulers()
         else:
             logger.warning("No scheduler was defined in the scheduled "
                            "component %s.\n\n%s\n* It is necessary either "
                            "define schedulers or scheduler loaders in the \n*"
                            " %s config file. \n* %s-- Firenado scheduled "
-                           "component\n%s\n" %
-                           (self.name, "*" * 70,
-                            self.get_config_file(), " " * 37, "*" * 70))
+                           "component\n%s\n", self.name, "*" * 70,
+                           self.get_config_file(), " " * 37, "*" * 70)
 
     def _initialize_schedulers(self):
         for scheduler_conf in self.conf['schedulers']:
@@ -400,7 +393,7 @@ def schedule_conf(self):
     def initialize(self):
         if self.has_conf:
             logger.debug("Configuration file found. Starting scheduled "
-                         "component %s initialization." % self.name)
+                         "component %s initialization.", self.name)
             self._config_schedule_component()
         else:
             logger.warning("No configuration file was found. Scheduled"
diff --git a/firenado/service.py b/firenado/service.py
index 5ecae60..b1a61ee 100644
--- a/firenado/service.py
+++ b/firenado/service.py
@@ -22,7 +22,7 @@
 logger = logging.getLogger(__name__)
 
 
-class FirenadoService(object):
+class FirenadoService:
     """ Base class to handle services. A Firenado service is usually connected
     to a handler or a service.
     The developer can add extra configuration using the configuration_service
@@ -155,14 +155,14 @@ def wrapper(self, *method_args, **method_kwargs):
                         session = data_source.session
                     method_kwargs['session'] = session
                 except KeyError:
-                    logger.exception("There is no datasource defined with"
-                                     "index \"%s\" related to the service." %
+                    logger.exception("There is no datasource defined with "
+                                     "index \"%s\" related to the service.",
                                      data_source)
             result = method(self, *method_args, **method_kwargs)
             if close:
                 if not session:
                     logger.warning("No session was resolved.")
-                logger.debug("Closing session %s." % session)
+                logger.debug("Closing session %s.", session)
                 session.close()
             return result
         return wrapper
diff --git a/firenado/session.py b/firenado/session.py
index e9ba5df..211a07f 100644
--- a/firenado/session.py
+++ b/firenado/session.py
@@ -37,7 +37,6 @@ def __init__(self, session_aware_instance):
         self.session_callback = None
         self.callback_time = None
         self.callback_hiccup = firenado.conf.session['callback_hiccup'] * 1000
-
         # TODO: By the way session could be disabled. How to handle that?
         # TODO: check if session type exists. Maybe disable it if type is not
         # defined. We need to inform the error here
@@ -73,7 +72,7 @@ def __init__(self, session_aware_instance):
             )
             self.session_encoder = encoder_class()
 
-    def get_session(self, request_handler):
+    async def get_session(self, request_handler):
         """Returns a valid session object. This session is handler by the
         session handler defined on the application configuration. """
         if firenado.conf.session['enabled']:
@@ -98,7 +97,7 @@ def get_session(self, request_handler):
                 session = Session(self, session_data, session_id)
             return session
 
-    def store_session(self, request_handler):
+    async def store_session(self, request_handler):
         """Sends the session data to be stored by the session handler defined
         on the application configuration. """
         if firenado.conf.session['enabled']:
@@ -110,15 +109,17 @@ def store_session(self, request_handler):
                 if not request_handler.session.is_destroyed():
                     encoded_session_data = self.encode_session_data(
                         request_handler.session.get_data())
+                    params = request_handler.session.get_params
                     if request_handler.session.is_changed():
                         self.session_handler.write_stored_session(
                             session_id,
-                            encoded_session_data
+                            encoded_session_data,
+                            **params
                         )
                 else:
                     # Generating a new session
                     logger.debug("Dispatching session %s destruction to the "
-                                 "session handler." % session_id)
+                                 "session handler.", session_id)
                     self.session_handler.destroy_stored_session(session_id)
 
     def encode_session_data(self, data):
@@ -132,11 +133,11 @@ def get_session_aware_instance(self):
 
     def set_purge_normal(self):
         self.session_callback.callback_time = self.callback_time
-        logger.debug("No hiccups. Callback in %sms." % self.callback_time)
+        logger.debug("No hiccups. Callback in %sms.", self.callback_time)
 
     def set_purge_hiccup(self):
         self.session_callback.callback_time = self.callback_hiccup
-        logger.warning("Purge hiccup in %sms." % self.callback_hiccup)
+        logger.warning("Purge hiccup in %sms.", self.callback_hiccup)
 
     def __renew_session(self, request_handler):
         if firenado.conf.session['enabled']:
@@ -178,6 +179,7 @@ def __init__(self, engine, data=None, sess_id=None):
         self.__engine = engine
         self.id = sess_id
         self.__data = {} if data is None else data
+        self.__params = {}
         self.__destroyed = False
         self.__changed = False
 
@@ -206,6 +208,10 @@ def get_data(self):
         self.__lock_if_destroyed()
         return self.__data.copy()
 
+    def get_params(self):
+        self.__lock_if_destroyed()
+        return self.__params.copy()
+
     def has(self, key):
         self.__lock_if_destroyed()
         """ Returns if session data has some data stored by a given key. """
@@ -229,42 +235,43 @@ def __lock_if_destroyed(self):
         if self.is_destroyed():
             raise SessionDestroyedError()
 
-    def set(self, key, value):
+    def set(self, key, value, **kwargs):
         """ Set a value into the session data labeled by a given key """
         self.__lock_if_destroyed()
         self.__data[key] = value
+        self.__params[key] = kwargs
         self.__changed = True
 
 
 class SessionDestroyedError(tornado.web.HTTPError):
 
-    def __init__(self, status_code, log_message=None, *args, **kwargs):
-        message = "The session was destroyed. It is necessary a renew the " \
-                  "session before use it again."
+    def __init__(self, status_code=500, log_message=None, *args, **kwargs):
+        message = ("The session is already destroyed. It is necessary to renew"
+                   " the session before using it again.")
         super(SessionDestroyedError, self).__init__(
-            505, message, *args, **kwargs)
+            status_code, message, *args, **kwargs)
 
 
-class SessionHandler(object):
+class SessionHandler:
 
     def __init__(self, engine):
         self.engine = engine
         self.settings = {}
 
     def create_session(self, session_id, data):
-        pass
+        raise NotImplementedError
 
     def read_stored_session(self, session_id):
-        pass
+        raise NotImplementedError
 
-    def write_stored_session(self, session_id, data):
-        pass
+    def write_stored_session(self, session_id, data, **kwargs):
+        raise NotImplementedError
 
     def destroy_stored_session(self, session_id):
-        pass
+        raise NotImplementedError
 
     def purge_expired_sessions(self):
-        pass
+        raise NotImplementedError
 
     @staticmethod
     def create_session_id_cookie(request_handler):
@@ -338,9 +345,9 @@ def create_session_engine(obj, session_engine_attribute, session_engine_class):
 
 def read(method):
     @functools.wraps(method)
-    def wrapper(self, *args, **kwargs):
+    async def wrapper(self, *args, **kwargs):
         if firenado.conf.session['enabled']:
-            session = self.application.session_engine.get_session(self)
+            session = await self.application.session_engine.get_session(self)
             self.session = session
             logging.debug("Reading session %s." % self.session.id)
         return method(self, *args, **kwargs)
@@ -350,6 +357,10 @@ def wrapper(self, *args, **kwargs):
 def write(method):
     @functools.wraps(method)
     def wrapper(self, *args, **kwargs):
+        async def write_session_callback():
+            logging.debug("Writing session %s." % self.session.id)
+            await self.application.session_engine.store_session(self)
+
         retval = method(self, *args, **kwargs)
         if firenado.conf.session['enabled']:
             if self.session is None:
@@ -358,8 +369,9 @@ def wrapper(self, *args, **kwargs):
                               " current handler status is: %s." %
                               self.get_status())
             else:
-                logging.debug("Writing session %s." % self.session.id)
-                self.application.session_engine.store_session(self)
+                tornado.ioloop.IOLoop.current().add_callback(
+                    callback=write_session_callback
+                )
         return retval
     return wrapper
 
@@ -399,7 +411,7 @@ def read_stored_session(self, session_id):
         timed_data = fs.read(session_file)
         return binascii.unhexlify(timed_data.split("--")[0])
 
-    def write_stored_session(self, session_id, data):
+    def write_stored_session(self, session_id, data, **kwargs):
         import binascii
         import time
         timed_data = "%s--%s" % (binascii.hexlify(data).decode("ascii"),
@@ -408,14 +420,14 @@ def write_stored_session(self, session_id, data):
         fs.write(session_file, timed_data)
 
     def destroy_stored_session(self, session_id):
-        logger.debug("Destroying session %s." % session_id)
+        logger.debug("Destroying session %s.", session_id)
         try:
             session_file = os.path.join(
                 self.path,
                 self.__get_filename(session_id)
             )
             os.remove(session_file)
-            logger.debug("Session %s destroyed." % session_id)
+            logger.debug("Session %s destroyed.", session_id)
         except OSError:
             # TODO Why we are deleting the session file twice?
             pass
@@ -430,8 +442,7 @@ def purge_expired_sessions(self):
         """
         logger.debug("File handler looking for expired sessions.")
         self.engine.session_callback.stop()
-        logging.debug("Session periodic callback stopped by the file "
-                      "handler.")
+        logger.debug("Session periodic callback stopped by the file handler.")
         purge_count = 0
         purge_hiccup = False
         for dirname, dirnames, filenames in os.walk(self.path):
@@ -448,10 +459,8 @@ def purge_expired_sessions(self):
                     purge_count += 1
             if purge_count == firenado.conf.session['purge_limit']:
                 purge_hiccup = True
-                logger.warning(
-                    "Expired 500 sessions. Exiting the call and waiting for "
-                    "purge hiccup."
-                )
+                logger.warning("Expired 500 sessions. Exiting the call and "
+                               "waiting for purge hiccup.")
                 break
         if purge_hiccup:
             self.engine.set_purge_hiccup()
@@ -493,16 +502,16 @@ def read_stored_session(self, session_id):
         self.data_source.get_connection().expire(key, self.life_time)
         return self.data_source.get_connection().get(key)
 
-    def write_stored_session(self, session_id, data):
+    def write_stored_session(self, session_id, data, **kwargs):
         key = self.__get_key(session_id)
         self.data_source.get_connection().set(key, data)
         self.data_source.get_connection().expire(key, self.life_time)
 
     def destroy_stored_session(self, session_id):
-        logger.debug("Destroying session %s." % session_id)
+        logger.debug("Destroying session %s.", session_id)
         key = self.__get_key(session_id)
         self.data_source.get_connection().delete(key)
-        logger.debug("Session %s destroyed." % session_id)
+        logger.debug("Session %s destroyed.", session_id)
 
     def purge_expired_sessions(self):
         """ On Redis we don't destroy expired sessions per-se.
@@ -518,17 +527,14 @@ def purge_expired_sessions(self):
         for key in keys:
             ttl = self.data_source.get_connection().ttl(key)
             if ttl == -1:
-                logger.warning(
-                    "Session %s without ttl. Setting expiration now." % key
-                )
+                logger.warning("Session %s without ttl. Setting expiration "
+                               "now.", key)
                 self.data_source.get_connection().expire(key, self.life_time)
                 purge_count += 1
                 if purge_count == firenado.conf.session['purge_limit']:
                     purge_hiccup = True
-                    logger.warning(
-                        "Set ttl to 500 sessions. Exiting the call and waiting"
-                        " for purge hiccup."
-                    )
+                    logger.warning("Set ttl to 500 sessions. Exiting the call"
+                                   " and waiting for purge hiccup.")
                     break
         if purge_hiccup:
             self.engine.set_purge_hiccup()
diff --git a/firenado/tornadoweb.py b/firenado/tornadoweb.py
index a3ef91a..f85525f 100644
--- a/firenado/tornadoweb.py
+++ b/firenado/tornadoweb.py
@@ -1,6 +1,6 @@
 # -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -93,7 +93,7 @@ class TornadoApplication(tornado.web.Application, data.DataConnectedMixin,
     """
 
     def __init__(self, default_host="", transforms=None, **settings):
-        logger.debug('Wiring application located at %s.' %
+        logger.debug("Wiring application located at %s.",
                      firenado.conf.APP_ROOT_PATH)
         self.components = {}
         settings.update(firenado.conf.app['settings'])
@@ -152,9 +152,8 @@ def __init__(self, default_host="", transforms=None, **settings):
                 handler_list[0] = rooted_path(
                     firenado.conf.app['url_root_path'], handler_list[0])
                 handlers[idx] = tuple(handler_list)
-        tornado.web.Application.__init__(self, handlers=handlers,
-                                         default_host=default_host,
-                                         transforms=transforms, **settings)
+        super().__init__(handlers=handlers, default_host=default_host,
+                         transforms=transforms, **settings)
         logger.debug("Checking if session is enabled.")
         if firenado.conf.session['enabled']:
             logger.debug("Session is enabled. Starting session engine.")
@@ -189,10 +188,9 @@ def __load_components(self):
                     else:
                         logger.debug("Failed to find the file for the "
                                      "component %s at %s. Component's "
-                                     "filename returned is %s." % (
-                                        key, firenado.conf.APP_CONFIG_PATH,
-                                        self.components[key].get_config_file())
-                                     )
+                                     "filename returned is %s.", key,
+                                     firenado.conf.APP_CONFIG_PATH,
+                                     self.components[key].get_config_file())
         # Initializing enabled components after the load.
         for key, value in firenado.conf.components.items():
             if value['enabled']:
@@ -333,6 +331,7 @@ class SessionHandler:
 
     def __init__(self):
         self.session = None
+        self.skip_auth = False
 
     def authenticated(self):
         """ Returns if the current user is authenticated. If the current user
@@ -352,8 +351,8 @@ class ComponentHandler(SessionHandler):
     """
 
     def __init__(self, **kwargs):
+        super().__init__()
         self.component = None
-        SessionHandler.__init__(self)
 
     def initialize(self, component):
         self.component = component
@@ -378,6 +377,9 @@ def write_error(self, status_code: int, **kwargs: Any) -> None:
 
     @session.read
     def prepare(self):
+        if hasattr(self, "authenticate"):
+            if self.authenticate and hasattr(self.authenticate, '__call__'):
+                self.authenticate()
         self.component.before_request(self)
         self.before_request()
 
@@ -433,9 +435,9 @@ def before_request(self):
 
 
 class TemplateHandler:
-    """ Deals with all aspects related to templates. The mixin will assume
-    it was applied to a ComponentHandler so we can resolve and deal with
-    templates defined in the same component or other components from the
+    """ Deals with all aspects related to templates. This mixin will assume
+    it was applied to a ComponentHandler. It will resolve and deal with
+    templates defined in the same component or other components of an
     application.
     """
 
@@ -477,7 +479,7 @@ def get_template_path(self):
         if self.component is None:
             # This is the default behaviour provided by Tornado.
             # No components on the request no fancy template path.
-            return super(TornadoHandler, self).get_template_path()
+            return super().get_template_path()
         else:
             return self.component.get_template_path()
 
@@ -566,7 +568,7 @@ def resolve_path(self, name, parent_path=None):
         :param parent_path: The template parent path
         :return: Tornado resolve_path result.
         """
-        logger.debug("Resolving template %s." % name)
+        logger.debug("Resolving template %s.", name)
         name_resolved = name
         if ':' in name:
             name_x = name.split(':')
@@ -575,7 +577,6 @@ def resolve_path(self, name, parent_path=None):
                 self.component.application.components[
                     component_name].get_template_path(), name_x[-1])
         if name != name_resolved:
-            logger.debug("Template %s resolved at %s." % (name, name_resolved))
+            logger.debug("Template %s resolved at %s.", name, name_resolved)
 
-        return super(FirenadoComponentLoader,
-                     self).resolve_path(name_resolved, parent_path)
+        return super().resolve_path(name_resolved, parent_path)
diff --git a/firenado/util/sqlalchemy_util.py b/firenado/util/sqlalchemy_util.py
index 60027be..371b6a5 100644
--- a/firenado/util/sqlalchemy_util.py
+++ b/firenado/util/sqlalchemy_util.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@ def run_script(script_path, session, handle_command=None, handle_line=None):
     :param handle_line: Function to handle a valid line
     :return:
     """
-    logger.debug("Opening script %s." % script_path)
+    logger.debug("Opening script %s.", script_path)
     with open(script_path, "r") as stream:
         sql_command = ""
         for line in stream:
@@ -72,8 +72,8 @@ def run_script(script_path, session, handle_command=None, handle_line=None):
             if not line.startswith("--") and line.strip("\n"):
                 # Append line to the command string
                 if handle_line is not None:
-                    logger.debug("Calling the handle line function for: "
-                                 "%s." % line)
+                    logger.debug("Calling the handle line function for: %s.",
+                                 line)
                     line = handle_line(line)
                 sql_command = "%s%s" % (sql_command, line.strip("\n"))
                 # If the command string ends with ";", it is a full statement
@@ -82,7 +82,7 @@ def run_script(script_path, session, handle_command=None, handle_line=None):
                     try:
                         if handle_command is not None:
                             logger.debug("Calling the handle command function "
-                                         "for: %s." % sql_command)
+                                         "for: %s.", sql_command)
                             sql_command = handle_command(sql_command)
                         session.execute(text(sql_command))
                     # Assert in case of error
diff --git a/requirements/basic.txt b/requirements/basic.txt
index bd7a401..981a244 100644
--- a/requirements/basic.txt
+++ b/requirements/basic.txt
@@ -1,3 +1,3 @@
-cartola==0.15
+cartola>=0.16
 pymobiledetect==1.3.2
-tornado==6.1
+tornado==6.2
diff --git a/requirements/pexpect.txt b/requirements/pexpect.txt
index 57d3570..1542537 100644
--- a/requirements/pexpect.txt
+++ b/requirements/pexpect.txt
@@ -1,1 +1,1 @@
-pexpect==4.8.0
+pexpect>=4.8.0
diff --git a/requirements/redis.txt b/requirements/redis.txt
index ef5c255..c691f21 100644
--- a/requirements/redis.txt
+++ b/requirements/redis.txt
@@ -1,2 +1,2 @@
-redis==4.1.4
-hiredis==2.0.0
+redis>=4.3.0
+hiredis>=2.0.0
diff --git a/requirements/schedule.txt b/requirements/schedule.txt
index 3f530dd..7493904 100644
--- a/requirements/schedule.txt
+++ b/requirements/schedule.txt
@@ -1,1 +1,1 @@
-croniter==1.3.4
+croniter==1.3.8
diff --git a/requirements/sqlalchemy.txt b/requirements/sqlalchemy.txt
index 83d644b..c7b02ed 100644
--- a/requirements/sqlalchemy.txt
+++ b/requirements/sqlalchemy.txt
@@ -1,1 +1,1 @@
-sqlalchemy==1.4.31
+sqlalchemy>=1.4.0
diff --git a/setup.py b/setup.py
index ed32eae..b4a7688 100644
--- a/setup.py
+++ b/setup.py
@@ -46,9 +46,6 @@ def resolve_requires(requirements_file):
 with open("README.md", "r") as fh:
     long_description = fh.read()
 
-# We still running: python setup.py sdist upload --repository=testpypi
-# Twine isn't handling long_descriptions as per:
-# https://github.com/pypa/twine/issues/262
 setup(
     name="Firenado",
     version=firenado.get_version(),
@@ -62,6 +59,7 @@ def resolve_requires(requirements_file):
     maintainer=firenado.get_author(),
     maintainer_email=firenado.get_author_email(),
     install_requires=resolve_requires("requirements/basic.txt"),
+    python_requires=">= 3.7",
     extras_require={
         'all': resolve_requires("requirements/all.txt"),
         'redis': resolve_requires("requirements/redis.txt"),
@@ -80,10 +78,10 @@ def resolve_requires(requirements_file):
         "License :: OSI Approved :: Apache Software License",
         "Operating System :: OS Independent",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3 :: Only",
         "Topic :: Internet :: WWW/HTTP",
         "Topic :: Internet :: WWW/HTTP :: Dynamic Content",


Test patch:
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 87a18e8..ef4cf2c 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -1,6 +1,6 @@
 name: Run firenado tests
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   build:
@@ -8,12 +8,12 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.7", "3.8", "3.9", "3.10"]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install dependencies
diff --git a/examples/testapp/app.py b/examples/testapp/app.py
index 9228899..5efc48d 100644
--- a/examples/testapp/app.py
+++ b/examples/testapp/app.py
@@ -67,7 +67,13 @@ def initialize(self):
         firenado.conf.app['login']['urls']['buga'] = 'buga'
         data_source_conf = {
             'connector': "sqlalchemy",
-            'url': "mysql+pymysql://root@localhost:3306/test"
+            'url': "mysql+pymysql://root@localhost:3306/test",
+            'pool': {
+                'size': 10,
+                'max_overflow': 10,
+                # 'isolation_level': 'REPEATABLE READ',
+                # 'pool_recycle': 400
+            }
         }
         data_source_name = "test"
         data_source = data.config_to_data_source(data_source_name,
diff --git a/examples/testapp/handlers.py b/examples/testapp/handlers.py
index 385d7bf..cb944f8 100644
--- a/examples/testapp/handlers.py
+++ b/examples/testapp/handlers.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -171,7 +171,7 @@ class LogoutHandler(AuthHandler, tornadoweb.TornadoHandler):
     def get(self):
         default_login = firenado.conf.app['login']['urls']['default']
         if self.authenticated():
-            self.session.delete("user")
+            self.session.destroy(self)
             self.redirect(self.get_rooted_path("private"))
         else:
             self.redirect(self.get_rooted_path(default_login))
diff --git a/firenado/testing.py b/firenado/testing.py
new file mode 100644
index 0000000..72e5829
--- /dev/null
+++ b/firenado/testing.py
@@ -0,0 +1,126 @@
+# -*- coding: UTF-8 -*-
+#
+# Copyright 2015-2022 Flávio Gonçalves Garcia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import asyncio
+from behave.api.async_step import async_run_until_complete
+from firenado.launcher import ProcessLauncher
+from tornado.testing import (bind_unused_port, AsyncTestCase,
+                             AsyncHTTPTestCase)
+from tornado.httpclient import HTTPResponse
+from tornado import gen, ioloop
+from typing import Any
+import warnings
+
+
+def get_event_loop():
+    """ Return a new event asyncio evenvent loop with trigerring a
+    depreciation warning from Python 10.
+
+    :return: A event loop
+    """
+    loop = asyncio.get_event_loop_policy().get_event_loop()
+    return loop if loop else asyncio.new_event_loop()
+
+
+class ProcessLauncherTestCase(AsyncHTTPTestCase):
+
+    @property
+    def launcher(self):
+        return self.__launcher
+
+    def get_launcher(self) -> ProcessLauncher:
+        """Should be overridden by subclasses to return a
+        `firenado.launcher.ProcessLauncher`.
+        """
+        raise NotImplementedError()
+
+    def get_http_port(self) -> int:
+        """Returns the port used by the server.
+
+        A new port is chosen for each test.
+        """
+        return self.__port
+
+    @async_run_until_complete(should_close=False, loop=get_event_loop())
+    async def setUp(self) -> None:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            sock, port = bind_unused_port()
+            sock.close()
+            self.__port = port
+            self.__launcher = self.get_launcher()
+            self.__launcher.port = self.__port
+            self.__launcher.load()
+            self.http_client = self.get_http_client()
+            self.io_loop = ioloop.IOLoop.current()
+            await self.__launcher.launch()
+            await gen.sleep(1)
+
+    def get_url(self, path: str) -> str:
+        """Returns an absolute url for the given path on the test server."""
+        return "%s://127.0.0.1:%s%s" % (self.get_protocol(),
+                                        self.get_http_port(), path)
+
+    async def fetch(
+        self, path: str, raise_error: bool = False, **kwargs: Any
+    ) -> HTTPResponse:
+        """Convenience method to synchronously fetch a URL.
+
+        The given path will be appended to the local server's host and
+        port.  Any additional keyword arguments will be passed directly to
+        `.AsyncHTTPClient.fetch` (and so could be used to pass
+        ``method="POST"``, ``body="..."``, etc).
+
+        If the path begins with http:// or https://, it will be treated as a
+        full URL and will be fetched as-is.
+
+        If ``raise_error`` is ``True``, a `tornado.httpclient.HTTPError` will
+        be raised if the response code is not 200. This is the same behavior
+        as the ``raise_error`` argument to `.AsyncHTTPClient.fetch`, but
+        the default is ``False`` here (it's ``True`` in `.AsyncHTTPClient`)
+        because tests often need to deal with non-200 response codes.
+
+        .. versionchanged:: 5.0
+           Added support for absolute URLs.
+
+        .. versionchanged:: 5.1
+
+           Added the ``raise_error`` argument.
+
+        .. deprecated:: 5.1
+
+           This method currently turns any exception into an
+           `.HTTPResponse` with status code 599. In Tornado 6.0,
+           errors other than `tornado.httpclient.HTTPError` will be
+           passed through, and ``raise_error=False`` will only
+           suppress errors that would be raised due to non-200
+           response codes.
+
+        """
+        if path.lower().startswith(("http://", "https://")):
+            url = path
+        else:
+            url = self.get_url(path)
+
+        return await self.http_client.fetch(url, raise_error=raise_error,
+                                            **kwargs)
+
+    def tearDown(self) -> None:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            self.http_client.close()
+            AsyncTestCase.tearDown(self)
+            self.__launcher.shutdown()
diff --git a/tests/__init__.py b/tests/__init__.py
index 590663d..b2a5d79 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -40,21 +40,21 @@ def chdir_fixture_app(app_name, **kwargs):
     return test_app_dirname
 
 
-def chdir_app(app_name, dir=None):
+def chdir_app(app_name, directory=None):
     """ Change to the application directory located at the resource directory
     for conf tests.
 
     The conf resources directory is firenado/tests/resources/conf.
 
     :param app_name: The application name
-    :param dir: The directory to be changed
+    :param directory: The directory to be changed
     """
     import firenado.conf
 
     test_dirname, filename = os.path.split(os.path.abspath(__file__))
-    if dir:
+    if directory:
         test_app_dirname = os.path.join(test_dirname, 'resources',
-                                        dir, app_name)
+                                        directory, app_name)
     else:
         test_app_dirname = os.path.join(test_dirname, 'resources', app_name)
     os.chdir(test_app_dirname)
diff --git a/tests/components_test.py b/tests/components_test.py
index 5985679..e6778ad 100644
--- a/tests/components_test.py
+++ b/tests/components_test.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
 from firenado.config import get_class_from_config
 from tests import chdir_app
 from firenado.tornadoweb import TornadoApplication
+import warnings
 
 chdir_app('file', 'session')
 
@@ -33,14 +34,16 @@ def setUp(self):
         """ Application configuration file will be read and components will be
         loaded.
         """
-        chdir_app('file', 'session')
-        self.application = TornadoApplication()
-        self.session_handler_config = firenado.conf.session[
-            'handlers'][firenado.conf.session['type']]
-        self.session_handler_config = firenado.conf.session[
-            'handlers'][firenado.conf.session['type']]
-        self.session_handler_class = get_class_from_config(
-            self.session_handler_config)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            chdir_app('file', 'session')
+            self.application = TornadoApplication()
+            self.session_handler_config = firenado.conf.session[
+                'handlers'][firenado.conf.session['type']]
+            self.session_handler_config = firenado.conf.session[
+                'handlers'][firenado.conf.session['type']]
+            self.session_handler_class = get_class_from_config(
+                self.session_handler_config)
 
     def test_application_session_handler(self):
         """ Checks if the session handler loaded is the same the session
diff --git a/tests/conf_test.py b/tests/conf_test.py
index 568c676..79dc74a 100644
--- a/tests/conf_test.py
+++ b/tests/conf_test.py
@@ -28,14 +28,6 @@ class ApplicationComponentTestCase(unittest.TestCase):
     configuration file.
     """
 
-    def test_conf_root(self):
-        """ Test if Firenado root matches the upper directory relative to the
-        current one. """
-        import os
-        current_path = os.path.dirname(os.path.realpath(__file__))
-        firenado_root = ("%s" % os.sep).join(current_path.split(os.sep)[:-1])
-        self.assertEqual(firenado_root, firenado.conf.ROOT)
-
     def test_conf_root(self):
         """ Test if Firenado root matches the upper directory relative to the
         current one. """
@@ -195,7 +187,7 @@ def test_root_url_slash_none(self):
         chdir_app("root_url_slash_none", "conf")
         self.assertEqual(None,  firenado.conf.app['url_root_path'])
 
-    def test_static_path(self):
+    def test_static_url_prefix(self):
         """ If static url prefix is defined on the app configuration.
         """
         chdir_app("yml", "conf")
@@ -230,6 +222,20 @@ def test_session_name_custom(self):
         self.assertEqual(firenado.conf.session['enabled'], True)
         self.assertEqual(firenado.conf.session['name'], "REDISSESSID")
 
+    def test_data_source_pool(self):
+        """ Checks if the data source pool is set correctly
+        """
+        chdir_app("data", "conf")
+        self.assertEqual(
+            firenado.conf.data['sources']['mysql']['pool']['size'],
+            10)
+        self.assertEqual(
+            firenado.conf.data['sources']['mysql']['pool']['max_overflow'],
+            10)
+        self.assertEqual(
+            firenado.conf.data['sources']['mysql']['pool']['isolation_level'],
+            "REPEATABLE READ")
+
 
 class MultiAppTestCase(unittest.TestCase):
     """ Case that tests multi app configuration.
diff --git a/tests/features/steps/launcher.py b/tests/features/steps/launcher.py
index 3c1a30f..237de7d 100644
--- a/tests/features/steps/launcher.py
+++ b/tests/features/steps/launcher.py
@@ -39,7 +39,7 @@ async def step_we_launch_application_using_process_launcher(context,
 
 @when("The application is running correctly at {port} port")
 @async_run_until_complete
-async def step_we_launch_application_using_process_launcher(context, port):
+async def step_application_running_correctly_at_port(context, port):
     http_client = AsyncHTTPClient()
     try:
         response = await http_client.fetch("http://localhost:%s" %
diff --git a/tests/resources/conf/data/conf/firenado.yml b/tests/resources/conf/data/conf/firenado.yml
new file mode 100644
index 0000000..f8ee4be
--- /dev/null
+++ b/tests/resources/conf/data/conf/firenado.yml
@@ -0,0 +1,45 @@
+app:
+  component: test
+  data:
+    sources:
+        # Set here references from sources defined on data.sources
+        - session
+  pythonpath: ..
+  port: 8887
+  static_path: 'a_static_path'
+
+data:
+  sources:
+    - name: session
+      connector: redis
+      # host: localhost
+      # port: 6379
+      # db: 0
+    - name: mysql
+      connector: sqlalchemy
+      url: mysql+pymysql://root@localhost:3306/database
+      pool:
+        size: 10
+        max_overflow: 10
+        isolation_level: "REPEATABLE READ"
+components:
+  - id: test
+    class: tests.tornadoweb_test.TestComponent
+    enabled: true
+  #- id: admin
+  #  enabled: true
+  #- id: assets
+  #  enabled: true
+  #- id: info
+  #  enabled: true
+
+# Session types could be:
+# file or redis.
+session:
+  type: file
+  enabled: true
+  # Redis session handler configuration
+  #data:
+    #source: session
+  # File session handler related configuration
+  # path: /tmp
diff --git a/tests/session_test.py b/tests/session_test.py
index 0eb49de..58c137e 100644
--- a/tests/session_test.py
+++ b/tests/session_test.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
 #
-# Copyright 2015-2021 Flavio Garcia
+# Copyright 2015-2022 Flávio Gonçalves Garcia
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
 from firenado.tornadoweb import TornadoApplication
 from tests import chdir_app
 import unittest
+import warnings
 
 
 class FileSessionTestCase(unittest.TestCase):
@@ -31,13 +32,15 @@ def setUp(self):
         loaded.
         """
         chdir_app("file", "session")
-        self.application = TornadoApplication()
-        self.session_handler_config = firenado.conf.session[
-            'handlers'][firenado.conf.session['type']]
-        self.session_handler_config = firenado.conf.session[
-            'handlers'][firenado.conf.session['type']]
-        self.session_handler_class = get_class_from_config(
-            self.session_handler_config)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            self.application = TornadoApplication()
+            self.session_handler_config = firenado.conf.session[
+                'handlers'][firenado.conf.session['type']]
+            self.session_handler_config = firenado.conf.session[
+                'handlers'][firenado.conf.session['type']]
+            self.session_handler_class = get_class_from_config(
+                self.session_handler_config)
 
     def test_defaults_session_parameters(self):
         """ Checks default session parameters on the configuration
@@ -59,12 +62,16 @@ def test_session_type_file(self):
         """ Checks if test component was loaded correctly by the application
         __init__ method.
         """
-        application = TornadoApplication()
-        session_handler_config = firenado.conf.session[
-            'handlers'][firenado.conf.session['type']]
-        session_handler_class = get_class_from_config(session_handler_config)
-        self.assertEqual(application.session_engine.session_handler.__class__,
-                         session_handler_class)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            application = TornadoApplication()
+            session_handler_config = firenado.conf.session[
+                'handlers'][firenado.conf.session['type']]
+            session_handler_class = get_class_from_config(
+                session_handler_config)
+            self.assertEqual(
+                application.session_engine.session_handler.__class__,
+                session_handler_class)
 
 
 class RedisSessionTestCase(unittest.TestCase):
diff --git a/tests/util/sqlalchemy_util_test.py b/tests/util/sqlalchemy_util_test.py
index cf0f0e2..c54769c 100644
--- a/tests/util/sqlalchemy_util_test.py
+++ b/tests/util/sqlalchemy_util_test.py
@@ -61,7 +61,7 @@ def test_base_to_dict(self):
         self.assertEqual(dict_from_base['created'], self.test_object.created)
         self.assertEqual(dict_from_base['modified'], self.test_object.modified)
 
-    def test_base_to_dict(self):
+    def test_base_to_dict_parametrized(self):
         dict_from_base = base_to_dict(self.test_object,
                                       ["id", "username", "first_name"])
         self.assertEqual(dict_from_base['id'], self.test_object.id)