aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2022-11-20 00:43:16 +0100
committerDaniel Schadt <kingdread@gmx.de>2022-11-20 00:43:16 +0100
commitb117ddcde172c4a9c2c377ac5aa08f5ede345f2d (patch)
tree87eb96d960c8df04eed0f4e2191bc6044fa9e773
parent97835c471bc10bd20e1253e1bbfd71e5e71b2883 (diff)
parent20fd6620deb79f214cd2b7c9474b5df0d2fbda5b (diff)
downloadfietsboek-b117ddcde172c4a9c2c377ac5aa08f5ede345f2d.tar.gz
fietsboek-b117ddcde172c4a9c2c377ac5aa08f5ede345f2d.tar.bz2
fietsboek-b117ddcde172c4a9c2c377ac5aa08f5ede345f2d.zip
Merge branch 'tile-proxy'
-rw-r--r--development.ini1
-rw-r--r--doc/administration/configuration.rst197
-rw-r--r--doc/administration/installation.rst12
-rw-r--r--doc/conf.py2
-rw-r--r--fietsboek/__init__.py21
-rw-r--r--fietsboek/jinja2.py37
-rw-r--r--fietsboek/locale/fietslog.pot102
-rw-r--r--fietsboek/routes.py3
-rw-r--r--fietsboek/static/osm-monkeypatch.js306
-rw-r--r--fietsboek/templates/layout.jinja24
-rw-r--r--fietsboek/views/tileproxy.py313
-rw-r--r--poetry.lock297
-rw-r--r--pylint.toml2
-rw-r--r--pyproject.toml5
14 files changed, 1161 insertions, 141 deletions
diff --git a/development.ini b/development.ini
index eb33586..3259d3b 100644
--- a/development.ini
+++ b/development.ini
@@ -18,6 +18,7 @@ pyramid.includes =
pyramid_debugtoolbar
sqlalchemy.url = sqlite:///%(here)s/fietsboek.sqlite
+redis.url = redis://localhost/
fietsboek.data_dir = %(here)s/data
retry.attempts = 3
diff --git a/doc/administration/configuration.rst b/doc/administration/configuration.rst
index 5268903..feb3e40 100644
--- a/doc/administration/configuration.rst
+++ b/doc/administration/configuration.rst
@@ -36,51 +36,164 @@ Most of the configuration is in the ``[app:main]`` category and looks like this:
sqlalchemy.url = sqlite:///%(here)s/fietsboek.sqlite
fietsboek.data_dir = %(here)s/data
+ redis.url = redis://localhost/
retry.attempts = 3
-* You should leave the ``use``, ``pyramid.reload_templates`` and
- ``pyramid.debug_*`` settings as they are.
-* ``pyramid.default_locale_name`` can be used to set the default language of
- the installation. Note that Fietsboek will try to detect the user's language,
- so the ``default_locale_name`` is used as a fallback.
-* ``available_locales`` sets the list of available languages. Currently,
- Fietsboek ships with English ("en") and German ("de"). Removing a language
- from this list will make it unavailable. If you create a custom language
- locally, make sure to add it to this list here!
-* ``enable_account_registration`` can be used to enable and disable the
- creation of new accounts via the web interface, for example if you want to
- have a private instance. New accounts can always be created using the CLI
- management tool.
-* ``session_key`` should be set to a random string of characters. This is the
- key used to sign session data, so it should not get into wrong hands!
-* ``sqlalchemy.url`` is the URL to the database. See the `SQLAlchemy
- documentation
- <https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls>`__ for
- more information.
-* ``fietsboek.data_dir`` sets the directory for data uploads. This directory
- must be writable by the Fietsboek process, as Fietsboek will save track
- images in there.
-* ``fietsboek.pages`` see :doc:`custom-pages`.
-* ``email.from`` sets the sender of emails, for example for account verifications.
-* ``email.smtp_url`` sets the URL of the SMTP server. The following formats are accepted:
-
- * ``debug://`` a debug implementation that simply prints emails to the
- standard output. Should not be used in production, as no emails would ever
- arrive.
- * ``smtp://host:port`` use the given SMTP server (without transport encryption!)
- * ``smtp+ssl://host:port`` use the given SMTP server over a SSL connection
- * ``smtp+starttls://host:port`` use the given SMTP server and the STARTTLS
- command to start an encrypted channel.
-
-* ``email.username`` and ``email.password`` can be used to set the login
- information for the SMTP server.
-* ``thunderforest.api_key`` can be set to an API key of `Thunderforest
- <https://www.thunderforest.com/>`__ to enable support for the OpenCycleMap,
- Landscape and Outdoors maps.
+General Settings
+----------------
+Use ``enable_account_registration`` enable or disable the creation of new
+accounts via the web interface, for example if you want to have a private
+instance. New accounts can always be created using the CLI management tool.
+
+Set ``session_key`` to a random string of characters. This is the key used to
+sign session data, so it should not get into wrong hands!
+
+You can set up custom pages using ``fietsboek.pages``. See :doc:`custom-pages`
+for more information.
+
+Pyramid Settings
+----------------
+
+You should leave the ``use``, ``pyramid.reload_templates`` and
+``pyramid.debug_*`` settings as they are. Refer to the `Pyramid documentation
+<https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html>`__
+for more information.
+
+Language Settings
+-----------------
+
+You can set the default language with the ``pyramid.default_locale_name``
+setting. Note that Fietsboek will try to detect the user's language, so the
+``default_locale_name`` is used as a fallback.
+
+You can use ``available_locales`` to set the list of available languages.
+Currently, Fietsboek ships with English ("en") and German ("de"). Removing a
+language from this list will make it unavailable. If you create a custom
+language locally, make sure to add it to this list here!
+
+Database Settings
+-----------------
+
+Fietsboek uses three different databases:
+A SQL database for persistent data (like user accounts), a file storage on the
+disk for big files (like images), and a redis server for ephemeral data (like
+cached tiles).
+
+Set ``sqlalchemy.url`` to the URL of the SQL database. See the `SQLAlchemy
+documentation
+<https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls>`__ for more
+information on available URL formats. Make sure to install the driver necessary
+to communicate with your database (e.g. ``psycopg2`` for PostreSQL)!
+
+Set ``fietsboek.data_dir`` to the directory for data uploads. This directory
+must be writable by the Fietsboek process, as Fietsboek will save track images
+in there.
+
+Set ``redis.url`` to the URL of the redis instance. See the `redis module
+documentation
+<https://redis.readthedocs.io/en/latest/connections.html#redis.Redis.from_url>`__
+for information about the possible syntaxes of this URL. Note that the redis
+server is only used for caching and temporary data, so don't sweat to make it
+persistent. A container running redis is fine.
+
+.. note::
+
+ Fietsboek will cache map tiles in the redis server.
+ To avoid using up too much memory, consider setting a maximum memory size
+ and policy in redis:
+
+ https://redis.io/docs/management/config/#configuring-redis-as-a-cache
+
+Email Settings
+--------------
+
+Use ``email.from`` to set the sender of emails, for example for account verifications.
+
+Set ``email.smtp_url`` to the URL of the SMTP server. The following formats are
+accepted:
+
+* ``debug://`` a debug implementation that simply prints emails to the
+ standard output. Should not be used in production, as no emails would ever
+ arrive.
+* ``smtp://host:port`` use the given SMTP server (without transport encryption!)
+* ``smtp+ssl://host:port`` use the given SMTP server over a SSL connection
+* ``smtp+starttls://host:port`` use the given SMTP server and the STARTTLS
+ command to start an encrypted channel.
+
+Use ``email.username`` and ``email.password`` to set the login credentials for
+the SMTP server.
+
+Map Layers & Thunderforest Integration
+--------------------------------------
+
+By default, Fietsboek offers the following map layers:
+
+* ``osm``: `OpenStreetMap <https://www.openstreetmap.org>`__
+* ``osmde``: `OpenStreetMap Deutschland <https://www.openstreetmap.de/>`__
+* ``satellite``: Satellite imaging from `Esri <https://www.esri.com>`__
+* ``opentopo``: `OpenTopoMap <https://opentopomap.org/>`__
+* ``topplusopen``: `TopPlus-Open
+ <https://www.bkg.bund.de/SharedDocs/Produktinformationen/BKG/EN/P-2017/171114-TopPlus-Web-Open.html>`__
+
+As well as the following overlay layers:
+
+* ``opensea``: `OpenSeaMap <https://openseamap.org>`__
+* ``cycling``: `Waymarked Trails: Cycling <https://cycling.waymarkedtrails.org>`__
+* ``hiking``: `Waymarked Trails: Hiking <https://hiking.waymarkedtrails.org/>`__
+
+You can use ``fietsboek.default_tile_layers`` to set the list of activated
+layers (by default, all of them), for example:
+
+.. code:: ini
+
+ fietsboek.default_tile_layers = osm osmde cycling
+
+You can enable `Thunderforest <https://www.thunderforest.com>`__ support by
+setting ``thunderforest.api_key``, and ``thunderforest.maps`` to a list of
+Thunderforest maps (e.g. "cycle" or "landscape"). By default, only logged in
+users will be able to use the Thunderforest maps (to protect your quota), this
+can be changed by setting ``thunderforest.access = public`` (default is
+"restricted").
+
+You can add custom tile layers in the following way:
+
+.. code:: ini
+
+ fietsboek.tile_layer.ID = My Custom Layer
+ fietsboek.tile_layer.ID.url = https://tiles.example.com/{z}/{x}/{y}.png
+ # Optional, set the type (base or overlay), default base
+ fietsboek.tile_layer.ID.type = base
+ # Optional, set the maximum zoom factor, default 22
+ fietsboek.tile_layer.ID.zoom = 22
+ # Optional, set the attribution
+ fietsboek.tile_layer.ID.attribution = Copyright Example
+ # Optional, set the access restriction (public or restricted), default
+ # public
+ fietsboek.tile_layer.ID.access = public
+
+``ID`` must be an alphanumerical identifier.
+
+By default, Fietsboek will proxy all tile requests through the Fietsboek
+instance. While this can slow down the user experience, it has the following
+benefits:
+
+* Your users' IPs stay private and protected, as no third party is contacted.
+ The tile servers will only see the IP from the Fietsboek server.
+* If you use private tile servers or servers that require a key, your key is
+ protected as it will not be given out to the users.
+* Fietsboek caches tile requests, which reduces the strain on the providers and
+ might even make maps faster if many people use them.
+
+You can disable the tile proxy by setting ``fietsboek.tile_proxy.disable =
+true``. This will cause the tiles to be loaded directly by the client.
.. warning::
- The API key will be embedded in the source of the website, therefore it is
- possible for visitors to "steal" the API key. Keep that in mind when
- setting an API key for a publicly accessible site!
+ If you disable the tile proxy, all tile source URLs will be given to the
+ user. If you use API keys or other private sources, **those keys will be
+ leaked to the users**.
+
+ In addition, depending on the jurisdiction, you might be required to tell
+ your users that third party content is included in your site, and that
+ their IP will be accessible to the third party.
diff --git a/doc/administration/installation.rst b/doc/administration/installation.rst
index b8ede06..4af0941 100644
--- a/doc/administration/installation.rst
+++ b/doc/administration/installation.rst
@@ -3,6 +3,18 @@ Installation
This document will outline the installation process of Fietsboek, step-by-step.
+Requirements
+------------
+
+Fietsboek has the following requirements (apart from the Python modules, which
+will be installed by ``pip``):
+
+* Python 3.7 or later
+* A `redis <https://redis.com/>`__ server, used for caching and temporary data
+* (Optionally) an SQL database server like `PostgreSQL
+ <https://www.postgresql.org/>`__ or `MariaDB <https://mariadb.org/>`__ (if
+ SQLite is not enough)
+
Creating an Environment
-----------------------
diff --git a/doc/conf.py b/doc/conf.py
index e7f04e0..7769cf1 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -36,6 +36,8 @@ intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'sqlalchemy': ('https://docs.sqlalchemy.org/en/14/', None),
+ 'jinja2': ('https://jinja.palletsprojects.com/en/3.0.x/', None),
+ 'markupsafe': ('https://markupsafe.palletsprojects.com/en/2.1.x/', None),
}
# Add any paths that contain templates here, relative to this directory.
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index a727230..d9077d5 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -4,6 +4,8 @@ For more information, see the README or the included documentation.
"""
from pathlib import Path
+import importlib_metadata
+import redis
from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory
from pyramid.csrf import CookieCSRFStoragePolicy
@@ -16,6 +18,9 @@ from .pages import Pages
from . import jinja2 as fiets_jinja2
+__VERSION__ = importlib_metadata.version('fietsboek')
+
+
def locale_negotiator(request):
"""Negotiates the right locale to use.
@@ -46,7 +51,8 @@ def locale_negotiator(request):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- # pylint: disable=unused-argument
+ # pylint: disable=unused-argument, import-outside-toplevel, cyclic-import
+ from .views import tileproxy
if settings.get('session_key', '<EDIT THIS>') == '<EDIT THIS>':
raise ValueError("Please set a session signing key (session_key) in your settings!")
@@ -57,12 +63,23 @@ def main(global_config, **settings):
data_dir = request.registry.settings["fietsboek.data_dir"]
return DataManager(Path(data_dir))
+ def redis_(request):
+ return redis.from_url(request.registry.settings["redis.url"])
+
settings['enable_account_registration'] = asbool(
settings.get('enable_account_registration', 'false'))
settings['available_locales'] = aslist(
settings.get('available_locales', 'en'))
settings['fietsboek.pages'] = aslist(
settings.get('fietsboek.pages', ''))
+ settings['fietsboek.tile_proxy.disable'] = asbool(
+ settings.get('fietsboek.tile_proxy.disable', 'false'))
+ settings['thunderforest.maps'] = aslist(
+ settings.get('thunderforest.maps', ''))
+ settings['fietsboek.default_tile_layers'] = aslist(
+ settings.get('fietsboek.default_tile_layers',
+ 'osm satellite osmde opentopo topplusopen opensea cycling hiking'))
+ settings['fietsboek.tile_layers'] = tileproxy.extract_tile_layers(settings)
# Load the pages
page_manager = Pages()
@@ -90,10 +107,12 @@ def main(global_config, **settings):
config.set_locale_negotiator(locale_negotiator)
config.add_request_method(data_manager, reify=True)
config.add_request_method(pages, reify=True)
+ config.add_request_method(redis_, name="redis", reify=True)
jinja2_env = config.get_jinja2_environment()
jinja2_env.filters['format_decimal'] = fiets_jinja2.filter_format_decimal
jinja2_env.filters['format_datetime'] = fiets_jinja2.filter_format_datetime
jinja2_env.filters['local_datetime'] = fiets_jinja2.filter_local_datetime
+ jinja2_env.globals['embed_tile_layers'] = fiets_jinja2.global_embed_tile_layers
return config.make_wsgi_app()
diff --git a/fietsboek/jinja2.py b/fietsboek/jinja2.py
index 6043791..a0b9457 100644
--- a/fietsboek/jinja2.py
+++ b/fietsboek/jinja2.py
@@ -1,5 +1,6 @@
"""Custom filters for Jinja2."""
import datetime
+import json
import jinja2
from markupsafe import Markup
@@ -77,3 +78,39 @@ def filter_local_datetime(ctx, value):
return Markup(
f'<span class="fietsboek-local-datetime" data-utc-timestamp="{timestamp}">{fallback}</span>'
)
+
+
+def global_embed_tile_layers(request):
+ """Renders the available tile servers for the current user, as a JSON object.
+
+ The returned value is wrapped as a :class:`~markupsafe.Markup` so that it
+ won't get escaped by jinja.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The available tile servers.
+ :rtype: markupsafe.Markup
+ """
+ # pylint: disable=import-outside-toplevel,cyclic-import
+ from .views import tileproxy
+ tile_sources = tileproxy.sources_for(request)
+
+ if request.registry.settings.get("fietsboek.tile_proxy.disable"):
+ def _url(source):
+ return source.url_template
+ else:
+ def _url(source):
+ return (request.route_url("tile-proxy", provider=source.key, x="{x}", y="{y}", z="{z}")
+ .replace("%7Bx%7D", "{x}")
+ .replace("%7By%7D", "{y}")
+ .replace("%7Bz%7D", "{z}"))
+
+ return Markup(json.dumps([
+ {
+ "name": source.name,
+ "url": _url(source),
+ "attribution": source.attribution,
+ "type": source.layer_type.value,
+ }
+ for source in tile_sources
+ ]))
diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot
index 91390c8..abb2bfc 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -8,14 +8,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2022-08-10 13:36+0200\n"
+"POT-Creation-Date: 2022-11-15 23:42+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.10.3\n"
+"Generated-By: Babel 2.11.0\n"
#: fietsboek/util.py:282
msgid "password_constraint.mismatch"
@@ -153,43 +153,43 @@ msgstr ""
msgid "page.browse.synthetic_tooltip"
msgstr ""
-#: fietsboek/templates/browse.jinja2:132 fietsboek/templates/details.jinja2:88
+#: fietsboek/templates/browse.jinja2:132 fietsboek/templates/details.jinja2:90
msgid "page.details.date"
msgstr ""
-#: fietsboek/templates/browse.jinja2:134 fietsboek/templates/details.jinja2:102
+#: fietsboek/templates/browse.jinja2:134 fietsboek/templates/details.jinja2:104
msgid "page.details.length"
msgstr ""
-#: fietsboek/templates/browse.jinja2:139 fietsboek/templates/details.jinja2:93
+#: fietsboek/templates/browse.jinja2:139 fietsboek/templates/details.jinja2:95
msgid "page.details.start_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:141 fietsboek/templates/details.jinja2:97
+#: fietsboek/templates/browse.jinja2:141 fietsboek/templates/details.jinja2:99
msgid "page.details.end_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:146 fietsboek/templates/details.jinja2:106
+#: fietsboek/templates/browse.jinja2:146 fietsboek/templates/details.jinja2:108
msgid "page.details.uphill"
msgstr ""
-#: fietsboek/templates/browse.jinja2:148 fietsboek/templates/details.jinja2:110
+#: fietsboek/templates/browse.jinja2:148 fietsboek/templates/details.jinja2:112
msgid "page.details.downhill"
msgstr ""
-#: fietsboek/templates/browse.jinja2:153 fietsboek/templates/details.jinja2:115
+#: fietsboek/templates/browse.jinja2:153 fietsboek/templates/details.jinja2:117
msgid "page.details.moving_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:155 fietsboek/templates/details.jinja2:119
+#: fietsboek/templates/browse.jinja2:155 fietsboek/templates/details.jinja2:121
msgid "page.details.stopped_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:159 fietsboek/templates/details.jinja2:123
+#: fietsboek/templates/browse.jinja2:159 fietsboek/templates/details.jinja2:125
msgid "page.details.max_speed"
msgstr ""
-#: fietsboek/templates/browse.jinja2:161 fietsboek/templates/details.jinja2:127
+#: fietsboek/templates/browse.jinja2:161 fietsboek/templates/details.jinja2:129
msgid "page.details.avg_speed"
msgstr ""
@@ -293,40 +293,40 @@ msgstr ""
msgid "page.details.delete.close"
msgstr ""
-#: fietsboek/templates/details.jinja2:69
+#: fietsboek/templates/details.jinja2:70
msgid "page.details.tags"
msgstr ""
-#: fietsboek/templates/details.jinja2:78 fietsboek/templates/edit.jinja2:10
+#: fietsboek/templates/details.jinja2:80 fietsboek/templates/edit.jinja2:10
#: fietsboek/templates/finish_upload.jinja2:10
msgid "page.noscript"
msgstr ""
-#: fietsboek/templates/details.jinja2:83
+#: fietsboek/templates/details.jinja2:85
msgid "page.details.download"
msgstr ""
-#: fietsboek/templates/details.jinja2:172
+#: fietsboek/templates/details.jinja2:174
msgid "page.details.comments"
msgstr ""
-#: fietsboek/templates/details.jinja2:176
+#: fietsboek/templates/details.jinja2:178
msgid "page.details.comments.author"
msgstr ""
-#: fietsboek/templates/details.jinja2:193
+#: fietsboek/templates/details.jinja2:195
msgid "page.details.comments.new.title"
msgstr ""
-#: fietsboek/templates/details.jinja2:196
+#: fietsboek/templates/details.jinja2:198
msgid "page.details.comments.new.input_title"
msgstr ""
-#: fietsboek/templates/details.jinja2:197
+#: fietsboek/templates/details.jinja2:199
msgid "page.details.comments.new.input_comment"
msgstr ""
-#: fietsboek/templates/details.jinja2:200
+#: fietsboek/templates/details.jinja2:202
msgid "page.details.comments.new.submit"
msgstr ""
@@ -394,40 +394,40 @@ msgstr ""
msgid "page.track.form.tags"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:43
+#: fietsboek/templates/edit_form.jinja2:50
msgid "page.track.form.add_tag"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:48
+#: fietsboek/templates/edit_form.jinja2:55
msgid "page.track.form.tagged_people"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:63
+#: fietsboek/templates/edit_form.jinja2:70
msgid "page.track.form.add_friend"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:83
+#: fietsboek/templates/edit_form.jinja2:90
msgid "page.track.form.badges"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:94
+#: fietsboek/templates/edit_form.jinja2:101
msgid "page.track.form.description"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:101
-#: fietsboek/templates/edit_form.jinja2:115
+#: fietsboek/templates/edit_form.jinja2:108
+#: fietsboek/templates/edit_form.jinja2:122
msgid "page.track.form.remove_image"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:110
+#: fietsboek/templates/edit_form.jinja2:117
msgid "page.track.form.select_images"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:126
+#: fietsboek/templates/edit_form.jinja2:133
msgid "page.track.form.image_description_modal"
msgstr ""
-#: fietsboek/templates/edit_form.jinja2:133
+#: fietsboek/templates/edit_form.jinja2:140
msgid "page.track.form.image_description_modal.save"
msgstr ""
@@ -457,43 +457,43 @@ msgstr ""
msgid "page.navbar.toggle"
msgstr ""
-#: fietsboek/templates/layout.jinja2:46
+#: fietsboek/templates/layout.jinja2:51
msgid "page.navbar.home"
msgstr ""
-#: fietsboek/templates/layout.jinja2:49
+#: fietsboek/templates/layout.jinja2:54
msgid "page.navbar.browse"
msgstr ""
-#: fietsboek/templates/layout.jinja2:53
+#: fietsboek/templates/layout.jinja2:58
msgid "page.navbar.upload"
msgstr ""
-#: fietsboek/templates/layout.jinja2:57
+#: fietsboek/templates/layout.jinja2:67
msgid "page.navbar.user"
msgstr ""
-#: fietsboek/templates/layout.jinja2:61
+#: fietsboek/templates/layout.jinja2:71
msgid "page.navbar.welcome_user"
msgstr ""
-#: fietsboek/templates/layout.jinja2:64
+#: fietsboek/templates/layout.jinja2:74
msgid "page.navbar.logout"
msgstr ""
-#: fietsboek/templates/layout.jinja2:67
+#: fietsboek/templates/layout.jinja2:77
msgid "page.navbar.profile"
msgstr ""
-#: fietsboek/templates/layout.jinja2:71
+#: fietsboek/templates/layout.jinja2:81
msgid "page.navbar.admin"
msgstr ""
-#: fietsboek/templates/layout.jinja2:77
+#: fietsboek/templates/layout.jinja2:87
msgid "page.navbar.login"
msgstr ""
-#: fietsboek/templates/layout.jinja2:81
+#: fietsboek/templates/layout.jinja2:91
msgid "page.navbar.create_account"
msgstr ""
@@ -645,43 +645,43 @@ msgstr ""
msgid "flash.badge_deleted"
msgstr ""
-#: fietsboek/views/default.py:75
+#: fietsboek/views/default.py:96
msgid "flash.invalid_credentials"
msgstr ""
-#: fietsboek/views/default.py:79
+#: fietsboek/views/default.py:100
msgid "flash.account_not_verified"
msgstr ""
-#: fietsboek/views/default.py:82
+#: fietsboek/views/default.py:103
msgid "flash.logged_in"
msgstr ""
-#: fietsboek/views/default.py:96
+#: fietsboek/views/default.py:117
msgid "flash.logged_out"
msgstr ""
-#: fietsboek/views/default.py:127
+#: fietsboek/views/default.py:148
msgid "flash.reset_invalid_email"
msgstr ""
-#: fietsboek/views/default.py:132
+#: fietsboek/views/default.py:153
msgid "flash.password_token_generated"
msgstr ""
-#: fietsboek/views/default.py:137
+#: fietsboek/views/default.py:158
msgid "page.password_reset.email.subject"
msgstr ""
-#: fietsboek/views/default.py:141
+#: fietsboek/views/default.py:162
msgid "page.password_reset.email.body"
msgstr ""
-#: fietsboek/views/default.py:168
+#: fietsboek/views/default.py:189
msgid "flash.email_verified"
msgstr ""
-#: fietsboek/views/default.py:182
+#: fietsboek/views/default.py:203
msgid "flash.password_updated"
msgstr ""
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index ab1eabf..9286f13 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -54,3 +54,6 @@ def includeme(config):
config.add_route('delete-friend', '/me/delete-friend')
config.add_route('accept-friend', '/me/accept-friend')
config.add_route('json-friends', '/me/friends.json')
+
+ config.add_route('tile-proxy',
+ '/tile/{provider}/{z:\\d+}/{x:\\d+}/{y:\\d+}')
diff --git a/fietsboek/static/osm-monkeypatch.js b/fietsboek/static/osm-monkeypatch.js
new file mode 100644
index 0000000..a8310a2
--- /dev/null
+++ b/fietsboek/static/osm-monkeypatch.js
@@ -0,0 +1,306 @@
+/* We want to override JB.Map to add our own maps instead.
+ * We do this by (ab)using the JS property system to override the setter, so
+ * that JB.Map won't actually set the new function. This means we don't have to
+ * source-patch the gmutils.js file.
+ */
+"use strict";
+
+(() => {
+ let ourMap = function(makemap) {
+ var dieses = this;
+ var id = makemap.id;
+ var mapcanvas = makemap.mapdiv;
+ dieses.id = id;
+ dieses.makemap = makemap;
+ dieses.mapcanvas = mapcanvas;
+ this.cluster_zoomhistory = [];
+
+ // Map anlegen
+
+ const mycp = '<a href="https://www.j-berkemeier.de/GPXViewer" title="GPX Viewer '+JB.GPX2GM.ver+'">GPXViewer</a> | ';
+
+ this.baseLayers = {};
+ this.overlayLayers = {};
+
+ for (let layer of TILE_LAYERS) {
+ if (layer.type === "base") {
+ this.baseLayers[layer.name] = L.tileLayer(layer.url, {
+ maxZoom: layer.zoom,
+ attribution: layer.attribution,
+ });
+ } else if (layer.type === "overlay") {
+ this.overlayLayers[layer.name] = L.tileLayer(layer.url, {
+ attribution: layer.attribution,
+ });
+ }
+ }
+
+ // https://tileserver.4umaps.com/${z}/${x}/${y}.png
+ // zoomlevel 16
+ // https://www.4umaps.com/
+
+ this.baseLayers[JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].noMap]= L.tileLayer(JB.GPX2GM.Path+"Icons/Grau256x256.png", {
+ maxZoom: 22,
+ attribution: mycp
+ });
+
+ this.layerNameTranslate = {
+ satellit: "Satellit",
+ satellite: "Satellit",
+ osm: "OSM",
+ osmde: "OSMDE",
+ opentopo: "Open Topo",
+ topplusopen: "TopPlusOpen",
+ cycle: "Cycle",
+ landscape: "Landscape",
+ outdoors: "Outdoors",
+ keinekarte: "Keine Karte",
+ pasdecarte: "Pas de carte",
+ nomap: "No Map",
+ ning\u00FAnmapa: "Ning\u00FAn Mapa",
+ nessunamappa: "Nessuna mappa",
+ opensea: "Open Sea",
+ hiking: "Hiking",
+ cycling: "Cycling",
+ }
+
+ // ['hiking', 'cycling', 'mtb', 'skating', 'slopes', 'riding'];
+
+ var genugplatz = JB.platzgenug(makemap.mapdiv);
+
+ this.map = L.map(mapcanvas, {
+ // layers: osm,
+ closePopupOnClick: false,
+ scrollWheelZoom: genugplatz & makemap.parameters.scrollwheelzoom,
+ tap: genugplatz,
+ keyboard: genugplatz,
+ touchZoom: true,
+ dragging: true,
+ } );
+
+ JB.handle_touch_action(dieses,genugplatz);
+
+ if(makemap.parameters.unit=="si") L.control.scale({imperial:false}).addTo(this.map); // Mit Maßstab km
+ else L.control.scale({metric:false}).addTo(this.map); // Mit Maßstab ml
+
+ var ctrl_layer = null;
+ var showmaptypecontroll_save = makemap.parameters.showmaptypecontroll;
+ JB.onresize(mapcanvas,function(w,h) {
+ makemap.parameters.showmaptypecontroll = (w>200 && h>190 && showmaptypecontroll_save);
+ if(makemap.parameters.showmaptypecontroll) {
+ if(!ctrl_layer) ctrl_layer = L.control.layers(dieses.baseLayers, dieses.overlayLayers).addTo(dieses.map);
+ }
+ else {
+ if(ctrl_layer) {
+ ctrl_layer.remove();
+ ctrl_layer = null;
+ }
+ }
+ },true);
+
+ // Button für Full Screen / normale Größe
+ var fullscreen = false;
+ if(makemap.parameters.fullscreenbutton) {
+ var fsb = document.createElement("button");
+ fsb.style.backgroundColor = "transparent";
+ fsb.style.border = "none";
+ fsb.style.padding = "7px 7px 7px 0";
+ fsb.style.cursor = "pointer";
+ var fsbim = document.createElement("img");
+ fsbim.width = 31;
+ fsbim.height = 31;
+ fsbim.src = JB.GPX2GM.Path+"Icons/fullscreen_p.svg";
+ fsb.title = fsbim.title = fsbim.alt = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].fullScreen;
+ fsbim.large = false;
+ var ele = mapcanvas.parentNode;
+ fsb.onclick = function() {
+ this.blur();
+ if(fsbim.large) {
+ document.body.style.overflow = "";
+ fsbim.src = JB.GPX2GM.Path+"Icons/fullscreen_p.svg";
+ fsb.title = fsbim.title = fsbim.alt = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].fullScreen;
+ ele.style.left = ele.oleft + "px";
+ ele.style.top = ele.otop + "px";
+ ele.style.width = ele.owidth + "px";
+ ele.style.height = ele.oheight + "px";
+ ele.style.margin = ele.omargin;
+ ele.style.padding = ele.opadding;
+ window.setTimeout(function() {
+ JB.removeClass("JBfull",ele);
+ ele.style.position = ele.sposition;
+ ele.style.left = ele.sleft;
+ ele.style.top = ele.stop;
+ ele.style.width = ele.swidth;
+ ele.style.height = ele.sheight;
+ //ele.style.zIndex = ele.szindex;
+ },1000);
+ JB.handle_touch_action(dieses,genugplatz);
+ fullscreen = false;
+ }
+ else {
+ document.body.style.overflow = "hidden";
+ fsbim.src = JB.GPX2GM.Path+"Icons/fullscreen_m.svg";
+ fsb.title = fsbim.title = fsbim.alt = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].normalSize;
+ var scrollY = 0;
+ if(document.documentElement.scrollTop && document.documentElement.scrollTop!=0) scrollY = document.documentElement.scrollTop;
+ else if(document.body.scrollTop && document.body.scrollTop!=0) scrollY = document.body.scrollTop;
+ else if(window.scrollY) scrollY = window.scrollY;
+ else if(window.pageYOffset) scrollY = window.pageYOffset;
+ var rect = JB.getRect(ele);
+ ele.oleft = rect.left;
+ ele.otop = rect.top - scrollY;
+ ele.owidth = rect.width;
+ ele.oheight = rect.height;
+ //ele.szindex = ele.style.zIndex;
+ ele.sposition = ele.style.position;
+ ele.omargin = ele.style.margin;
+ ele.opadding = ele.style.padding;
+ ele.sleft = ele.style.left;
+ ele.stop = ele.style.top;
+ ele.swidth = ele.style.width;
+ ele.sheight = ele.style.height;
+ ele.style.position = "fixed";
+ ele.style.left = ele.oleft+"px";
+ ele.style.top = ele.otop+"px";
+ ele.style.width = ele.owidth+"px";
+ ele.style.height = ele.oheight+"px";
+ //ele.style.zIndex = "1001";
+ window.setTimeout(function() {
+ JB.addClass("JBfull",ele);
+ ele.style.width = "100%";
+ ele.style.height = "100%";
+ ele.style.left = "0px";
+ ele.style.top = "0px";
+ ele.style.margin = "0px";
+ ele.style.padding = "0px";
+ },100);
+ dieses.map.scrollWheelZoom.enable();
+ JB.handle_touch_action(dieses,true);
+ makemap.mapdiv.focus();
+ fullscreen = true;
+ }
+ fsbim.large = !fsbim.large;
+ };
+ fsb.appendChild(fsbim);
+ fsb.index = 0;
+ L.Control.Fsbutton = L.Control.extend({
+ onAdd: function(map) {
+ return fsb;
+ }
+ });
+ var fsbutton = new L.Control.Fsbutton({ position: 'topright' });
+ fsbutton.addTo(this.map);
+ } // fullscreenbutton
+
+ // Button für Traffic-Layer
+ if(makemap.parameters.trafficbutton) {
+ console.warn("Traffic-Layer wird unter Leaflet (noch) nicht unterstützt.");
+ }
+
+ // Button für Anzeige aktuelle Position
+ if(makemap.parameters.currentlocationbutton) {
+ var clb = document.createElement("button");
+ clb.style.backgroundColor = "white";
+ clb.style.border = "none";
+ clb.style.width = "28px";
+ clb.style.height = "28px";
+ clb.style.margin = "10px 10px 0 0";
+ clb.style.borderRadius = "2px";
+ clb.style.cursor = "pointer";
+ clb.title = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].showCurrentLocation;
+ var clbimg = document.createElement("img");
+ clbimg.style.position = "absolute";
+ clbimg.style.top = "50%";
+ clbimg.style.left = "50%";
+ clbimg.style.transform = "translate(-50%, -50%)";
+ clbimg.src = JB.GPX2GM.Path+"Icons/whereami.svg";
+ var wpid = -1, marker = null, first;
+ clb.onclick = function() {
+ this.blur();
+ if (navigator.geolocation) {
+ var geolocpos = function(position) {
+ var lat = position.coords.latitude;
+ var lon = position.coords.longitude;
+ marker.setLatLng([lat,lon]);
+ if(first) {
+ dieses.map.setView([lat,lon]);
+ first = false;
+ }
+ }
+ var geolocerror = function(error) {
+ var errorCodes = ["Permission Denied","Position unavailible","Timeout"];
+ var errorString = (error.code<=3)?errorCodes[error.code-1]:"Error code: "+error.code;
+ JB.Debug_Info("Geolocation-Dienst fehlgeschlagen!",errorString+". "+error.message,true);
+ }
+ first = true;
+ if(!marker) marker = dieses.Marker({lat:0,lon:0},JB.icons.CL)[0];
+ if ( wpid == -1 ) {
+ clb.title = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].hideCurrentLocation;
+ wpid = navigator.geolocation.watchPosition(geolocpos,geolocerror,{enableHighAccuracy:true, timeout: 5000, maximumAge: 60000});
+ marker.addTo(dieses.map);
+ JB.Debug_Info("","Geolocation-Dienst wird eingerichtet.",false);
+ }
+ else {
+ clb.title = JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].showCurrentLocation;
+ navigator.geolocation.clearWatch(wpid);
+ wpid = -1;
+ marker.remove();
+ JB.Debug_Info("","Geolocation-Dienst wird abgeschaltet.",false);
+ }
+ }
+ else JB.Debug_Info("geolocation","Geolocation wird nicht unterstützt!",true);
+ } // click-Handler
+ clb.appendChild(clbimg);
+ L.Control.Clbutton = L.Control.extend({
+ onAdd: function(map) {
+ return clb;
+ }
+ });
+ var clbutton = new L.Control.Clbutton({ position: 'topright' });
+ clbutton.addTo(this.map);
+ } // currentlocationbutton
+
+ // Scalieren nach MAP-Resize
+ dieses.zoomstatus = {};
+ dieses.zoomstatus.iszoomed = false;
+ dieses.zoomstatus.zoom_changed = function() {
+ dieses.zoomstatus.iszoomed = true;
+ dieses.zoomstatus.level = dieses.map.getZoom();
+ dieses.zoomstatus.w = mapcanvas.offsetWidth;
+ dieses.zoomstatus.h = mapcanvas.offsetHeight;
+ }
+ dieses.zoomstatus.move_end = function() {
+ dieses.zoomstatus.iszoomed = true;
+ dieses.mapcenter = dieses.map.getCenter();
+ }
+ dieses.map.on("moveend", dieses.zoomstatus.move_end);
+ JB.onresize(mapcanvas,function(w,h) {
+ if(w*h==0) return;
+ dieses.map.invalidateSize();
+ dieses.map.setView(dieses.mapcenter);
+ dieses.map.off("zoomend", dieses.zoomstatus.zoom_changed);
+ if(dieses.zoomstatus.iszoomed) {
+ var dz = Math.round(Math.min(Math.log(w/dieses.zoomstatus.w)/Math.LN2,Math.log(h/dieses.zoomstatus.h)/Math.LN2));
+ dieses.map.setZoom(dieses.zoomstatus.level+dz);
+ }
+ else {
+ if(dieses.bounds) {
+ dieses.map.fitBounds(dieses.bounds,{padding:[20,20]});
+ dieses.map.setView(dieses.mapcenter);
+ dieses.zoomstatus.level = dieses.map.getZoom();
+ dieses.zoomstatus.w = w;
+ dieses.zoomstatus.h = h;
+ }
+ }
+ if(!fullscreen) {
+ genugplatz = JB.platzgenug(makemap.mapdiv);
+ JB.handle_touch_action(dieses,genugplatz);
+ }
+ });
+ };
+ window.JB = window.JB || {};
+ Object.defineProperty(window.JB, "Map", {
+ get() { return ourMap; },
+ set(_) {},
+ });
+})();
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index 7371052..bf30143 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -20,6 +20,8 @@
<script>
const FRIENDS_URL = {{ request.route_url('json-friends') | tojson }};
+const TILE_LAYERS = {{ embed_tile_layers(request) }};
+const BASE_URL = {{ request.route_url('home') | tojson }};
const LOCALE = {{ request.localizer.locale_name.replace('_', '-') | tojson }};
{% if request.registry.settings.get("thunderforest.api_key") %}
{% set api_key = request.registry.settings.get("thunderforest.api_key") %}
@@ -112,6 +114,8 @@ window.JB.GPX2GM.OSM_Outdoors_Api_Key = {{ api_key | tojson }};
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{{request.static_url('fietsboek:static/bootstrap.bundle.min.js')}}"></script>
+ <!-- Our patch to the GPX viewer, load before the actual GPX viewer -->
+ <script src="{{request.static_url('fietsboek:static/osm-monkeypatch.js')}}"></script>
<!-- Jürgen Berkemeier's GPX viewer -->
<script src="{{request.static_url('fietsboek:static/GM_Utils/GPX2GM.js')}}"></script>
<script src="{{request.static_url('fietsboek:static/fietsboek.js')}}"></script>
diff --git a/fietsboek/views/tileproxy.py b/fietsboek/views/tileproxy.py
new file mode 100644
index 0000000..b9a32c4
--- /dev/null
+++ b/fietsboek/views/tileproxy.py
@@ -0,0 +1,313 @@
+"""Tile proxying layer.
+
+While this might slow down the initial load (as we now load everything through
+fietsboek), we can cache the OSM tiles per instance, and we can provide better
+access control for services like thunderforest.com.
+
+Additionally, this protects the users' IP, as only fietsboek can see it.
+"""
+import datetime
+import random
+import logging
+import re
+from enum import Enum
+from typing import NamedTuple
+from itertools import chain
+
+from pyramid.view import view_config
+from pyramid.response import Response
+from pyramid.httpexceptions import HTTPBadRequest, HTTPGatewayTimeout
+
+import requests
+from requests.exceptions import ReadTimeout
+
+from .. import __VERSION__
+
+
+class LayerType(Enum):
+ """Enum to distinguish base layers and overlay layers."""
+ BASE = "base"
+ OVERLAY = "overlay"
+
+
+class LayerAccess(Enum):
+ """Enum discerning whether a layer is publicly accessible or restriced to
+ logged-in users.
+
+ Note that in the future, a finer-grained distinction might be possible.
+ """
+ PUBLIC = "public"
+ RESTRICTED = "restricted"
+
+
+class TileSource(NamedTuple):
+ """Represents a remote server that can provide tiles to us."""
+ key: str
+ """Key to indicate this source in URLs."""
+ name: str
+ """Human-readable name of the source."""
+ url_template: str
+ """URL with placeholders."""
+ layer_type: LayerType
+ """Type of this layer."""
+ zoom: int
+ """Max zoom of this layer."""
+ access: LayerAccess
+ """Access restrictions to use this layer."""
+ attribution: str
+ """Attribution string."""
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+def _href(url, text):
+ return f'<a href="{url}" target="_blank">{text}</a>'
+
+
+_jb_copy = _href("https://www.j-berkemeier.de/GPXViewer", "GPXViewer")
+
+
+DEFAULT_TILE_LAYERS = [
+ # Main base layers
+ TileSource(
+ 'osm',
+ 'OSM',
+ 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 19,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ',
+ _href("https://www.openstreetmap.org/", "OpenStreetMap"), ' and contributors ',
+ _href("https://creativecommons.org/licenses/by-sa/2.0/", "CC-BY-SA"),
+ ]),
+ ),
+ TileSource(
+ 'satellite',
+ 'Satellit',
+ 'https://server.arcgisonline.com/ArcGIS/rest/services/'
+ 'World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ LayerType.BASE,
+ 21,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ', _href("https://www.esri.com", "Esri"),
+ ', i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, ',
+ 'IGP, UPR-EGP, and the GIS User Community',
+ ]),
+ ),
+ TileSource(
+ 'osmde',
+ 'OSMDE',
+ 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 19,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ',
+ _href("https://www.openstreetmap.org/", "OpenStreetMap"), ' and contributors ',
+ _href("https://creativecommons.org/licenses/by-sa/2.0/", "CC-BY-SA")
+ ]),
+ ),
+ TileSource(
+ 'opentopo',
+ 'Open Topo',
+ 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 17,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy,
+ ' | Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © ',
+ _href("https://opentopomap.org/about", "OpenTopoMap"), ' (CC-BY-SA)',
+ ]),
+ ),
+ TileSource(
+ 'topplusopen',
+ 'TopPlusOpen',
+ 'https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/'
+ '1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png',
+ LayerType.BASE,
+ 18,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Kartendaten: © ',
+ _href("https://www.bkg.bund.de/SharedDocs/Produktinformationen"
+ "/BKG/DE/P-2017/170922-TopPlus-Web-Open.html",
+ "Bundesamt für Kartographie und Geodäsie"),
+ ]),
+ ),
+
+ # Overlay layers
+ TileSource(
+ 'opensea',
+ 'OpenSea',
+ 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
+ LayerType.OVERLAY,
+ None,
+ LayerAccess.PUBLIC,
+ 'Kartendaten: © <a href="http://www.openseamap.org">OpenSeaMap</a> contributors',
+ ),
+ TileSource(
+ 'hiking',
+ 'Hiking',
+ 'https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png',
+ LayerType.OVERLAY,
+ None,
+ LayerAccess.PUBLIC,
+ f'&copy; {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} '
+ f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})',
+ ),
+ TileSource(
+ 'cycling',
+ 'Cycling',
+ 'https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png',
+ LayerType.OVERLAY,
+ None,
+ LayerAccess.PUBLIC,
+ f'&copy; {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} '
+ f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})',
+ ),
+]
+
+TTL = datetime.timedelta(days=7)
+"""Time to live of cached tiles."""
+
+TIMEOUT = datetime.timedelta(seconds=1.5)
+"""Timeout when requesting new tiles from a source server."""
+
+PUNISHMENT_TTL = datetime.timedelta(minutes=10)
+"""Block-out period after too many requests of a server have timed out."""
+
+PUNISHMENT_THRESHOLD = 10
+"""Block a provider after that many requests have timed out."""
+
+
+@view_config(route_name='tile-proxy', http_cache=3600)
+def tile_proxy(request):
+ """Requests the given tile from the proxy.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ if request.registry.settings.get("fietsboek.tile_proxy.disable"):
+ raise HTTPBadRequest("Tile proxying is disabled")
+
+ provider = request.matchdict['provider']
+ tile_sources = {source.key: source for source in sources_for(request)}
+ if provider not in tile_sources:
+ raise HTTPBadRequest("Invalid provider")
+
+ x, y, z = (int(request.matchdict['x']), int(request.matchdict['y']),
+ int(request.matchdict['z']))
+ cache_key = f"tile:{provider}-{x}-{y}-{z}"
+ content_type = "image/png"
+
+ cached = request.redis.get(cache_key)
+ if cached is not None:
+ return Response(cached, content_type=content_type)
+
+ timeout_tracker = f"provider-timeout:{provider}"
+ if int(request.redis.get(timeout_tracker) or "0") > PUNISHMENT_THRESHOLD:
+ # We've gotten too many timeouts from this provider recently, so avoid
+ # contacting it in the first place.
+ LOGGER.debug("Aborted attempt to contact %s due to previous timeouts", provider)
+ raise HTTPGatewayTimeout(f"Avoiding request to {provider}")
+
+ url = tile_sources[provider].url_template.format(x=x, y=y, z=z, s=random.choice("abc"))
+ headers = {
+ "user-agent": f"Fietsboek-Tile-Proxy/{__VERSION__}",
+ }
+ from_mail = request.registry.settings.get('email.from')
+ if from_mail:
+ headers["from"] = from_mail
+
+ try:
+ resp = requests.get(url, headers=headers, timeout=TIMEOUT.total_seconds())
+ except ReadTimeout:
+ LOGGER.debug("Proxy timeout when accessing %r", url)
+ request.redis.incr(timeout_tracker)
+ request.redis.expire(timeout_tracker, PUNISHMENT_TTL)
+ raise HTTPGatewayTimeout(f"No response in time from {provider}") from None
+ else:
+ try:
+ resp.raise_for_status()
+ except requests.HTTPError as exc:
+ LOGGER.info("Proxy request failed for %s: %s", provider, exc)
+ return Response(f"Failed to get tile from {provider}",
+ status_code=resp.status_code)
+ request.redis.set(cache_key, resp.content, ex=TTL)
+ return Response(resp.content, content_type=resp.headers.get("Content-type", content_type))
+
+
+def sources_for(request):
+ """Returns all eligible tile sources for the given request.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: A list of tile sources.
+ :rtype: list[TileSource]
+ """
+ settings = request.registry.settings
+ return [
+ source for source in chain(
+ (default_layer for default_layer in DEFAULT_TILE_LAYERS
+ if default_layer.key in settings["fietsboek.default_tile_layers"]),
+ settings["fietsboek.tile_layers"]
+ )
+ if source.access == LayerAccess.PUBLIC or request.identity is not None
+ ]
+
+
+def extract_tile_layers(settings):
+ """Extract all defined tile layers from the settings.
+
+ :param settings: The application settings.
+ :type settings: dict
+ :return: A list of extracted tile sources.
+ :rtype: list[TileSource]
+ """
+ layers = []
+ layers.extend(_extract_thunderforest(settings))
+ layers.extend(_extract_user_layers(settings))
+ return layers
+
+
+def _extract_thunderforest(settings):
+ # Thunderforest Shortcut!
+ tf_api_key = settings.get("thunderforest.api_key")
+ if tf_api_key:
+ tf_access = LayerAccess(settings.get("thunderforest.access", "restricted"))
+ tf_attribution = ' | '.join([
+ _jb_copy,
+ _href("https://www.thunderforest.com/", "Thunderforest"),
+ _href("https://www.openstreetmap.org/", "OpenStreetMap"),
+ ])
+ for tf_map in settings["thunderforest.maps"]:
+ url = (f"https://tile.thunderforest.com/{tf_map}/"
+ f"{{z}}/{{x}}/{{y}}.png?apikey={tf_api_key}")
+ yield TileSource(
+ f"tf-{tf_map}", f"TF {tf_map.title()}", url,
+ LayerType.BASE, 22, tf_access, tf_attribution,
+ )
+
+
+def _extract_user_layers(settings):
+ # Any other custom maps
+ for key in settings.keys():
+ match = re.match("^fietsboek\\.tile_layer\\.([A-Za-z0-9_-]+)$", key)
+ if not match:
+ continue
+
+ provider_id = match.group(1)
+ name = settings[key]
+ url = settings[f"{key}.url"]
+ layer_type = LayerType(settings.get(f"{key}.type", "base"))
+ zoom = int(settings.get(f"{key}.zoom", 22))
+ attribution = settings.get(f"{key}.attribution", _jb_copy)
+ access = LayerAccess(settings.get(f"{key}.access", "public"))
+
+ yield TileSource(provider_id, name, url, layer_type, zoom, access, attribution)
diff --git a/poetry.lock b/poetry.lock
index d9fb521..314a8c9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,6 +16,17 @@ SQLAlchemy = ">=1.3.0"
tz = ["python-dateutil"]
[[package]]
+name = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
+
+[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
@@ -72,6 +83,14 @@ css = ["tinycss2 (>=1.1.0,<1.2)"]
dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"]
[[package]]
+name = "certifi"
+version = "2022.9.24"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
@@ -83,6 +102,17 @@ python-versions = "*"
pycparser = "*"
[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode-backport = ["unicodedata2"]
+
+[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
@@ -136,6 +166,20 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
[[package]]
+name = "deprecated"
+version = "1.2.13"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"]
+
+[[package]]
name = "exceptiongroup"
version = "1.0.4"
description = "Backport of PEP 654 (exception groups)"
@@ -179,6 +223,14 @@ docs = ["Sphinx", "pylons-sphinx-themes", "watchdog"]
testing = ["mock", "pytest", "pytest-cov", "watchdog"]
[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
name = "importlib-metadata"
version = "5.0.0"
description = "Read metadata from Python packages"
@@ -276,7 +328,7 @@ name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "main"
-optional = true
+optional = false
python-versions = ">=3.6"
[package.dependencies]
@@ -367,7 +419,7 @@ name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
-optional = true
+optional = false
python-versions = ">=3.6.8"
[package.extras]
@@ -525,6 +577,25 @@ optional = false
python-versions = "*"
[[package]]
+name = "redis"
+version = "4.3.4"
+description = "Python client for Redis database and key-value store"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+async-timeout = ">=4.0.2"
+deprecated = ">=1.2.3"
+importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""}
+packaging = ">=20.4"
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+hiredis = ["hiredis (>=1.0.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
+
+[[package]]
name = "repoze-lru"
version = "0.7"
description = "A tiny LRU cache implementation and decorator"
@@ -537,8 +608,26 @@ docs = ["Sphinx"]
testing = ["coverage", "nose"]
[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
name = "setuptools"
-version = "65.5.1"
+version = "65.6.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
@@ -642,6 +731,19 @@ optional = false
python-versions = ">=3.7"
[[package]]
+name = "urllib3"
+version = "1.26.12"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
name = "venusian"
version = "3.0.0"
description = "A library for deferring decorator actions"
@@ -703,6 +805,14 @@ docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.8)"]
tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-cov"]
[[package]]
+name = "wrapt"
+version = "1.14.1"
+description = "Module for decorators, wrappers and monkey patching."
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
name = "zipp"
version = "3.10.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
@@ -731,7 +841,7 @@ test = ["zope.testrunner"]
[[package]]
name = "zope-interface"
-version = "5.5.1"
+version = "5.5.2"
description = "Interfaces for Python"
category = "main"
optional = false
@@ -768,13 +878,17 @@ testing = ["WebTest", "pytest", "pytest-cov"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
-content-hash = "5452a4dee7949b3ed0f47ddae85defaef989f8ffc06b77cfea1e0f3116e6a40c"
+content-hash = "ca923fd9e1fe5d896c832286f2227c4a0a73fcf56124b8b18e1d70bcef22cbe7"
[metadata.files]
alembic = [
{file = "alembic-1.8.1-py3-none-any.whl", hash = "sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4"},
{file = "alembic-1.8.1.tar.gz", hash = "sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa"},
]
+async-timeout = [
+ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
+ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
+]
attrs = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
@@ -791,6 +905,10 @@ bleach = [
{file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"},
{file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"},
]
+certifi = [
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
@@ -857,6 +975,10 @@ cffi = [
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
+charset-normalizer = [
+ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
+ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
+]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
@@ -945,6 +1067,10 @@ cryptography = [
{file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"},
{file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"},
]
+deprecated = [
+ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
+ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
+]
exceptiongroup = [
{file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
{file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
@@ -1018,6 +1144,10 @@ hupper = [
{file = "hupper-1.10.3-py2.py3-none-any.whl", hash = "sha256:f683850d62598c02faf3c7cdaaa727d8cbe3c5a2497a5737a8358386903b2601"},
{file = "hupper-1.10.3.tar.gz", hash = "sha256:cd6f51b72c7587bc9bce8a65ecd025a1e95f1b03284519bfe91284d010316cd9"},
]
+idna = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
importlib-metadata = [
{file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
{file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
@@ -1152,13 +1282,21 @@ pytz = [
{file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
{file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
]
+redis = [
+ {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"},
+ {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"},
+]
repoze-lru = [
{file = "repoze.lru-0.7-py3-none-any.whl", hash = "sha256:f77bf0e1096ea445beadd35f3479c5cff2aa1efe604a133e67150bc8630a62ea"},
{file = "repoze.lru-0.7.tar.gz", hash = "sha256:0429a75e19380e4ed50c0694e26ac8819b4ea7851ee1fc7583c8572db80aff77"},
]
+requests = [
+ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+]
setuptools = [
- {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
- {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
+ {file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"},
+ {file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1227,6 +1365,10 @@ typing-extensions = [
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
+urllib3 = [
+ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
+ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
+]
venusian = [
{file = "venusian-3.0.0-py3-none-any.whl", hash = "sha256:06e7385786ad3a15c70740b2af8d30dfb063a946a851dcb4159f9e2a2302578f"},
{file = "venusian-3.0.0.tar.gz", hash = "sha256:f6842b7242b1039c0c28f6feef29016e7e7dd3caaeb476a193acf737db31ee38"},
@@ -1247,6 +1389,72 @@ webtest = [
{file = "WebTest-3.0.0-py3-none-any.whl", hash = "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead"},
{file = "WebTest-3.0.0.tar.gz", hash = "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb"},
]
+wrapt = [
+ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
+ {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
+ {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
+ {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
+ {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
+ {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
+ {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
+ {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
+ {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
+ {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
+ {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
+ {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
+ {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
+ {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
+ {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
+ {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
+ {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
+ {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
+ {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
+ {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
+ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
+ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
+]
zipp = [
{file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
{file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
@@ -1256,45 +1464,42 @@ zope-deprecation = [
{file = "zope.deprecation-4.4.0.tar.gz", hash = "sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df"},
]
zope-interface = [
- {file = "zope.interface-5.5.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dd4b9251e95020c3d5d104b528dbf53629d09c146ce9c8dfaaf8f619ae1cce35"},
- {file = "zope.interface-5.5.1-cp27-cp27m-win32.whl", hash = "sha256:061a41a3f96f076686d7f1cb87f3deec6f0c9f0325dcc054ac7b504ae9bb0d82"},
- {file = "zope.interface-5.5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:7f2e4ebe0a000c5727ee04227cf0ff5ae612fe599f88d494216e695b1dac744d"},
- {file = "zope.interface-5.5.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:c9552ee9e123b7997c7630fb95c466ee816d19e721c67e4da35351c5f4032726"},
- {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a923d2dec50f2b4d41ce198af3516517f2e458220942cf393839d2f9e22000"},
- {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a20fc9cccbda2a28e8db8cabf2f47fead7e9e49d317547af6bf86a7269e4b9a1"},
- {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6f51ffbdcf865f140f55c484001415505f5e68eb0a9eab1d37d0743b503b423"},
- {file = "zope.interface-5.5.1-cp310-cp310-win32.whl", hash = "sha256:8de7bde839d72d96e0c92e8d1fdb4862e89b8fc52514d14b101ca317d9bcf87c"},
- {file = "zope.interface-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:90f611d4cdf82fb28837fe15c3940255755572a4edf4c72e2306dbce7dcb3092"},
- {file = "zope.interface-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489c4c46fcbd9364f60ff0dcb93ec9026eca64b2f43dc3b05d0724092f205e27"},
- {file = "zope.interface-5.5.1-cp311-cp311-win32.whl", hash = "sha256:9ad58724fabb429d1ebb6f334361f0a3b35f96be0e74bfca6f7de8530688b2df"},
- {file = "zope.interface-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:a69f6d8b639f2317ba54278b64fef51d8250ad2c87acac1408b9cc461e4d6bb6"},
- {file = "zope.interface-5.5.1-cp35-cp35m-win32.whl", hash = "sha256:d743b03a72fefed807a4512c079fb1aa5e7777036cc7a4b6ff79ae4650a14f73"},
- {file = "zope.interface-5.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:3e42b1c3f4fd863323a8275c52c78681281a8f2e1790f0e869d911c1c7b25c46"},
- {file = "zope.interface-5.5.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:7b4547a2f624a537e90fb99cec4d8b3b6be4af3f449c3477155aae65396724ad"},
- {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a96d499ff6faa9b85b1309f50bf3744eb786e24833f7b500cbb7052dc4ae29"},
- {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3c293c5c0e1cabe59c33e0d02fcee5c3eb365f79a20b8199a26ca784e406bd0d"},
- {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8c8764226daad39004b7873c3880eb4860c594ff549ea47c045cdf313e1bad5"},
- {file = "zope.interface-5.5.1-cp36-cp36m-win32.whl", hash = "sha256:4477930451521ac7da97cc31d49f7b83086d5ae76e52baf16aac659053119f6d"},
- {file = "zope.interface-5.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:27c53aa2f46d42940ccdcb015fd525a42bf73f94acd886296794a41f229d5946"},
- {file = "zope.interface-5.5.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:2204a9d545fdbe0d9b0bf4d5e2fc67e7977de59666f7131c1433fde292fc3b41"},
- {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:475b6e371cdbeb024f2302e826222bdc202186531f6dc095e8986c034e4b7961"},
- {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a1393229c9c126dd1b4356338421e8882347347ab6fe3230cb7044edc813e424"},
- {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e4988d94962f517f6da2d52337170b84856905b31b7dc504ed9c7b7e4bab2fc3"},
- {file = "zope.interface-5.5.1-cp37-cp37m-win32.whl", hash = "sha256:0eda7f61da6606a28b5efa5d8ad79b4b5bb242488e53a58993b2ec46c924ffee"},
- {file = "zope.interface-5.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:185f0faf6c3d8f2203e8755f7ca16b8964d97da0abde89c367177a04e36f2568"},
- {file = "zope.interface-5.5.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:026e7da51147910435950a46c55159d68af319f6e909f14873d35d411f4961db"},
- {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58331d2766e8e409360154d3178449d116220348d46386430097e63d02a1b6d2"},
- {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0587d238b7867544134f4dcca19328371b8fd03fc2c56d15786f410792d0a68"},
- {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd423d49abcf0ebf02c29c3daffe246ff756addb891f8aab717b3a4e2e1fd675"},
- {file = "zope.interface-5.5.1-cp38-cp38-win32.whl", hash = "sha256:13a7c6e3df8aa453583412de5725bf761217d06f66ff4ed776d44fbcd13ec4e4"},
- {file = "zope.interface-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:72a93445937cc71f0b8372b0c9e7c185328e0db5e94d06383a1cb56705df1df4"},
- {file = "zope.interface-5.5.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:6cb8f9a1db47017929634264b3fc7ea4c1a42a3e28d67a14f14aa7b71deaa0d2"},
- {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e5540b7d703774fd171b7a7dc2a3cb70e98fc273b8b260b1bf2f7d3928f125b"},
- {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d1f2d91c9c6cd54d750fa34f18bd73c71b372d0e6d06843bc7a5f21f5fd66fe0"},
- {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:76cf472c79d15dce5f438a4905a1309be57d2d01bc1de2de30bda61972a79ab4"},
- {file = "zope.interface-5.5.1-cp39-cp39-win32.whl", hash = "sha256:509a8d39b64a5e8d473f3f3db981f3ca603d27d2bc023c482605c1b52ec15662"},
- {file = "zope.interface-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c"},
- {file = "zope.interface-5.5.1.tar.gz", hash = "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2"},
+ {file = "zope.interface-5.5.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5"},
+ {file = "zope.interface-5.5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9"},
+ {file = "zope.interface-5.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f"},
+ {file = "zope.interface-5.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d"},
+ {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b"},
+ {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c"},
+ {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7"},
+ {file = "zope.interface-5.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296"},
+ {file = "zope.interface-5.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d"},
+ {file = "zope.interface-5.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d"},
+ {file = "zope.interface-5.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6"},
+ {file = "zope.interface-5.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f"},
+ {file = "zope.interface-5.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c"},
+ {file = "zope.interface-5.5.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32"},
+ {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b"},
+ {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf"},
+ {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e"},
+ {file = "zope.interface-5.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf"},
+ {file = "zope.interface-5.5.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0"},
+ {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d"},
+ {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16"},
+ {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452"},
+ {file = "zope.interface-5.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7"},
+ {file = "zope.interface-5.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e"},
+ {file = "zope.interface-5.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f"},
+ {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188"},
+ {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a"},
+ {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a"},
+ {file = "zope.interface-5.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0"},
+ {file = "zope.interface-5.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f"},
+ {file = "zope.interface-5.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4"},
+ {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396"},
+ {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc"},
+ {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"},
+ {file = "zope.interface-5.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189"},
+ {file = "zope.interface-5.5.2.tar.gz", hash = "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671"},
]
zope-sqlalchemy = [
{file = "zope.sqlalchemy-1.6-py2.py3-none-any.whl", hash = "sha256:63c4560fdd2d55c6e5658a22f6835a3c12eac1a3af8140a01400f7367aac91ed"},
diff --git a/pylint.toml b/pylint.toml
index 7495cf7..9e3a63c 100644
--- a/pylint.toml
+++ b/pylint.toml
@@ -149,7 +149,7 @@ function-naming-style = "snake_case"
# function-rgx =
# Good variable names which should always be accepted, separated by a comma.
-good-names = ["i", "j", "k", "ex", "Run", "_", "id"]
+good-names = ["i", "j", "k", "ex", "Run", "_", "id", "x", "y", "z"]
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
diff --git a/pyproject.toml b/pyproject.toml
index 9fab1a6..515c83b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,14 +37,19 @@ SQLAlchemy = "^1.4"
alembic = "^1.8"
transaction = "^3"
"zope.sqlalchemy" = "^1.6"
+redis = "^4.3.4"
+# Compatibility with old Python versions
importlib_resources = "^5.10"
+importlib_metadata = "^5.0.0"
+
Babel = "^2.11"
cryptography = "^38"
gpxpy = "^1.5"
markdown = "^3.4"
bleach = "^5"
Click = "^8.1"
+requests = "^2.28.1"
WebTest = {version = "^3", optional = true}
pytest = {version = "^7.2", optional = true}