diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py
index b88a833601bcee7a2b1d074e95f1c9b6e98bdaee..9ec7b0db526f0a5225707717762ede4f69ba0415 100644
--- a/addons/bus/models/bus.py
+++ b/addons/bus/models/bus.py
@@ -99,6 +99,7 @@ class ImDispatch(object):
     def __init__(self):
         self.channels = {}
         self.started = False
+        self.Event = None
 
     def poll(self, dbname, channels, last, options=None, timeout=TIMEOUT):
         if options is None:
@@ -181,7 +182,7 @@ class ImDispatch(object):
     def start(self):
         if odoo.evented:
             # gevent mode
-            import gevent
+            import gevent.event  # pylint: disable=import-outside-toplevel
             self.Event = gevent.event.Event
             gevent.spawn(self.run)
         else:
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 96127e5636740ff1048492e1705ada7d5df349db..01b5b77a19e951407a7ff011e0ffc06f3c1dd54a 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -528,8 +528,13 @@ class IrHttp(models.AbstractModel):
             raise Exception("Rerouting limit exceeded")
         request.httprequest.environ['PATH_INFO'] = path
         # void werkzeug cached_property. TODO: find a proper way to do this
-        for key in ('path', 'full_path', 'url', 'base_url'):
+        for key in ('full_path', 'url', 'base_url'):
             request.httprequest.__dict__.pop(key, None)
+        # since werkzeug 2.0 `path`` became an attribute and is not a cached property anymore
+        if hasattr(type(request.httprequest), 'path'): # cached property
+            request.httprequest.__dict__.pop('path', None)
+        else: # direct attribute
+            request.httprequest.path = '/' + path.lstrip('/')
 
         return cls._dispatch()
 
diff --git a/addons/link_tracker/models/mail_render_mixin.py b/addons/link_tracker/models/mail_render_mixin.py
index 3fa3159f361b0907cfaebd8d852de6f233837a13..21ba81f4d90a4c6f9c9a69ae8815f011b17e2dda 100644
--- a/addons/link_tracker/models/mail_render_mixin.py
+++ b/addons/link_tracker/models/mail_render_mixin.py
@@ -3,7 +3,8 @@
 
 import re
 
-from werkzeug import urls, utils
+from html import unescape
+from werkzeug import urls
 
 from odoo import api, models, tools
 
@@ -40,7 +41,7 @@ class MailRenderMixin(models.AbstractModel):
             label = (match[3] or '').strip()
 
             if not blacklist or not [s for s in blacklist if s in long_url] and not long_url.startswith(short_schema):
-                create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label))
+                create_vals = dict(link_tracker_vals, url=unescape(long_url), label=unescape(label))
                 link = self.env['link.tracker'].create(create_vals)
                 if link.short_url:
                     new_href = href.replace(long_url, link.short_url)
@@ -69,7 +70,7 @@ class MailRenderMixin(models.AbstractModel):
             if blacklist and any(item in parsed.path for item in blacklist):
                 continue
 
-            create_vals = dict(link_tracker_vals, url= utils.unescape(original_url))
+            create_vals = dict(link_tracker_vals, url=unescape(original_url))
             link = self.env['link.tracker'].create(create_vals)
             if link.short_url:
                 content = content.replace(original_url, link.short_url, 1)
diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py
index c30d0c645149dbc965f1cacfe75dba62de6b672d..23457bc025e51a5791b26fc85a1d56b25112a576 100644
--- a/addons/pos_mercury/models/pos_mercury_transaction.py
+++ b/addons/pos_mercury/models/pos_mercury_transaction.py
@@ -4,7 +4,8 @@
 from datetime import date, timedelta
 
 import requests
-import werkzeug
+
+from html import unescape
 
 from odoo import models, api, service
 from odoo.tools.translate import _
@@ -69,7 +70,7 @@ class MercuryTransaction(models.Model):
         try:
             r = requests.post(url, data=xml_transaction, headers=headers, timeout=65)
             r.raise_for_status()
-            response = werkzeug.utils.unescape(r.content.decode())
+            response = unescape(r.content.decode())
         except Exception:
             response = "timeout"
 
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 813a0d941d84bd6698749894451228827da5550c..cdd8daacd44ff7797a3bc6af454448f0fe090352 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1099,10 +1099,9 @@ class Proxy(http.Controller):
             if not data:
                 raise werkzeug.exceptions.BadRequest()
             from werkzeug.test import Client
-            from werkzeug.wrappers import BaseResponse
             base_url = request.httprequest.base_url
             query_string = request.httprequest.query_string
-            client = Client(http.root, BaseResponse)
+            client = Client(http.root, werkzeug.wrappers.Response)
             headers = {'X-Openerp-Session-Id': request.session.sid}
             return client.post('/' + path, base_url=base_url, query_string=query_string,
                                headers=headers, data=data)
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index a839ccde34e4b0b9402d54b9bb56458bc26b0a2b..c5da71c16cf43f951d3dbcb2fdf9a83c252289e0 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -129,7 +129,7 @@ class Website(Home):
         """
         if not redirect and request.params.get('login_success'):
             if request.env['res.users'].browse(uid).has_group('base.group_user'):
-                redirect = b'/web?' + request.httprequest.query_string
+                redirect = '/web?' + request.httprequest.query_string.decode()
             else:
                 redirect = '/my'
         return super()._login_redirect(uid, redirect=redirect)
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index d2fdd23a8e54d613c58abd892f9531d30d365973..9f350cc45c80e00ee9c75b677b265de28ee011cc 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -22,11 +22,18 @@ import json
 from lxml import etree
 from contextlib import closing
 from distutils.version import LooseVersion
-from reportlab.graphics.barcode import createBarcodeDrawing
 from PyPDF2 import PdfFileWriter, PdfFileReader, utils
 from collections import OrderedDict
 from collections.abc import Iterable
 from PIL import Image, ImageFile
+
+# Ignore a deprecation warning `load_module` usage in importlib from python 3.10 (Jammy)
+# Catched here in order to not miss the same warning from elsewhere
+import warnings
+with warnings.catch_warnings():
+    warnings.simplefilter("ignore")
+    from reportlab.graphics.barcode import createBarcodeDrawing
+
 # Allow truncated images
 ImageFile.LOAD_TRUNCATED_IMAGES = True
 
diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py
index 25fc7a832d83f877bfb69b3d51896b7bd8e85eee..957e85e94a1d862828edbec6b573511e37064ca3 100644
--- a/odoo/addons/base/models/qweb.py
+++ b/odoo/addons/base/models/qweb.py
@@ -14,10 +14,9 @@ from textwrap import dedent
 import itertools
 from lxml import etree, html
 from psycopg2.extensions import TransactionRollbackError
-import werkzeug
-from werkzeug.utils import escape as _escape
 
 from odoo.tools import pycompat, freehash
+from odoo.tools.misc import html_escape as escape
 from odoo.tools.safe_eval import check_values
 
 import builtins
@@ -160,9 +159,6 @@ class QWebException(Exception):
     def __repr__(self):
         return str(self)
 
-# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
-escape = (lambda text: _escape(text, quote=True)) if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0') else _escape
-
 def foreach_iterator(base_ctx, enum, name):
     ctx = base_ctx.copy()
     if not enum:
diff --git a/odoo/http.py b/odoo/http.py
index 551a16fe5fd8f9804d98a25b0fae21d636292d34..5536daedc15fb74442beb060956a21e7ac60bc72 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -55,8 +55,10 @@ from .tools import ustr, consteq, frozendict, pycompat, unique, date_utils
 from .tools.mimetypes import guess_mimetype
 from .tools.misc import str2bool
 from .tools._vendor import sessions
+from .tools._vendor.useragents import UserAgent
 from .modules.module import module_manifest
 
+
 _logger = logging.getLogger(__name__)
 rpc_request = logging.getLogger(__name__ + '.rpc.request')
 rpc_response = logging.getLogger(__name__ + '.rpc.response')
@@ -537,7 +539,7 @@ def route(route=None, **kw):
 
             if isinstance(response, werkzeug.exceptions.HTTPException):
                 response = response.get_response(request.httprequest.environ)
-            if isinstance(response, werkzeug.wrappers.BaseResponse):
+            if isinstance(response, werkzeug.wrappers.Response):
                 response = Response.force_type(response)
                 response.set_default()
                 return response
@@ -1451,7 +1453,7 @@ class Root(object):
 
     def set_csp(self, response):
         # ignore HTTP errors
-        if not isinstance(response, werkzeug.wrappers.BaseResponse):
+        if not isinstance(response, werkzeug.wrappers.Response):
             return
 
         headers = response.headers
@@ -1471,6 +1473,7 @@ class Root(object):
         """
         try:
             httprequest = werkzeug.wrappers.Request(environ)
+            httprequest.user_agent_class = UserAgent  # use vendored userAgent since it will be removed in 2.1
             httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
 
             current_thread = threading.current_thread()
diff --git a/odoo/netsvc.py b/odoo/netsvc.py
index 0e5baefae78740852a4c71700058a113e8b95951..deea2e288b28fb58ef403e5d080a34c740ac581b 100644
--- a/odoo/netsvc.py
+++ b/odoo/netsvc.py
@@ -127,9 +127,12 @@ def init_logger():
     warnings.filterwarnings('default', category=DeprecationWarning)
     # ignore deprecation warnings from invalid escape (there's a ton and it's
     # pretty likely a super low-value signal)
-    warnings.filterwarnings('ignore', r'^invalid escape sequence \\.', category=DeprecationWarning)
+    warnings.filterwarnings('ignore', r'^invalid escape sequence \'?\\.', category=DeprecationWarning)
     # recordsets are both sequence and set so trigger warning despite no issue
     warnings.filterwarnings('ignore', r'^Sampling from a set', category=DeprecationWarning, module='odoo')
+    # (Jammy) distutils, currentThread, isDaemon, setDaemon future removal are properly handled in upper versions
+    warnings.filterwarnings('ignore', r'^The distutils package is deprecated and slated for removal', category=DeprecationWarning)
+    warnings.filterwarnings('ignore', r'^(currentThread|isDaemon|setDaemon)\(\) is deprecated', category=DeprecationWarning)
     # ignore a bunch of warnings we can't really fix ourselves
     for module in [
         'babel.util', # deprecated parser module, no release yet
diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py
index c2c3a643cea7f8489c3535f10f95e516e1a0a090..4c8d8db6ef3bd1e140f68c148b76945151af8333 100644
--- a/odoo/tools/_vendor/sessions.py
+++ b/odoo/tools/_vendor/sessions.py
@@ -19,14 +19,13 @@ import os
 import re
 import tempfile
 from hashlib import sha1
-from os import path
+from os import path, replace as rename
 from pickle import dump
 from pickle import HIGHEST_PROTOCOL
 from pickle import load
 from time import time
 
 from werkzeug.datastructures import CallbackDict
-from werkzeug.posixemulation import rename
 
 _sha1_re = re.compile(r"^[a-f0-9]{40}$")
 
diff --git a/odoo/tools/_vendor/useragents.py b/odoo/tools/_vendor/useragents.py
new file mode 100644
index 0000000000000000000000000000000000000000..7627ccc973bf69e4b88642b8509178b33e5722bf
--- /dev/null
+++ b/odoo/tools/_vendor/useragents.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+"""
+    werkzeug.useragents
+    ~~~~~~~~~~~~~~~~~~~
+
+    This module provides a helper to inspect user agent strings.  This module
+    is far from complete but should work for most of the currently available
+    browsers.
+
+
+    :copyright: 2007 Pallets
+    :license: BSD-3-Clause
+
+    This package was vendored in odoo in order to prevent errors with werkzeug 2.1
+"""
+import re
+
+
+class UserAgentParser(object):
+    """A simple user agent parser.  Used by the `UserAgent`."""
+
+    platforms = (
+        ("cros", "chromeos"),
+        ("iphone|ios", "iphone"),
+        ("ipad", "ipad"),
+        (r"darwin|mac|os\s*x", "macos"),
+        ("win", "windows"),
+        (r"android", "android"),
+        ("netbsd", "netbsd"),
+        ("openbsd", "openbsd"),
+        ("freebsd", "freebsd"),
+        ("dragonfly", "dragonflybsd"),
+        ("(sun|i86)os", "solaris"),
+        (r"x11|lin(\b|ux)?", "linux"),
+        (r"nintendo\s+wii", "wii"),
+        ("irix", "irix"),
+        ("hp-?ux", "hpux"),
+        ("aix", "aix"),
+        ("sco|unix_sv", "sco"),
+        ("bsd", "bsd"),
+        ("amiga", "amiga"),
+        ("blackberry|playbook", "blackberry"),
+        ("symbian", "symbian"),
+    )
+    browsers = (
+        ("googlebot", "google"),
+        ("msnbot", "msn"),
+        ("yahoo", "yahoo"),
+        ("ask jeeves", "ask"),
+        (r"aol|america\s+online\s+browser", "aol"),
+        ("opera", "opera"),
+        ("edge", "edge"),
+        ("chrome|crios", "chrome"),
+        ("seamonkey", "seamonkey"),
+        ("firefox|firebird|phoenix|iceweasel", "firefox"),
+        ("galeon", "galeon"),
+        ("safari|version", "safari"),
+        ("webkit", "webkit"),
+        ("camino", "camino"),
+        ("konqueror", "konqueror"),
+        ("k-meleon", "kmeleon"),
+        ("netscape", "netscape"),
+        (r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"),
+        ("lynx", "lynx"),
+        ("links", "links"),
+        ("Baiduspider", "baidu"),
+        ("bingbot", "bing"),
+        ("mozilla", "mozilla"),
+    )
+
+    _browser_version_re = r"(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?"
+    _language_re = re.compile(
+        r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|"
+        r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)"
+    )
+
+    def __init__(self):
+        self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platforms]
+        self.browsers = [
+            (b, re.compile(self._browser_version_re % a, re.I))
+            for a, b in self.browsers
+        ]
+
+    def __call__(self, user_agent):
+        for platform, regex in self.platforms:  # noqa: B007
+            match = regex.search(user_agent)
+            if match is not None:
+                break
+        else:
+            platform = None
+        for browser, regex in self.browsers:  # noqa: B007
+            match = regex.search(user_agent)
+            if match is not None:
+                version = match.group(1)
+                break
+        else:
+            browser = version = None
+        match = self._language_re.search(user_agent)
+        if match is not None:
+            language = match.group(1) or match.group(2)
+        else:
+            language = None
+        return platform, browser, version, language
+
+
+class UserAgent(object):
+    """Represents a user agent.  Pass it a WSGI environment or a user agent
+    string and you can inspect some of the details from the user agent
+    string via the attributes.  The following attributes exist:
+
+    .. attribute:: string
+
+       the raw user agent string
+
+    .. attribute:: platform
+
+       the browser platform.  The following platforms are currently
+       recognized:
+
+       -   `aix`
+       -   `amiga`
+       -   `android`
+       -   `blackberry`
+       -   `bsd`
+       -   `chromeos`
+       -   `dragonflybsd`
+       -   `freebsd`
+       -   `hpux`
+       -   `ipad`
+       -   `iphone`
+       -   `irix`
+       -   `linux`
+       -   `macos`
+       -   `netbsd`
+       -   `openbsd`
+       -   `sco`
+       -   `solaris`
+       -   `symbian`
+       -   `wii`
+       -   `windows`
+
+    .. attribute:: browser
+
+        the name of the browser.  The following browsers are currently
+        recognized:
+
+        -   `aol` *
+        -   `ask` *
+        -   `baidu` *
+        -   `bing` *
+        -   `camino`
+        -   `chrome`
+        -   `edge`
+        -   `firefox`
+        -   `galeon`
+        -   `google` *
+        -   `kmeleon`
+        -   `konqueror`
+        -   `links`
+        -   `lynx`
+        -   `mozilla`
+        -   `msie`
+        -   `msn`
+        -   `netscape`
+        -   `opera`
+        -   `safari`
+        -   `seamonkey`
+        -   `webkit`
+        -   `yahoo` *
+
+        (Browsers marked with a star (``*``) are crawlers.)
+
+    .. attribute:: version
+
+        the version of the browser
+
+    .. attribute:: language
+
+        the language of the browser
+    """
+
+    _parser = UserAgentParser()
+
+    def __init__(self, environ_or_string):
+        if isinstance(environ_or_string, dict):
+            environ_or_string = environ_or_string.get("HTTP_USER_AGENT", "")
+        self.string = environ_or_string
+        self.platform, self.browser, self.version, self.language = self._parser(
+            environ_or_string
+        )
+
+    def to_header(self):
+        return self.string
+
+    def __str__(self):
+        return self.string
+
+    def __nonzero__(self):
+        return bool(self.browser)
+
+    __bool__ = __nonzero__
+
+    def __repr__(self):
+        return "<%s %r/%s>" % (self.__class__.__name__, self.browser, self.version)
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 8059a0d802b52f76f9e46cc0cc37d49751ed8bbf..40c5664afcaa95978a10e3300dcc9e27ab2e7e39 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -35,7 +35,6 @@ import babel
 import babel.dates
 import passlib.utils
 import pytz
-import werkzeug.utils
 from lxml import etree
 
 import odoo
@@ -1239,13 +1238,27 @@ def ignore(*exc):
     except exc:
         pass
 
-# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
-if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
-    def html_escape(text):
-        return werkzeug.utils.escape(text, quote=True)
-else:
-    def html_escape(text):
-        return werkzeug.utils.escape(text)
+def html_escape(text):
+    """ Vendored from werkzeug.utils.escape which is deprecated in 2.0
+    Replace special characters "&", "<", ">" and (") to HTML-safe sequences.
+
+    There is a special handling for `None` which escapes to an empty string.
+
+    :param s: the string to escape.
+    """
+    if  text is None:
+        return ""
+    elif hasattr(text, "__html__"):
+        return str(text.__html__())
+    elif not isinstance(text, str):
+        text = str(text)
+    text = (
+        text.replace("&", "&amp;")
+        .replace("<", "&lt;")
+        .replace(">", "&gt;")
+        .replace('"', "&quot;")
+    )
+    return text
 
 def get_lang(env, lang_code=False):
     """
diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py
index ce47edb9d0f193a4b13022e8d478c618c0c97b9f..ca21cc7210afd3f6614b66f253f72adcee3fb3c3 100644
--- a/odoo/tools/safe_eval.py
+++ b/odoo/tools/safe_eval.py
@@ -89,6 +89,7 @@ _EXPR_OPCODES = _CONST_OPCODES.union(to_opcodes([
 ])) - _BLACKLIST
 
 _SAFE_OPCODES = _EXPR_OPCODES.union(to_opcodes([
+    'GEN_START',  # added in 3.10
     'POP_BLOCK', 'POP_EXCEPT',
 
     # note: removed in 3.8
diff --git a/requirements.txt b/requirements.txt
index b46ed6a3370611ac4f06d3cbf672406e91109f2d..5f0139ae7494f7520bb32f88aac7744334d0a079 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
-Babel==2.6.0
+Babel==2.6.0; python_version <= '3.9'
+Babel==2.9.1; python_version > '3.9'  # (Jammy) 2.6.0 has issues with python 3.10
 chardet==3.0.4
 decorator==4.3.0
 docutils==0.14
@@ -6,12 +7,14 @@ ebaysdk==2.1.5
 freezegun==0.3.11; python_version < '3.8'
 freezegun==0.3.15; python_version >= '3.8'
 gevent==1.1.2 ; sys_platform != 'win32' and python_version < '3.7'
-gevent==1.5.0 ; python_version == '3.7'
-gevent==20.9.0 ; python_version >= '3.8'
 gevent==1.4.0 ; sys_platform == 'win32' and python_version < '3.7'
+gevent==1.5.0 ; python_version == '3.7'
+gevent==20.9.0 ; python_version > '3.7' and python_version <= '3.9'
+gevent==21.8.0 ; python_version > '3.9'  # (Jammy)
 greenlet==0.4.10 ; python_version < '3.7'
 greenlet==0.4.15 ; python_version == '3.7'
-greenlet==0.4.17 ; python_version > '3.7'
+greenlet==0.4.17 ; python_version > '3.7' and python_version <= '3.9'
+greenlet==1.1.2 ; python_version  > '3.9'  # (Jammy)
 idna==2.6
 Jinja2==2.10.1; python_version < '3.8'
 # bullseye version, focal patched 2.10
@@ -19,12 +22,14 @@ Jinja2==2.11.2; python_version >= '3.8'
 libsass==0.17.0
 lxml==3.7.1 ; sys_platform != 'win32' and python_version < '3.7'
 lxml==4.3.2 ; sys_platform != 'win32' and python_version == '3.7'
-lxml==4.6.1 ; sys_platform != 'win32' and python_version > '3.7'
+# lxml 4.6.1 has incompatibility issues with python 3.10
+lxml==4.6.5 ; sys_platform != 'win32' and python_version > '3.7'  # min version = 4.5.0 (Focal - with security backports)
 lxml ; sys_platform == 'win32'
 Mako==1.0.7
 MarkupSafe==1.1.0
 num2words==0.5.6
-ofxparse==0.19
+ofxparse==0.19; python_version <= '3.9'
+ofxparse==0.21; python_version > '3.9'  # (Jammy) ABC removed from collections in 3.10 but still used in ofxparse < 0.21
 passlib==1.7.1
 Pillow==5.4.1 ; python_version <= '3.7' and sys_platform != 'win32'
 Pillow==6.1.0 ; python_version <= '3.7' and sys_platform == 'win32'
@@ -43,11 +48,14 @@ pyusb==1.0.2
 qrcode==6.1
 reportlab==3.5.13; python_version < '3.8'
 reportlab==3.5.55; python_version >= '3.8'
-requests==2.21.0
+requests==2.21.0; python_version <= '3.9'
+requests==2.25.1; python_version > '3.9'  # (Jammy) versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal)
+urllib3==1.26.5; python_version > '3.9'  # (Jammy) indirect / min version = 1.25.8 (Focal with security backports)
 zeep==3.2.0
 python-stdnum==1.8
 vobject==0.9.6.1
-Werkzeug==0.16.1
+Werkzeug==0.16.1 ; python_version <= '3.9'
+Werkzeug==2.0.2 ; python_version > '3.9'  # (Jammy)
 XlsxWriter==1.1.2
 xlwt==1.3.*
 xlrd==1.1.0; python_version < '3.8'